fix(oauth): forward the caller's anthropic-beta flags and stop injecting descriptions into server-side tools - #775
Merged
Conversation
…ding them
The native OAuth path built its outbound header as
`'anthropic-beta': REQUIRED_BETAS.join(',')`, which overwrites whatever the
client sent. REQUIRED_BETAS is a hand-maintained snapshot of a genuine Claude
Code request ("TO UPDATE: inspect a genuine Claude Code CLI request"), so it
goes stale every time the client ships a new beta-gated feature, and every
caller flag beyond the snapshot was silently dropped.
Anthropic then rejects the tool that the dropped flag gates, e.g. a client
sending `advisor-tool-2026-03-01` + the `advisor_20260301` server tool gets:
400 tools.N: Input tag 'advisor_20260301' found using 'type' does not match
any of the expected tags: 'bash_20250124', ...
The header was already captured (`routes/inference/messages.ts` ->
`unifiedRequest.anthropicBeta`) and already forwarded on the standard
`messages` path (`services/providers/provider-request-headers.ts`); it was
simply never threaded into the OAuth path.
Add `mergeBetas()`, which unions REQUIRED_BETAS with the caller's header
(REQUIRED first, caller-only flags appended, trimmed and deduped), and thread a
`callerBetas` option through prepareAnthropicOAuthRequest ->
prepareOAuthNativeRequest -> prepareNativeOAuthDispatch, supplied by
request-payload-builder as `request.anthropicBeta`.
Merging rather than replacing preserves the masking-critical flags the caller
never sends (notably `oauth-2025-04-20`) while letting newer client betas
through without waiting for the constant to be refreshed.
`stripDescriptionsAndInjectSyntheticTools()` blanks client-authored tool
descriptions so no caller fingerprint reaches Anthropic. It did that with an
unconditional `{ ...t, description: note ?? '' }` over every entry in `tools[]`
— including server-side tools (`bash_20250124`, `web_search_*`,
`advisor_20260301`, ...), which are identified by a `type` other than "custom".
Those tools have closed schemas, so adding a key Anthropic doesn't expect makes
it reject the whole request:
400 tools.N.advisor_20260301.description: Extra inputs are not permitted
They also carry no client-authored description to strip, so there was nothing
to gain. Skip any tool whose `type` is present and not "custom", returning it
untouched — the same rule `applyClaudeOAuthTransform` already applies ("Skip
built-in tools (they have a `type` field)"). Custom and type-less tools are
still blanked, so the fingerprint-stripping purpose is unchanged.
Owner
|
Merged — thank you, @kaspesi! Excellent write-up: the two bugs were clearly diagnosed as sequential, the reproduction with the exact 400 errors made review straightforward, and the fixes are minimal and consistent with existing patterns ( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two bugs on the native Anthropic OAuth path that, together, make every request from a current Claude Code client fail with a
400from Anthropic. They're sequential — fixing the first one just surfaces the second.anthropic-betaheader is overwritten byREQUIRED_BETASinstead of merged with it, so any beta flag newer than that hand-maintained constant is silently dropped.stripDescriptionsAndInjectSyntheticTools()injects adescriptionkey into server-side tools, whose schemas are closed, so Anthropic rejects the request.Bug 1 — the caller's
anthropic-betaflags are discardedpackages/backend/src/services/oauth/oauth-native-request.tsbuilt its outbound headers with:That replaces the caller's
anthropic-betarather than merging with it.REQUIRED_BETAS(transformers/oauth/masking/cc-constants.ts) is a hand-maintained snapshot of a genuine Claude Code request — its own comment says "TO UPDATE: inspect a genuine Claude Code CLI request" — so it necessarily goes stale whenever the client ships a new beta-gated feature.Concretely: a current Claude Code client (v2.1.220) sends nine flags;
REQUIRED_BETAScovers five of them. One of the dropped ones isadvisor-tool-2026-03-01, which is what makes theadvisor_20260301server tool legal. Because the gateway stripped the flag but forwarded the tool, Anthropic rejected every such request:Notably the header is already handled correctly everywhere else —
routes/inference/messages.ts:74captures it intounifiedRequest.anthropicBeta, andservices/providers/provider-request-headers.ts:70forwards it on the standardmessagespath. It was simply never threaded into the OAuth path.Fix: a
mergeBetas()helper that unionsREQUIRED_BETASwith the caller's header (REQUIRED first, caller-only flags appended, trimmed, deduped), plus acallerBetasoption threaded throughprepareAnthropicOAuthRequest→prepareOAuthNativeRequest→prepareNativeOAuthDispatch, supplied byservices/dispatch/request-payload-builder.tsasrequest.anthropicBeta.Merging rather than replacing is deliberate: it preserves masking-critical flags the caller never sends (notably
oauth-2025-04-20) while letting newer client betas through without waiting for the constant to be refreshed.Bug 2 —
descriptioninjected into server-side toolspackages/backend/src/transformers/oauth/masking/cc-tools.ts, instripDescriptionsAndInjectSyntheticTools():This ran over every entry in
tools[], including server-side tools (bash_20250124,web_search_*,advisor_20260301, …). Those have closed schemas, so the extra key is rejected outright:They also carry no client-authored description to strip, so there was never anything to gain by touching them.
applyClaudeOAuthTransformintransformers/oauth/oauth-claude.tsalready skips them for exactly this reason ("Skip built-in tools (they have atypefield)").Fix: skip tools whose
typeis present and!== 'custom', returning them untouched. Custom and type-less tools are still blanked, so the fingerprint-stripping purpose of the function is unchanged.Reproduction
With a provider on the native Anthropic OAuth path, send a request carrying a beta flag that isn't in
REQUIRED_BETASplus the tool it gates:Before:
400 ... Input tag 'advisor_20260301' ... does not match any of the expected tags.After fixing only Bug 1:
400 tools.0.advisor_20260301.description: Extra inputs are not permitted.After both:
200.Testing
bunx --bun vitest run --config vitest.config.tsfrompackages/backend).bun run typecheckclean across all workspaces;bunx biome format/bunx biome lint .clean.packages/backend/src/services/oauth/__tests__/oauth-native-request-betas.test.ts(3 tests) — caller flags appear alongsideREQUIRED_BETAS, no duplicates or stray whitespace, and the header is byte-identical to today's behaviour when the caller sends nothing. 2 of 3 fail without the Bug 1 fix.packages/backend/src/transformers/oauth/masking/__tests__/cc-tools-server-tools.test.ts(3 tests) — server tools pass through with nodescriptionkey added, custom/type-less tools are still blanked. 2 of 3 fail without the Bug 2 fix.200, and requests that don't use beta-gated server tools are unaffected.No schema or migration changes.