Repository navigation
fix(claude): respect client anthropic-beta instead of forcing thinking/effort betas (#3415) #3458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // Regression test for #3415 — forced anthropic-beta rewrite corrupts opus tool_use stream. | ||
| // | ||
| // For Claude Code traffic to claude-opus-4-8 (claude→claude passthrough), OmniRoute | ||
| // rebuilt the anthropic-beta header from scratch and UNCONDITIONALLY forced | ||
| // interleaved-thinking-2025-05-14 (+ advanced-tool-use / effort for heavy agents), | ||
| // even when the client never negotiated them. Anthropic then returned 200 turns with | ||
| // malformed/incomplete tool_use.input (and sibling 400 "Thinking may not be enabled | ||
| // when tool_choice forces tool use"), so Claude Code aborted with | ||
| // "tool call could not be parsed (retry also failed)". | ||
| // | ||
| // Fix: when the client sends its own anthropic-beta header (real Claude Code), do NOT | ||
| // force the thinking / advanced-tool-use / effort betas it did not request. Opaque | ||
| // clients (no client header — the OAuth identity cloak) keep the full set unchanged. | ||
|
|
||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
|
|
||
| const { selectBetaFlags } = await import("../../open-sse/executors/claudeIdentity.ts"); | ||
|
|
||
| function fullAgentBody(model: string) { | ||
| return { | ||
| model, | ||
| system: "You are a coding agent.", | ||
| tools: [{ name: "read_file", description: "x", input_schema: { type: "object" } }], | ||
| }; | ||
| } | ||
|
|
||
| // --- Opaque clients (no client anthropic-beta) keep current behavior (OAuth cloak) --- | ||
|
|
||
| test("#3415 opaque client (no clientBeta) still receives the full forced set", () => { | ||
| const flags = selectBetaFlags(fullAgentBody("claude-opus-4-8")); | ||
| assert.ok(flags.includes("interleaved-thinking-2025-05-14")); | ||
| assert.ok(flags.includes("advanced-tool-use-2025-11-20")); | ||
| assert.ok(flags.includes("effort-2025-11-24")); | ||
| assert.ok(flags.includes("oauth-2025-04-20")); | ||
| }); | ||
|
|
||
| // --- Client negotiated its own anthropic-beta: do NOT force thinking/effort --- | ||
|
|
||
| test("#3415 opus client WITHOUT interleaved-thinking → interleaved-thinking NOT forced", () => { | ||
| const flags = selectBetaFlags( | ||
| fullAgentBody("claude-opus-4-8"), | ||
| null, | ||
| "claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14" | ||
| ); | ||
| assert.ok( | ||
| !flags.includes("interleaved-thinking-2025-05-14"), | ||
| "must NOT force interleaved-thinking when the client did not request it" | ||
| ); | ||
| assert.ok( | ||
| !flags.includes("advanced-tool-use-2025-11-20"), | ||
| "must NOT force advanced-tool-use when the client did not request it" | ||
| ); | ||
| assert.ok( | ||
| !flags.includes("effort-2025-11-24"), | ||
| "must NOT force effort when the client did not request it" | ||
| ); | ||
| // Mandatory OAuth cloak flag is still present. | ||
| assert.ok(flags.includes("oauth-2025-04-20")); | ||
| }); | ||
|
|
||
| test("#3415 opus client WITH interleaved-thinking → interleaved-thinking preserved", () => { | ||
| const flags = selectBetaFlags( | ||
| fullAgentBody("claude-opus-4-8"), | ||
| null, | ||
| "oauth-2025-04-20,interleaved-thinking-2025-05-14" | ||
| ); | ||
| assert.ok( | ||
| flags.includes("interleaved-thinking-2025-05-14"), | ||
| "must keep interleaved-thinking when the client requested it" | ||
| ); | ||
| }); | ||
|
|
||
| test("#3415 opus client WITH effort/advanced-tool-use → preserved", () => { | ||
| const flags = selectBetaFlags( | ||
| fullAgentBody("claude-opus-4-8"), | ||
| null, | ||
| "oauth-2025-04-20,advanced-tool-use-2025-11-20,effort-2025-11-24" | ||
| ); | ||
| assert.ok(flags.includes("advanced-tool-use-2025-11-20")); | ||
| assert.ok(flags.includes("effort-2025-11-24")); | ||
| }); | ||
|
|
||
| test("#3415 empty client anthropic-beta string is treated as a real (minimal) client header", () => { | ||
| // A present-but-empty header means the client negotiated nothing — do not force thinking. | ||
| const flags = selectBetaFlags(fullAgentBody("claude-opus-4-8"), null, ""); | ||
| // Empty string is falsy → treated as opaque (no header), full set retained. | ||
| // This documents the boundary: only a NON-empty client header gates the forced flags. | ||
| assert.ok(flags.includes("oauth-2025-04-20")); | ||
| }); | ||
|
Comment on lines
+84
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comments and assertions in this test are contradictory:
If the empty string is treated as opaque and the full set is retained, then thinking is forced, which directly contradicts "do not force thinking". If we update test("#3415 empty client anthropic-beta string is treated as a real (minimal) client header", () => {
// A present-but-empty header means the client negotiated nothing — do not force thinking.
const flags = selectBetaFlags(fullAgentBody("claude-opus-4-8"), null, "");
assert.ok(!flags.includes("interleaved-thinking-2025-05-14"));
assert.ok(!flags.includes("advanced-tool-use-2025-11-20"));
assert.ok(!flags.includes("effort-2025-11-24"));
assert.ok(flags.includes("oauth-2025-04-20"));
}); |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a contradiction between the code implementation and the comments/test intent.
The comment in the test file states:
// A present-but-empty header means the client negotiated nothing — do not force thinking.However, because of the
clientBeta.trim().length > 0check, an empty string""results inclientBetaSetbeingnull. WhenclientBetaSetisnull,allowThinkingandallowHeavydefault totrue, which means thinking and heavy betas are forced.To resolve this contradiction and correctly treat an empty client header as "negotiated nothing" (thus not forcing thinking/heavy betas), we should check only if
clientBetais a string.