import { describe, expect, it } from 'vitest'; import { createLlmClient } from './llm'; function json(body: unknown) { return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body), } as unknown as Response; } describe('createLlmClient (injectable transport)', () => { it('chat posts model+messages to {base}/chat/completions and returns content', async () => { const calls: { url: string; init?: RequestInit }[] = []; const transport = async (url: string, init?: RequestInit) => { calls.push({ url, init }); return json({ choices: [{ message: { content: 'hello back' } }] }); }; const llm = createLlmClient({ transport, apiBase: 'http://x/v1/', model: 'm1' }); const out = await llm.chat([{ role: 'user', content: 'hi' }], { maxTokens: 10, temperature: 0.5 }); expect(out).toBe('hello back'); expect(calls[0].url).toBe('http://x/v1/chat/completions'); // trailing slash normalized const body = JSON.parse(calls[0].init!.body as string); expect(body).toEqual({ model: 'm1', messages: [{ role: 'user', content: 'hi' }], maxTokens: 10, temperature: 0.5, }); }); it('listModels returns data[].id from GET {base}/models', async () => { const transport = async (url: string) => { expect(url).toBe('http://x/v1/models'); return json({ data: [{ id: 'a' }, { id: 'b' }] }); }; const llm = createLlmClient({ transport, apiBase: 'http://x/v1', model: 'm' }); expect(await llm.listModels()).toEqual(['a', 'b']); }); it('chat throws on non-2xx with the HTTP status in the message', async () => { const transport = async () => ({ ok: false, status: 503, text: async () => 'upstream down', json: async () => ({}), } as unknown as Response); const llm = createLlmClient({ transport, apiBase: 'http://x/v1', model: 'm' }); await expect(llm.chat([{ role: 'user', content: 'hi' }])).rejects.toThrow(/HTTP 503/); }); it('chat throws when the response has no content', async () => { const transport = async () => json({ choices: [] }); const llm = createLlmClient({ transport, apiBase: 'http://x/v1', model: 'm' }); await expect(llm.chat([{ role: 'user', content: 'hi' }])).rejects.toThrow(/no choices/); }); });