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
This commit is contained in:
Felipe Fidelix
2026-09-18 11:29:54 -03:00
committed by GitHub
parent a5d603ebc9
commit 1533f16ed8
3 changed files with 74 additions and 0 deletions

View File

@@ -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))

View File

@@ -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",
]);
/**

View File

@@ -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}`);
});