




















@@ -62,10 +62,16 @@ function latestAnthropicRequest() {
6262};
6363}
646465+function latestAnthropicRequestHeaders() {
66+return new Headers(latestAnthropicRequest().init?.headers);
67+}
68+6569function makeAnthropicTransportModel(
6670params: {
6771id?: string;
6872name?: string;
73+provider?: string;
74+baseUrl?: string;
6975reasoning?: boolean;
7076maxTokens?: number;
7177headers?: Record<string, string>;
@@ -77,8 +83,8 @@ function makeAnthropicTransportModel(
7783id: params.id ?? "claude-sonnet-4-6",
7884name: params.name ?? "Claude Sonnet 4.6",
7985api: "anthropic-messages",
80-provider: "anthropic",
81-baseUrl: "https://api.anthropic.com",
86+provider: params.provider ?? "anthropic",
87+baseUrl: params.baseUrl ?? "https://api.anthropic.com",
8288reasoning: params.reasoning ?? true,
8389input: ["text"],
8490cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
@@ -159,6 +165,97 @@ describe("anthropic transport stream", () => {
159165model: "claude-sonnet-4-6",
160166stream: true,
161167});
168+expect(latestAnthropicRequestHeaders().get("anthropic-beta")).toBe(
169+"fine-grained-tool-streaming-2025-05-14",
170+);
171+});
172+173+it("does not add implicit Anthropic beta headers for custom compatible API-key endpoints", async () => {
174+const model = makeAnthropicTransportModel({
175+provider: "anthropic",
176+baseUrl: "https://custom-proxy.example",
177+});
178+179+await runTransportStream(
180+model,
181+{
182+messages: [{ role: "user", content: "hello" }],
183+} as AnthropicStreamContext,
184+{
185+apiKey: "sk-ant-api",
186+} as AnthropicStreamOptions,
187+);
188+189+expect(guardedFetchMock).toHaveBeenCalledWith(
190+"https://custom-proxy.example/v1/messages",
191+expect.objectContaining({ method: "POST" }),
192+);
193+expect(latestAnthropicRequestHeaders().get("anthropic-beta")).toBeNull();
194+});
195+196+it("does not add implicit Anthropic beta headers for custom compatible OAuth endpoints", async () => {
197+await runTransportStream(
198+makeAnthropicTransportModel({
199+provider: "anthropic",
200+baseUrl: "https://custom-proxy.example",
201+}),
202+{
203+messages: [{ role: "user", content: "hello" }],
204+} as AnthropicStreamContext,
205+{
206+apiKey: "sk-ant-oat-token",
207+} as AnthropicStreamOptions,
208+);
209+210+const headers = latestAnthropicRequestHeaders();
211+expect(headers.get("authorization")).toBe("Bearer sk-ant-oat-token");
212+expect(headers.get("anthropic-beta")).toBeNull();
213+});
214+215+it("keeps Anthropic beta headers for direct Anthropic OAuth endpoints", async () => {
216+await runTransportStream(
217+makeAnthropicTransportModel(),
218+{
219+messages: [{ role: "user", content: "hello" }],
220+} as AnthropicStreamContext,
221+{
222+apiKey: "sk-ant-oat-token",
223+} as AnthropicStreamOptions,
224+);
225+226+expect(latestAnthropicRequestHeaders().get("anthropic-beta")).toBe(
227+"claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14",
228+);
229+});
230+231+it("recognizes schemeless api.anthropic.com base URLs as direct Anthropic", async () => {
232+await runTransportStream(
233+makeAnthropicTransportModel({ baseUrl: "api.anthropic.com" }),
234+{
235+messages: [{ role: "user", content: "hello" }],
236+} as AnthropicStreamContext,
237+{
238+apiKey: "sk-ant-api",
239+} as AnthropicStreamOptions,
240+);
241+242+expect(latestAnthropicRequestHeaders().get("anthropic-beta")).toBe(
243+"fine-grained-tool-streaming-2025-05-14",
244+);
245+});
246+247+it("does not add implicit Anthropic beta headers for foreign hosts mentioning api.anthropic.com", async () => {
248+await runTransportStream(
249+makeAnthropicTransportModel({ baseUrl: "https://attacker.example/api.anthropic.com" }),
250+{
251+messages: [{ role: "user", content: "hello" }],
252+} as AnthropicStreamContext,
253+{
254+apiKey: "sk-ant-api",
255+} as AnthropicStreamOptions,
256+);
257+258+expect(latestAnthropicRequestHeaders().get("anthropic-beta")).toBeNull();
162259});
163260164261it("ignores non-positive runtime maxTokens overrides and falls back to the model limit", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。