import { describe, expect, it, vi } from "vitest"; // Mock @ai-sdk/anthropic to capture what options createAnthropic is called with const mockAnthropicInstance = vi.fn((modelId: string) => ({ specificationVersion: "v3" as const, provider: "anthropic.messages", modelId, supportedUrls: {}, doGenerate: vi.fn(), doStream: vi.fn(), })); const mockCreateAnthropic = vi.fn(() => mockAnthropicInstance); vi.mock("@ai-sdk/anthropic", () => ({ createAnthropic: mockCreateAnthropic, })); // Mock @ai-sdk/openai-compatible to capture what options createOpenAICompatible // is called with, and what model id the returned factory is called with. const mockOpenAICompatibleFactory = vi.fn((modelId: string) => ({ specificationVersion: "v3" as const, provider: "openai-compatible", modelId, supportedUrls: {}, doGenerate: vi.fn(), doStream: vi.fn(), })); const mockCreateOpenAICompatible = vi.fn(() => mockOpenAICompatibleFactory); vi.mock("@ai-sdk/openai-compatible", () => ({ createOpenAICompatible: mockCreateOpenAICompatible, })); const { createProvider } = await import("../../src/llm/provider.js"); describe("createProvider (default OpenAI-compatible path)", () => { it("does not wrap the model in a middleware layer — v6 SDK handles reasoning round-trip natively", () => { mockCreateOpenAICompatible.mockClear(); mockOpenAICompatibleFactory.mockClear(); const model = createProvider({ apiKey: "test-key", baseURL: "https://example.com/v1", })("deepseek-v4-pro"); // The factory should have been invoked with the model id directly, // without going through `wrapLanguageModel`. If a middleware were // still in place, the returned object would carry an `_middleware` // property (set by our test mock pattern). The bare provider model // has no such property — verifying the v4-era normalizeMessages // middleware is gone. expect(mockOpenAICompatibleFactory).toHaveBeenCalledWith("deepseek-v4-pro"); expect((model as { _middleware?: unknown })._middleware).toBeUndefined(); }); it("passes name, apiKey, baseURL to createOpenAICompatible", () => { mockCreateOpenAICompatible.mockClear(); createProvider({ apiKey: "zen-key", baseURL: "https://opencode.ai/zen/v1", })("deepseek-v4-pro"); // We assert by property rather than full-object equality because the // provider also passes a `fetch:` wrapper (the debug-logger tee). The // load-bearing wiring is name/apiKey/baseURL; the fetch field is // tested separately via the wrap-fetch tests. expect(mockCreateOpenAICompatible).toHaveBeenCalledOnce(); const zenArgs = mockCreateOpenAICompatible.mock.calls[0]?.[0] as Record; expect(zenArgs.name).toBe("opencode-zen"); expect(zenArgs.apiKey).toBe("zen-key"); expect(zenArgs.baseURL).toBe("https://opencode.ai/zen/v1"); expect(typeof zenArgs.fetch).toBe("function"); }); }); describe("createClaudeOAuthProvider", () => { it("passes authToken (not apiKey) to createAnthropic for OAuth flow", () => { mockCreateAnthropic.mockClear(); createProvider({ provider: "anthropic", apiKey: "fallback-api-key", baseURL: "", claudeCredentials: { accessToken: "oauth-access-token" }, })("claude-opus-4-5"); expect(mockCreateAnthropic).toHaveBeenCalledOnce(); const callArgs = mockCreateAnthropic.mock.calls[0]?.[0] as Record; expect(callArgs.authToken).toBe("oauth-access-token"); expect(callArgs.apiKey).toBeUndefined(); }); it("falls back to apiKey as authToken when claudeCredentials are absent", () => { mockCreateAnthropic.mockClear(); createProvider({ provider: "anthropic", apiKey: "sk-ant-api-key", baseURL: "", })("claude-opus-4-5"); expect(mockCreateAnthropic).toHaveBeenCalledOnce(); const callArgs = mockCreateAnthropic.mock.calls[0]?.[0] as Record; expect(callArgs.authToken).toBe("sk-ant-api-key"); expect(callArgs.apiKey).toBeUndefined(); }); it("includes required Claude CLI headers", () => { mockCreateAnthropic.mockClear(); createProvider({ provider: "anthropic", apiKey: "test-key", baseURL: "", claudeCredentials: { accessToken: "tok" }, })("claude-opus-4-5"); const callArgs = mockCreateAnthropic.mock.calls[0]?.[0] as Record< string, Record >; expect(callArgs.headers?.["anthropic-dangerous-direct-browser-access"]).toBe("true"); expect(callArgs.headers?.["x-app"]).toBe("cli"); expect(callArgs.headers?.["user-agent"]).toMatch(/claude-cli/); }); it("installs a fetch wrapper that restructures the body and stamps Claude Code session headers", async () => { mockCreateAnthropic.mockClear(); // Capture what global fetch receives after the wrapper runs. const globalFetchMock = vi.fn(async () => new Response("{}", { status: 200 })); const prevFetch = globalThis.fetch; globalThis.fetch = globalFetchMock as unknown as typeof fetch; try { createProvider({ provider: "anthropic", apiKey: "test-key", baseURL: "", claudeCredentials: { accessToken: "tok" }, })("claude-opus-4-8"); const callArgs = mockCreateAnthropic.mock.calls[0]?.[0] as { fetch?: typeof fetch }; expect(typeof callArgs.fetch).toBe("function"); const IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude."; const BILLING = "x-anthropic-billing-header: cc_version=2.1.112.x; cc_entrypoint=sdk-cli; cch=abcde;"; const body = JSON.stringify({ system: [ { type: "text", text: `${BILLING}\n${IDENTITY}\n\nDispatch system prompt.`, cache_control: { type: "ephemeral" }, }, ], messages: [{ role: "user", content: "hi there" }], }); await callArgs.fetch?.("https://api.anthropic.com/v1/messages", { method: "POST", body, headers: { "content-type": "application/json" }, }); expect(globalFetchMock).toHaveBeenCalledOnce(); const [, init] = globalFetchMock.mock.calls[0] as unknown as [unknown, RequestInit]; // Body was restructured: billing isolated, third-party prompt moved to user msg. const sent = JSON.parse(init.body as string) as { system: Array<{ text: string; cache_control?: unknown }>; messages: Array<{ content: string }>; }; expect(sent.system).toHaveLength(2); expect(sent.system[0]?.text).toBe(BILLING); expect(sent.system[0]?.cache_control).toBeUndefined(); expect(sent.system[1]?.text).toBe(IDENTITY); expect(sent.messages[0]?.content).toBe("Dispatch system prompt.\n\nhi there"); // Claude Code session headers were stamped. const headers = new Headers(init.headers); expect(headers.get("X-Claude-Code-Session-Id")).toBeTruthy(); expect(headers.get("x-client-request-id")).toBeTruthy(); } finally { globalThis.fetch = prevFetch; } }); it("sends the anthropic-beta header so prompt-caching is honored (notes/claude-report.md Root Cause 1)", () => { // Without `anthropic-beta: ...,prompt-caching-scope-2026-01-05,...` the // Anthropic API silently ignores every `cache_control` marker we attach // to messages, producing a 0% cache hit rate. `@ai-sdk/anthropic` does // NOT inject this beta on its own — it only derives betas from tool // definitions — so the OAuth provider MUST set it on its config headers. mockCreateAnthropic.mockClear(); createProvider({ provider: "anthropic", apiKey: "test-key", baseURL: "", claudeCredentials: { accessToken: "tok" }, })("claude-opus-4-5"); const callArgs = mockCreateAnthropic.mock.calls[0]?.[0] as Record< string, Record >; const betaHeader = callArgs.headers?.["anthropic-beta"]; expect(betaHeader).toBeDefined(); const betas = (betaHeader ?? "").split(",").map((b) => b.trim()); // The load-bearing caching + oauth betas must be present. expect(betas).toContain("prompt-caching-scope-2026-01-05"); expect(betas).toContain("oauth-2025-04-20"); }); it("uses default Anthropic baseURL when none provided", () => { mockCreateAnthropic.mockClear(); createProvider({ provider: "anthropic", apiKey: "test-key", baseURL: "", claudeCredentials: { accessToken: "tok" }, })("claude-opus-4-5"); const callArgs = mockCreateAnthropic.mock.calls[0]?.[0] as Record; expect(callArgs.baseURL).toBe("https://api.anthropic.com/v1"); }); it("uses configured baseURL when provided", () => { mockCreateAnthropic.mockClear(); createProvider({ provider: "anthropic", apiKey: "test-key", baseURL: "https://custom.proxy.example.com/v1", claudeCredentials: { accessToken: "tok" }, })("claude-opus-4-5"); const callArgs = mockCreateAnthropic.mock.calls[0]?.[0] as Record; expect(callArgs.baseURL).toBe("https://custom.proxy.example.com/v1"); }); }); describe("createApiKeyAnthropicProvider", () => { it("passes apiKey (not authToken) to createAnthropic", () => { mockCreateAnthropic.mockClear(); createProvider({ provider: "opencode-anthropic", apiKey: "zen-api-key", baseURL: "", })("minimax-model"); expect(mockCreateAnthropic).toHaveBeenCalledOnce(); const callArgs = mockCreateAnthropic.mock.calls[0]?.[0] as Record; expect(callArgs.apiKey).toBe("zen-api-key"); expect(callArgs.authToken).toBeUndefined(); }); it("uses default OpenCode Zen baseURL when none provided", () => { mockCreateAnthropic.mockClear(); createProvider({ provider: "opencode-anthropic", apiKey: "zen-api-key", baseURL: "", })("minimax-model"); const callArgs = mockCreateAnthropic.mock.calls[0]?.[0] as Record; expect(callArgs.baseURL).toBe("https://opencode.ai/zen/go/v1"); }); });