| import { describe, it, expect, vi } from 'vitest'; |
| import { |
| buildRequestConfig, |
| fetchModels, |
| streamCompletion, |
| transcribeAudio, |
| } from '../src/api.js'; |
|
|
| |
|
|
| describe('buildRequestConfig β dev mode (isDev=true)', () => { |
| it('uses /lw-proxy as urlBase', () => { |
| const { urlBase } = buildRequestConfig('http://10.168.21.119:8000', '', true); |
| expect(urlBase).toBe('/lw-proxy'); |
| }); |
|
|
| it('sets X-LW-Target header to the cleaned baseUrl', () => { |
| const { headers } = buildRequestConfig('http://10.168.21.119:8000/', '', true); |
| expect(headers['X-LW-Target']).toBe('http://10.168.21.119:8000'); |
| }); |
|
|
| it('strips trailing slash from X-LW-Target', () => { |
| const { headers } = buildRequestConfig('http://localhost:8000/', '', true); |
| expect(headers['X-LW-Target']).toBe('http://localhost:8000'); |
| }); |
|
|
| it('does NOT add Authorization header when apiKey is empty', () => { |
| const { headers } = buildRequestConfig('http://localhost:8000', '', true); |
| expect(headers['Authorization']).toBeUndefined(); |
| }); |
|
|
| it('adds Authorization header when apiKey is provided', () => { |
| const { headers } = buildRequestConfig('http://localhost:8000', 'sk-test', true); |
| expect(headers['Authorization']).toBe('Bearer sk-test'); |
| }); |
| }); |
|
|
| describe('buildRequestConfig β prod mode (isDev=false)', () => { |
| it('uses cleaned baseUrl as urlBase', () => { |
| const { urlBase } = buildRequestConfig('http://10.168.21.119:8000/', '', false); |
| expect(urlBase).toBe('http://10.168.21.119:8000'); |
| }); |
|
|
| it('does NOT set X-LW-Target header', () => { |
| const { headers } = buildRequestConfig('http://localhost:8000', '', false); |
| expect(headers['X-LW-Target']).toBeUndefined(); |
| }); |
|
|
| it('does NOT add Authorization header when apiKey is empty', () => { |
| const { headers } = buildRequestConfig('http://localhost:8000', '', false); |
| expect(headers['Authorization']).toBeUndefined(); |
| }); |
|
|
| it('adds Authorization header when apiKey is provided', () => { |
| const { headers } = buildRequestConfig('http://localhost:8000', 'sk-key', false); |
| expect(headers['Authorization']).toBe('Bearer sk-key'); |
| }); |
| }); |
|
|
| |
|
|
| describe('fetchModels (dev mode via buildRequestConfig isDev=true default in Vitest)', () => { |
| it('calls /lw-proxy/v1/models and sets X-LW-Target', async () => { |
| const captured = {}; |
| vi.stubGlobal('fetch', vi.fn(async (url, opts) => { |
| captured.url = url; |
| captured.headers = opts?.headers ?? {}; |
| return { ok: true, json: async () => ({ data: [{ id: 'model-a' }] }) }; |
| })); |
|
|
| await fetchModels('http://10.168.21.119:8000', ''); |
| expect(captured.url).toBe('/lw-proxy/v1/models'); |
| expect(captured.headers['X-LW-Target']).toBe('http://10.168.21.119:8000'); |
| vi.unstubAllGlobals(); |
| }); |
|
|
| it('parses OpenAI-style { data: [{id}] } response', async () => { |
| vi.stubGlobal('fetch', vi.fn(async () => ({ |
| ok: true, |
| json: async () => ({ data: [{ id: 'AXERA-TECH/Qwen3-VL-2B-Instruct' }] }), |
| }))); |
|
|
| const models = await fetchModels('http://10.168.21.119:8000', ''); |
| expect(models).toContain('AXERA-TECH/Qwen3-VL-2B-Instruct'); |
| vi.unstubAllGlobals(); |
| }); |
|
|
| it('parses flat string-array response', async () => { |
| vi.stubGlobal('fetch', vi.fn(async () => ({ |
| ok: true, |
| json: async () => ['model-x', 'model-y'], |
| }))); |
|
|
| const models = await fetchModels('http://localhost:8000', ''); |
| expect(models).toContain('model-x'); |
| vi.unstubAllGlobals(); |
| }); |
|
|
| it('does NOT add Authorization header when apiKey is empty', async () => { |
| const captured = {}; |
| vi.stubGlobal('fetch', vi.fn(async (url, opts) => { |
| captured.headers = opts?.headers ?? {}; |
| return { ok: true, json: async () => ({ data: [] }) }; |
| })); |
|
|
| await fetchModels('http://localhost:8000', ''); |
| expect(captured.headers['Authorization']).toBeUndefined(); |
| vi.unstubAllGlobals(); |
| }); |
|
|
| it('adds Authorization header when apiKey is provided', async () => { |
| const captured = {}; |
| vi.stubGlobal('fetch', vi.fn(async (url, opts) => { |
| captured.headers = opts?.headers ?? {}; |
| return { ok: true, json: async () => ({ data: [] }) }; |
| })); |
|
|
| await fetchModels('http://localhost:8000', 'sk-my-key'); |
| expect(captured.headers['Authorization']).toBe('Bearer sk-my-key'); |
| vi.unstubAllGlobals(); |
| }); |
|
|
| it('throws on non-OK HTTP response', async () => { |
| vi.stubGlobal('fetch', vi.fn(async () => ({ |
| ok: false, status: 500, text: async () => 'Internal error', |
| }))); |
|
|
| await expect(fetchModels('http://localhost:8000', '')).rejects.toThrow('500'); |
| vi.unstubAllGlobals(); |
| }); |
| }); |
|
|
| |
|
|
| describe('streamCompletion (dev mode)', () => { |
| it('calls /lw-proxy/v1/chat/completions and sets X-LW-Target', async () => { |
| const captured = {}; |
| const sseChunks = [ |
| 'data: {"choices":[{"delta":{"content":"Hi"}}]}\n\n', |
| 'data: [DONE]\n\n', |
| ]; |
| let idx = 0; |
| const reader = { |
| read: vi.fn(async () => |
| idx >= sseChunks.length |
| ? { done: true } |
| : { done: false, value: new TextEncoder().encode(sseChunks[idx++]) } |
| ), |
| }; |
|
|
| vi.stubGlobal('fetch', vi.fn(async (url, opts) => { |
| captured.url = url; |
| captured.headers = opts?.headers ?? {}; |
| return { ok: true, body: { getReader: () => reader } }; |
| })); |
|
|
| const chunks = []; |
| for await (const chunk of streamCompletion('http://10.168.21.119:8000', '', 'model', [])) { |
| chunks.push(chunk); |
| } |
| expect(captured.url).toBe('/lw-proxy/v1/chat/completions'); |
| expect(captured.headers['X-LW-Target']).toBe('http://10.168.21.119:8000'); |
| expect(chunks).toContain('Hi'); |
| vi.unstubAllGlobals(); |
| }); |
| }); |
|
|
| describe('audio uploads', () => { |
| it('transcribeAudio posts multipart form data to /v1/audio/transcriptions', async () => { |
| const captured = {}; |
| vi.stubGlobal('fetch', vi.fn(async (url, opts) => { |
| captured.url = url; |
| captured.method = opts?.method; |
| captured.headers = opts?.headers ?? {}; |
| captured.body = opts?.body; |
| return { ok: true, json: async () => ({ text: 'hello transcript' }) }; |
| })); |
|
|
| const file = new File(['audio'], 'meeting.wav', { type: 'audio/wav' }); |
| const text = await transcribeAudio('http://localhost:8000', 'sk-key', 'audio-model', file); |
|
|
| expect(captured.url).toBe('/lw-proxy/v1/audio/transcriptions'); |
| expect(captured.method).toBe('POST'); |
| expect(captured.headers['Content-Type']).toBeUndefined(); |
| expect(captured.headers['Authorization']).toBe('Bearer sk-key'); |
| expect(captured.body).toBeInstanceOf(FormData); |
| expect(captured.body.get('model')).toBe('audio-model'); |
| expect(captured.body.get('file').name).toBe(file.name); |
| expect(captured.body.get('file').type).toBe(file.type); |
| expect(text).toBe('hello transcript'); |
| vi.unstubAllGlobals(); |
| }); |
| }); |
|
|