1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
|
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, string>
>;
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<string, string>
>;
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<string, string>;
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<string, string>;
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<string, unknown>;
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<string, string>;
expect(callArgs.baseURL).toBe("https://opencode.ai/zen/go/v1");
});
});
|