import { describe, expect, it } from "vitest"; import { AnthropicAdapter } from "./providers/anthropic.js"; import { GoogleAdapter } from "./providers/google.js"; import { OpenAIAdapter, OssAdapter } from "./providers/openai-compatible.js"; function sseResponse(events: string[]): Response { const body = events.map((e) => `data: ${e}\n\n`).join("") + "data: [DONE]\n\n"; return new Response(body, { status: 200, headers: { "content-type": "text/event-stream" } }); } function jsonResponse(value: unknown, status = 200): Response { return new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } }); } function fakeFetch(handler: (url: string, init?: RequestInit) => Response): { fn: typeof fetch; calls: Array<{ url: string; body?: Record }>; } { const calls: Array<{ url: string; body?: Record }> = []; const fn = ((url: string, init?: RequestInit) => { const body = init?.body ? (JSON.parse(init.body as string) as Record) : undefined; calls.push(body === undefined ? { url } : { url, body }); return Promise.resolve(handler(url, init)); }) as typeof fetch; return { fn, calls }; } describe("OpenAIAdapter", () => { it("streams SSE deltas + tool calls + final usage", async () => { const events = [ JSON.stringify({ choices: [{ delta: { role: "assistant", content: "Hel" } }] }), JSON.stringify({ choices: [{ delta: { content: "lo " } }] }), JSON.stringify({ choices: [{ delta: { tool_calls: [{ index: 0, id: "call_1", function: { name: "search", arguments: '{"q":' } }] } }], }), JSON.stringify({ choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '"ai"}' } }] } }], }), JSON.stringify({ choices: [{ delta: {}, finish_reason: "tool_calls" }], usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, }), ]; const { fn } = fakeFetch(() => sseResponse(events)); const adapter = new OpenAIAdapter({ fetchImpl: fn }); const deltas: string[] = []; const toolCalls: string[] = []; let usage: { in: number; out: number; total: number } | undefined; let finishReason: string | undefined; for await (const chunk of adapter.chat({ model: "gpt-4o", messages: [{ role: "user", content: "hi" }], tools: [{ name: "search", description: "d", parameters: { type: "object" } }], })) { if (chunk.delta) deltas.push(chunk.delta); if (chunk.toolCallDelta) toolCalls.push(chunk.toolCallDelta.name); if (chunk.done) { usage = chunk.usage; finishReason = chunk.finishReason; } } expect(deltas.join("")).toBe("Hello "); expect(toolCalls).toEqual(["search"]); expect(usage).toEqual({ in: 10, out: 5, total: 15 }); expect(finishReason).toBe("tool_calls"); }); it("sends Bearer auth and the OpenAI body shape", async () => { let seenHeaders: Record = {}; let seenBody: Record = {}; const fn = ((url: string, init?: RequestInit) => { seenHeaders = (init?.headers as Record) ?? {}; seenBody = JSON.parse((init?.body as string) ?? "{}"); return Promise.resolve( sseResponse([JSON.stringify({ choices: [{ delta: { content: "y" }, finish_reason: "stop" }] })]), ); }) as typeof fetch; const adapter = new OpenAIAdapter({ fetchImpl: fn, apiKey: "sk-test" }); for await (const _ of adapter.chat({ model: "gpt-4o", messages: [{ role: "user", content: "hi" }] })) { // collect } expect(seenHeaders.authorization).toBe("Bearer sk-test"); expect(seenBody.stream).toBe(true); expect(seenBody.stream_options).toEqual({ include_usage: true }); expect(seenBody.messages).toEqual([{ role: "user", content: "hi" }]); }); it("maps non-ok responses to retryable ProviderHttpError", async () => { const { fn } = fakeFetch(() => jsonResponse({ error: { message: "rate limited" } }, 429)); const adapter = new OpenAIAdapter({ fetchImpl: fn }); let caught: unknown; try { for await (const _ of adapter.chat({ model: "gpt-4o", messages: [{ role: "user", content: "hi" }] })) { // collect } } catch (e) { caught = e; } expect(caught).toBeDefined(); expect((caught as { status: number }).status).toBe(429); expect((caught as { retryable: boolean }).retryable).toBe(true); }); it("embed returns vectors in index order and the dimension", async () => { const { fn } = fakeFetch(() => jsonResponse({ data: [ { index: 1, embedding: [0.2, 0.3] }, { index: 0, embedding: [0.1, 0.2] }, ], model: "text-embedding-3-small", usage: { prompt_tokens: 7, total_tokens: 7 }, }), ); const adapter = new OpenAIAdapter({ fetchImpl: fn }); const result = await adapter.embed({ model: "text-embedding-3-small", texts: ["a", "b"] }); expect(result.vectors).toEqual([ [0.1, 0.2], [0.2, 0.3], ]); expect(result.dimension).toBe(2); expect(result.usage.tokens).toBe(7); }); it("OssAdapter discovers the dimension at first use", async () => { const { fn } = fakeFetch(() => jsonResponse({ data: [{ index: 0, embedding: [1, 0, 0] }], model: "nomic-embed-text", usage: { total_tokens: 1 } }), ); const adapter = new OssAdapter({ fetchImpl: fn, baseUrl: "http://ollama:11434/v1" }); expect(adapter.dimension).toBeUndefined(); const result = await adapter.embed({ model: "nomic-embed-text", texts: ["x"] }); expect(result.dimension).toBe(3); expect(adapter.dimension).toBe(3); }); }); describe("AnthropicAdapter", () => { it("maps system to the top-level param and streams text", async () => { const events = [ JSON.stringify({ type: "message_start", message: { usage: { input_tokens: 5 } } }), JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hi" } }), JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: " there" } }), JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 3 } }), ]; const { fn, calls } = fakeFetch(() => sseResponse(events)); const adapter = new AnthropicAdapter({ fetchImpl: fn, apiKey: "key" }); const text: string[] = []; let usage: { in: number; out: number; total: number } | undefined; for await (const chunk of adapter.chat({ model: "claude-sonnet-4-20250514", messages: [ { role: "system", content: "be nice" }, { role: "user", content: "hello" }, ], })) { if (chunk.delta) text.push(chunk.delta); if (chunk.done) usage = chunk.usage; } expect(text.join("")).toBe("Hi there"); expect(usage).toEqual({ in: 5, out: 3, total: 8 }); const body = calls[0]!.body!; expect(body.system).toBe("be nice"); expect((body.messages as unknown[]).map((m) => (m as { role: string }).role)).toEqual(["user"]); }); it("accumulates tool_use blocks (id/name + partial_json) into ToolCall deltas", async () => { const events = [ JSON.stringify({ type: "message_start", message: { usage: { input_tokens: 5 } } }), JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "tool_use", id: "toolu_1", name: "search" } }), JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: '{"q":' } }), JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: '"ai"}' } }), JSON.stringify({ type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 9 } }), ]; const { fn } = fakeFetch(() => sseResponse(events)); const adapter = new AnthropicAdapter({ fetchImpl: fn, apiKey: "key" }); const toolCalls: Array<{ id: string; name: string; arguments: Record }> = []; let finishReason: string | undefined; for await (const chunk of adapter.chat({ model: "claude-sonnet-4-20250514", messages: [{ role: "user", content: "find ai" }], tools: [{ name: "search", description: "d", parameters: { type: "object" } }], })) { if (chunk.toolCallDelta) toolCalls.push(chunk.toolCallDelta); if (chunk.done) finishReason = chunk.finishReason; } expect(toolCalls).toEqual([{ id: "toolu_1", name: "search", arguments: { q: "ai" } }]); expect(finishReason).toBe("tool_calls"); }); it("embed throws (no API) with a non-retryable 501", async () => { const adapter = new AnthropicAdapter({ fetchImpl: (async () => new Response("{}")) as typeof fetch, apiKey: "k" }); const err = await adapter.embed({ model: "x", texts: ["y"] }).catch((e: unknown) => e); expect((err as { status: number }).status).toBe(501); expect((err as { retryable: boolean }).retryable).toBe(false); }); it("sends x-api-key and anthropic-version headers", async () => { let seen: Record = {}; const fn = ((url: string, init?: RequestInit) => { seen = init?.headers as Record; return Promise.resolve( sseResponse([JSON.stringify({ type: "message_start", message: { usage: { input_tokens: 1 } } })]), ); }) as typeof fetch; const adapter = new AnthropicAdapter({ fetchImpl: fn, apiKey: "secret" }); for await (const _ of adapter.chat({ model: "claude-3-5-haiku-20241022", messages: [{ role: "user", content: "x" }] })) { // collect } expect(seen["x-api-key"]).toBe("secret"); expect(seen["anthropic-version"]).toBe("2023-06-01"); }); }); describe("GoogleAdapter", () => { it("streams text + functionCall and maps usage", async () => { const events = [ JSON.stringify({ candidates: [{ content: { role: "model", parts: [{ text: "Hello " }] }, finishReason: "STOP" }], }), JSON.stringify({ candidates: [{ content: { role: "model", parts: [{ text: "world" }] } }], usageMetadata: { promptTokenCount: 4, candidatesTokenCount: 2, totalTokenCount: 6 }, }), ]; const { fn, calls } = fakeFetch(() => sseResponse(events)); const adapter = new GoogleAdapter({ fetchImpl: fn, apiKey: "gkey" }); const text: string[] = []; let usage: { in: number; out: number; total: number } | undefined; for await (const chunk of adapter.chat({ model: "gemini-2.0-flash", messages: [{ role: "user", content: "hi" }] })) { if (chunk.delta) text.push(chunk.delta); if (chunk.done) usage = chunk.usage; } expect(text.join("")).toBe("Hello world"); expect(usage).toEqual({ in: 4, out: 2, total: 6 }); expect(calls[0]!.url).toContain("models/gemini-2.0-flash:streamGenerateContent"); expect(calls[0]!.url).toContain("alt=sse"); }); it("maps system → systemInstruction and assistant tool calls → functionCall parts", async () => { let body: Record = {}; const fn = ((url: string, init?: RequestInit) => { body = JSON.parse(init!.body as string); return Promise.resolve( sseResponse([ JSON.stringify({ candidates: [{ content: { role: "model", parts: [{ functionCall: { name: "search", args: { q: "x" } } }] } }], }), ]), ); }) as typeof fetch; const adapter = new GoogleAdapter({ fetchImpl: fn, apiKey: "gkey" }); const toolCalls: string[] = []; for await (const chunk of adapter.chat({ model: "gemini-2.0-flash", messages: [ { role: "system", content: "sys" }, { role: "assistant", content: "", toolCalls: [{ id: "c1", name: "search", arguments: { q: "x" } }], }, { role: "user", content: "more" }, ], })) { if (chunk.toolCallDelta) toolCalls.push(chunk.toolCallDelta.name); } expect(toolCalls).toEqual(["search"]); expect((body.systemInstruction as { parts: { text: string }[] }).parts[0]!.text).toBe("sys"); const contents = body.contents as Array<{ role: string; parts: Array> }>; const assistant = contents.find((c) => c.role === "model")!; expect(assistant.parts[0]!.functionCall).toEqual({ name: "search", args: { q: "x" } }); }); it("embed uses batchEmbedContents and reports the dimension", async () => { const { fn } = fakeFetch(() => jsonResponse({ embeddings: [{ values: [0.1, 0.2, 0.3] }, { values: [0.4, 0.5, 0.6] }] }), ); const adapter = new GoogleAdapter({ fetchImpl: fn, apiKey: "gkey" }); const result = await adapter.embed({ model: "text-embedding-004", texts: ["a", "b"] }); expect(result.dimension).toBe(3); expect(result.vectors).toHaveLength(2); expect(result.provider).toBe("google"); }); }); // ── TASK-0122: caller cancellation (AbortSignal) ────────────────────────────── /** An SSE response that flushes two deltas and then HANGS (never closes) — * the only thing that can end it is the caller's abort. */ function hangingSseResponse(): Response { const stream = new ReadableStream({ start(ctrl) { const enc = new TextEncoder(); ctrl.enqueue(enc.encode(`data: ${JSON.stringify({ choices: [{ delta: { content: "a" } }] })}\n\n`)); ctrl.enqueue(enc.encode(`data: ${JSON.stringify({ choices: [{ delta: { content: "b" } }] })}\n\n`)); // then never read more — the third reader.read() hangs until abort }, }); return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } }); } describe("caller abort (TASK-0122)", () => { it("aborts the provider stream mid-flight and surfaces AbortError (not a wrapped 502)", async () => { const ac = new AbortController(); const { fn } = fakeFetch(() => hangingSseResponse()); const adapter = new OpenAIAdapter({ fetchImpl: fn }); const deltas: string[] = []; const run = (async () => { for await (const chunk of adapter.chat({ model: "gpt-4o", messages: [{ role: "user", content: "hi" }], stream: true, signal: ac.signal, })) { if (chunk.delta) deltas.push(chunk.delta); } })(); // let both deltas flush, then abort while the third read is hanging await new Promise((r) => setTimeout(r, 15)); ac.abort(); let err: unknown; try { await run; } catch (e) { err = e; } expect(err).toBeDefined(); expect((err as Error).name).toBe("AbortError"); expect(deltas).toEqual(["a", "b"]); }); });