From fb9fab0206aa753e104ae4620e6ddcfb7a2ea5b2 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Sat, 5 Sep 2026 21:25:28 +0700 Subject: [PATCH] fix(anthropic-compatible): send Claude beta flags to nodes fronting Anthropic (#3797) --- open-sse/executors/default.js | 13 ++++++- tests/unit/claude-header-forwarding.test.js | 40 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index 92c78e92..00f3e3de 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -154,7 +154,18 @@ export class DefaultExecutor extends BaseExecutor { for (const hook of desc.hooks || []) HEADER_HOOKS[hook]?.(headers, credentials); applyAuth(headers, desc, credentials); - if (this.provider === "claude" && model) { + // anthropic-compatible-* nodes serving a real Claude model sit in front of + // Anthropic itself (a rotating multi-account proxy, a corporate gateway), + // so the request needs the same beta flags the `claude` provider sends: + // without `context-management-2025-06-27` upstream rejects the + // `context_management` block Claude Code puts in every request with + // "context_management: Extra inputs are not permitted" (HTTP 400), and the + // combo silently falls through to the next model. The model id gates this: + // a node fronting Kimi or GLM answers on its own ids and never matches, so + // gateways that would choke on unknown beta flags are left untouched. + const isClaudeModel = typeof model === "string" && /^claude-/.test(model); + if (model && (this.provider === "claude" + || (this.provider?.startsWith?.("anthropic-compatible-") && isClaudeModel))) { headers["Anthropic-Beta"] = selectAnthropicBeta(model); } diff --git a/tests/unit/claude-header-forwarding.test.js b/tests/unit/claude-header-forwarding.test.js index 840a8559..d813f356 100644 --- a/tests/unit/claude-header-forwarding.test.js +++ b/tests/unit/claude-header-forwarding.test.js @@ -187,6 +187,46 @@ describe("DefaultExecutor.buildHeaders() — anthropic-compatible stripping", () headers["Anthropic-Version"] || headers["anthropic-version"]; expect(hasVersion).toBeDefined(); }); + + // A node fronting Anthropic (rotating multi-account proxy, corporate gateway) + // needs the same beta flags the `claude` provider sends. Without + // context-management-2025-06-27 upstream answers HTTP 400 + // "context_management: Extra inputs are not permitted" and the combo falls + // through to the next model without anyone noticing. + it("sends context-management beta for a Claude model on a custom host", () => { + const executor = new DefaultExecutor("anthropic-compatible-custom"); + const headers = executor.buildHeaders( + { + apiKey: "key", + providerSpecificData: { baseUrl: "https://myproxy.example.com/v1" }, + }, + true, + undefined, + "claude-opus-5" + ); + + const betaFlags = (headers["Anthropic-Beta"] || headers["anthropic-beta"] || "") + .split(",").map(s => s.trim()); + expect(betaFlags).toContain("context-management-2025-06-27"); + // The first-party identity flag is still stripped for a non-Anthropic host. + expect(betaFlags).not.toContain("claude-code-20250219"); + }); + + it("gates the beta flags on the model id, not the provider prefix", () => { + const executor = new DefaultExecutor("anthropic-compatible-custom"); + const headers = executor.buildHeaders( + { + apiKey: "key", + providerSpecificData: { baseUrl: "https://myproxy.example.com/v1" }, + }, + true, + undefined, + "kimi-k3" + ); + + const betaVal = headers["Anthropic-Beta"] || headers["anthropic-beta"] || ""; + expect(betaVal).not.toContain("context-management-2025-06-27"); + }); }); // ─── proxyFetch anthropicFetch routing ────────────────────────────────────────