Skip to content

fix(oauth): forward the caller's anthropic-beta flags and stop injecting descriptions into server-side tools - #775

Merged
mcowger merged 2 commits into
mcowger:mainfrom
kaspesi:fix/forward-caller-anthropic-beta
Aug 1, 2026
Merged

fix(oauth): forward the caller's anthropic-beta flags and stop injecting descriptions into server-side tools#775
mcowger merged 2 commits into
mcowger:mainfrom
kaspesi:fix/forward-caller-anthropic-beta

Conversation

@kaspesi

@kaspesi kaspesi commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Two bugs on the native Anthropic OAuth path that, together, make every request from a current Claude Code client fail with a 400 from Anthropic. They're sequential — fixing the first one just surfaces the second.

  1. The caller's anthropic-beta header is overwritten by REQUIRED_BETAS instead of merged with it, so any beta flag newer than that hand-maintained constant is silently dropped.
  2. stripDescriptionsAndInjectSyntheticTools() injects a description key into server-side tools, whose schemas are closed, so Anthropic rejects the request.

Bug 1 — the caller's anthropic-beta flags are discarded

packages/backend/src/services/oauth/oauth-native-request.ts built its outbound headers with:

'anthropic-beta': REQUIRED_BETAS.join(','),

That replaces the caller's anthropic-beta rather 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_BETAS covers five of them. One of the dropped ones is advisor-tool-2026-03-01, which is what makes the advisor_20260301 server tool legal. Because the gateway stripped the flag but forwarded the tool, Anthropic rejected every such request:

400 tools.N: Input tag 'advisor_20260301' found using 'type' does not match
    any of the expected tags: 'bash_20250124', ...

Notably the header is already handled correctly everywhere else — routes/inference/messages.ts:74 captures it into unifiedRequest.anthropicBeta, and services/providers/provider-request-headers.ts:70 forwards it on the standard messages path. It was simply never threaded into the OAuth path.

Fix: a mergeBetas() helper that unions REQUIRED_BETAS with the caller's header (REQUIRED first, caller-only flags appended, trimmed, deduped), plus a callerBetas option threaded through prepareAnthropicOAuthRequestprepareOAuthNativeRequestprepareNativeOAuthDispatch, supplied by services/dispatch/request-payload-builder.ts as request.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 — description injected into server-side tools

packages/backend/src/transformers/oauth/masking/cc-tools.ts, in stripDescriptionsAndInjectSyntheticTools():

return { ...t, description: note ?? '' };

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:

400 tools.N.advisor_20260301.description: Extra inputs are not permitted

They also carry no client-authored description to strip, so there was never anything to gain by touching them. applyClaudeOAuthTransform in transformers/oauth/oauth-claude.ts already skips them for exactly this reason ("Skip built-in tools (they have a type field)").

Fix: skip tools whose type is 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_BETAS plus the tool it gates:

curl -sS http://127.0.0.1:4000/v1/messages \
  -H 'x-api-key: <client-key>' \
  -H 'anthropic-version: 2023-06-01' \
  -H 'anthropic-beta: advisor-tool-2026-03-01,claude-code-20250219' \
  -H 'content-type: application/json' \
  -d '{
        "model": "<oauth-anthropic-alias>",
        "max_tokens": 64,
        "messages": [{"role": "user", "content": "hi"}],
        "tools": [{"type": "advisor_20260301", "name": "advisor", "model": "claude-sonnet-4-5"}]
      }'

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

  • Full backend suite green: 2656 tests / 238 files (bunx --bun vitest run --config vitest.config.ts from packages/backend).
  • bun run typecheck clean across all workspaces; bunx biome format / bunx biome lint . clean.
  • New unit tests, both verified non-vacuous by reverting the corresponding fix and confirming they fail:
    • packages/backend/src/services/oauth/__tests__/oauth-native-request-betas.test.ts (3 tests) — caller flags appear alongside REQUIRED_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 no description key added, custom/type-less tools are still blanked. 2 of 3 fail without the Bug 2 fix.
  • Also verified end-to-end against the real Anthropic API through a patched build: the previously-failing request returns 200, and requests that don't use beta-gated server tools are unaffected.

No schema or migration changes.

kaspesi added 2 commits July 31, 2026 18:14
…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.
@mcowger
mcowger merged commit c314528 into mcowger:main Aug 1, 2026
2 checks passed
@mcowger

mcowger commented Aug 1, 2026

Copy link
Copy Markdown
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 (mergeBetas preserving masking-critical flags, and the server-tool skip mirroring applyClaudeOAuthTransform). Especially appreciated the non-vacuous test verification. Great work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants