From 1533f16ed8faf299257bc989e02b6d982c3698d1 Mon Sep 17 00:00:00 2001 From: Felipe Fidelix <309092+fidelix@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:29:54 -0300 Subject: [PATCH] fix(claude): forward client-negotiated thinking-binding-controls and thinking-display-updates betas (#12989) * fix(claude): forward client-negotiated thinking-binding-controls and thinking-display-updates betas Gateways drops anthropic-beta tokens not on FORWARDABLE_CLIENT_BETAS, so @ai-sdk/anthropic Fable 5.1 requests carrying thinking.block_binding were rejected upstream with thinking.adaptive.block_binding: Extra inputs are not permitted even when the client negotiated thinking-binding-controls-2026-08-01 correctly. Same class for thinking.display updates (thinking-display-updates-2026-08-18). Regression guard: tests/unit/thinking-binding-controls-beta-forward.test.ts * chore(changelog): fragment for #12989 * chore: trim comments --- ...-thinking-binding-controls-beta-forward.md | 1 + open-sse/config/anthropicHeaders.ts | 4 ++ ...king-binding-controls-beta-forward.test.ts | 69 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 changelog.d/fixes/12989-thinking-binding-controls-beta-forward.md create mode 100644 tests/unit/thinking-binding-controls-beta-forward.test.ts diff --git a/changelog.d/fixes/12989-thinking-binding-controls-beta-forward.md b/changelog.d/fixes/12989-thinking-binding-controls-beta-forward.md new file mode 100644 index 0000000000..9c8a32234e --- /dev/null +++ b/changelog.d/fixes/12989-thinking-binding-controls-beta-forward.md @@ -0,0 +1 @@ +- **fix(claude):** forward client-negotiated `thinking-binding-controls-2026-08-01` and `thinking-display-updates-2026-08-18` betas so Fable 5.1 `thinking.block_binding` / `thinking.display` requests are no longer rejected upstream with `Extra inputs are not permitted` ([#12989](https://github.com/diegosouzapw/OmniRoute/pull/12989)) diff --git a/open-sse/config/anthropicHeaders.ts b/open-sse/config/anthropicHeaders.ts index 6625c4dd07..afbfaceead 100644 --- a/open-sse/config/anthropicHeaders.ts +++ b/open-sse/config/anthropicHeaders.ts @@ -65,6 +65,10 @@ export const FORWARDABLE_CLIENT_BETAS = Object.freeze([ // gate (#9505), so a client that sent it must keep it through the merge — // otherwise its effort negotiation is silently dropped. "effort-2025-11-24", + // Fable 5.1 betas (@ai-sdk/anthropic sends both automatically): without them + // upstream rejects `thinking.block_binding` / `thinking.display` with 400. + "thinking-binding-controls-2026-08-01", + "thinking-display-updates-2026-08-18", ]); /** diff --git a/tests/unit/thinking-binding-controls-beta-forward.test.ts b/tests/unit/thinking-binding-controls-beta-forward.test.ts new file mode 100644 index 0000000000..ee2a3ae6ef --- /dev/null +++ b/tests/unit/thinking-binding-controls-beta-forward.test.ts @@ -0,0 +1,69 @@ +/** + * Fable 5.1 sends `thinking.block_binding` + `thinking-binding-controls-2026-08-01` + * (and `thinking.display` + `thinking-display-updates-2026-08-18`) automatically. + * The gateway stripped both betas, so upstream 400'd with `Extra inputs are not + * permitted`. Both must be forwarded when the client negotiates them. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { ANTHROPIC_BETA_API_KEY, mergeClientAnthropicBeta, FORWARDABLE_CLIENT_BETAS } = + await import("../../open-sse/config/anthropicHeaders.ts"); + +const BINDING_CONTROLS = "thinking-binding-controls-2026-08-01"; +const DISPLAY_UPDATES = "thinking-display-updates-2026-08-18"; + +// ── allowlist membership ──────────────────────────────────────────────────── + +test("FORWARDABLE_CLIENT_BETAS must include thinking-binding-controls beta", () => { + assert.ok(FORWARDABLE_CLIENT_BETAS.includes(BINDING_CONTROLS)); +}); + +test("FORWARDABLE_CLIENT_BETAS must include thinking-display-updates beta", () => { + assert.ok(FORWARDABLE_CLIENT_BETAS.includes(DISPLAY_UPDATES)); +}); + +// ── client-negotiated beta forwarding ─────────────────────────────────────── + +test("mergeClientAnthropicBeta must forward client-negotiated thinking-binding-controls beta", () => { + const out = mergeClientAnthropicBeta( + ANTHROPIC_BETA_API_KEY, + `claude-code-20250219,${BINDING_CONTROLS}` + ); + const tokens = out.split(",").map((s) => s.trim()); + assert.ok(tokens.includes(BINDING_CONTROLS), `client beta dropped: ${out}`); +}); + +test("mergeClientAnthropicBeta must forward client-negotiated thinking-display-updates beta", () => { + const out = mergeClientAnthropicBeta( + ANTHROPIC_BETA_API_KEY, + `claude-code-20250219,${DISPLAY_UPDATES}` + ); + const tokens = out.split(",").map((s) => s.trim()); + assert.ok(tokens.includes(DISPLAY_UPDATES), `client beta dropped: ${out}`); +}); + +test("mergeClientAnthropicBeta forwards both betas together without duplication", () => { + const out = mergeClientAnthropicBeta( + `${ANTHROPIC_BETA_API_KEY},${BINDING_CONTROLS}`, + `claude-code-20250219,${BINDING_CONTROLS},${DISPLAY_UPDATES}` + ); + const tokens = out.split(",").map((s) => s.trim()); + assert.ok(tokens.includes(BINDING_CONTROLS), `binding-controls missing: ${out}`); + assert.ok(tokens.includes(DISPLAY_UPDATES), `display-updates missing: ${out}`); + assert.equal( + tokens.filter((t) => t.toLowerCase() === BINDING_CONTROLS).length, + 1, + `binding-controls duplicated: ${out}` + ); +}); + +// ── guard: allowlist still closed ─────────────────────────────────────────── + +test("mergeClientAnthropicBeta still ignores non-allowlisted client betas", () => { + const out = mergeClientAnthropicBeta( + ANTHROPIC_BETA_API_KEY, + "some-random-future-beta-2099-01-01" + ); + assert.ok(!out.includes("some-random-future-beta"), `unknown beta leaked: ${out}`); +});