import type { ApiKeyCredentials, ChatMessage, ProviderStreamOptions } from "@dispatch/kernel"; import type { FetchLike } from "@dispatch/trace-replay"; import { describe, expect, it, vi } from "vitest"; import { createOpenAICompatProvider } from "./provider.js"; function makeCreds(): ApiKeyCredentials { return { type: "api-key", apiKey: "sk-test-1234567890abcdef", baseURL: "https://api.example.com/v1", }; } function makeMessages(): readonly ChatMessage[] { return [{ role: "user", chunks: [{ type: "text", text: "Hello" }] }]; } function sseBody(...lines: string[]): ReadableStream { const encoder = new TextEncoder(); const chunks = lines.map((l) => encoder.encode(`${l}\n`)); let index = 0; return new ReadableStream({ pull(controller) { if (index < chunks.length) { const chunk = chunks[index]; if (chunk === undefined) throw new Error("empty chunk"); controller.enqueue(chunk); index++; } else { controller.close(); } }, }); } function okSseResponse(): Response { return new Response( sseBody( 'data: {"id":"cmpl-1","choices":[{"delta":{"content":"Hi"},"index":0}]}', 'data: {"id":"cmpl-1","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ); } async function collectEvents(iter: AsyncIterable): Promise { const events: unknown[] = []; for await (const event of iter) { events.push(event); } return events; } describe("createOpenAICompatProvider stamps the given id on the ProviderContract + listModels", () => { it("stamps opts.id on ProviderContract.id", () => { const provider = createOpenAICompatProvider({ credentials: makeCreds(), model: "test-model", id: "my-custom-id", }); expect(provider.id).toBe("my-custom-id"); }); it("uses opts.id in listModels error labels (was hardcoded 'openai-compat')", async () => { const fetchFn = vi.fn( () => new Response("Unauthorized", { status: 401, headers: { "Content-Type": "text/plain" }, }) as unknown as ReturnType, ); const provider = createOpenAICompatProvider({ credentials: makeCreds(), model: "test-model", id: "my-custom-id", fetchFn, }); const listModels = provider.listModels; if (!listModels) throw new Error("listModels not defined"); await expect(listModels()).rejects.toThrow("listModels[my-custom-id]: HTTP 401 — Unauthorized"); }); }); describe("transformBody", () => { it("transformBody merges its returned fields into the request body", async () => { let capturedInit: RequestInit | undefined; const fetchFn = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { capturedInit = init; return okSseResponse(); }) as unknown as FetchLike; let receivedBody: Record | undefined; let receivedOpts: ProviderStreamOptions | undefined; const provider = createOpenAICompatProvider({ credentials: makeCreds(), model: "test-model", id: "umans", fetchFn, transformBody: (body, opts) => { receivedBody = body; receivedOpts = opts; return { reasoning_effort: "high" }; }, }); await collectEvents(provider.stream(makeMessages(), [], { temperature: 0.5 })); // The hook was called with the body built so far + the stream opts. expect(receivedBody).toBeDefined(); expect(receivedOpts?.temperature).toBe(0.5); expect(receivedBody?.model).toBe("test-model"); // The captured wire body carries the merged field. expect(capturedInit?.body).toBeTypeOf("string"); const wireBody = JSON.parse(capturedInit?.body as string) as Record; expect(wireBody.reasoning_effort).toBe("high"); expect(wireBody.model).toBe("test-model"); expect(wireBody.stream).toBe(true); expect(wireBody.temperature).toBe(0.5); }); it("transformBody absent → body byte-identical to before (regression)", async () => { let capturedInit: RequestInit | undefined; const fetchFn = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { capturedInit = init; return okSseResponse(); }) as unknown as FetchLike; const provider = createOpenAICompatProvider({ credentials: makeCreds(), model: "test-model", id: "openai-compat", fetchFn, // No transformBody — default behavior. }); await collectEvents(provider.stream(makeMessages(), [], { temperature: 0.5, maxTokens: 42 })); expect(capturedInit?.body).toBeTypeOf("string"); const wireBody = JSON.parse(capturedInit?.body as string) as Record; // Exact pre-refactor shape — no extra fields, no transformBody key leakage. expect(wireBody).toEqual({ model: "test-model", messages: [{ role: "user", content: "Hello" }], stream: true, stream_options: { include_usage: true }, temperature: 0.5, max_tokens: 42, }); expect("reasoning_effort" in wireBody).toBe(false); }); });