Skip to content

Relay anthropic-beta tokens on the native Anthropic Messages routes - #1069

Open
PierreLeGuen wants to merge 1 commit into
mainfrom
fix/anthropic-beta-passthrough
Open

PierreLeGuen wants to merge 1 commit into
mainfrom
fix/anthropic-beta-passthrough

Conversation

@PierreLeGuen

Copy link
Copy Markdown
Contributor

Summary

  • stop validating anthropic-beta tokens against a static allowlist on /v1/messages and /v1/messages/count_tokens; syntax-check them and relay them to Anthropic verbatim
  • replace ANTHROPIC_ALLOWED_BETAS with ANTHROPIC_DENIED_BETAS, an operator kill-switch that refuses a named token without a release
  • cover the current 13-token client header, verbatim relay of an unknown token, denial, and malformed input in unit tests

Fixes #988. Fixes #1068. Same class as #970.

Why

Each client release adds dated beta tokens. The allowlist rejects the first unknown one, which fails the whole request, so the native lane stops serving current clients until someone edits an env var. That has now happened three times in three weeks (#970 on 08-23, #988 on 08-28, #1068 today), and the reporter's production cutover is blocked on the class rather than on any one token.

The allowlist was never the protection it looked like:

  • Anthropic is the authority on token validity. Its beta headers documentation specifies a 400 invalid_request_error — "Unexpected value(s) for the anthropic-beta header" — for an invalid name or one the organization cannot access. Relaying an unknown token produces that same 400, from the party that actually knows, and a non-2xx upstream response relays without billing.
  • Billing and product policy are enforced on the body. reject_unsupported_features already refuses speed: "fast", service_tier other than standard_only, inference_geo, mcp_servers, container, server-side fallbacks, every typed Anthropic tool and one-hour cache TTL. Every premium beta is inert without one of those fields, so the header alone cannot unlock one. A test now pins that: fast-mode-2026-02-01 relays, while speed: "fast" in the body is still rejected.
  • The header cannot change upstream auth or routing. The transport builds upstream headers from scratch with our own x-api-key and never forwards Authorization, so an oauth-* token is a no-op; endpoint-scoped betas name paths this router does not serve.

What relaying admits

Tokens are still bounded: ASCII letters, digits, -, _ and ., at most 128 bytes each and 64 distinct tokens per request, deduplicated in order. The bounds are roughly triple and five times the largest values Anthropic has published, so this endpoint is never the reason a plausible future token fails. Rejections carry their own wording rather than the old "not supported by the native Anthropic endpoint yet", so an operator denial or malformed input is distinguishable from the retired gate in triage.

Residual risk

A future header-only premium beta — one that raises Anthropic's price without a body field — would be relayed and billed upstream at premium while we meter at catalog rates. ANTHROPIC_DENIED_BETAS covers that in an env change plus a rolling update, no release. The only such toggle to date, context-1m-2025-08-07, is a no-op on current models (#979), and the provider's recent premium features are all body-gated and already refused.

Separately and unchanged by this PR: anthropic/claude-sonnet-4-5 is in the catalog at 200K, and on that model the long-context beta is premium-priced above 200K input while we bill flat. That token has been admitted since the #988 mitigation and current clients send it for 1M-suffixed models, so it cannot go in a global denylist; I will file the per-model follow-up separately.

Rollout

ANTHROPIC_ALLOWED_BETAS stays set in staging and production until this image is live; the new image ignores it and logs a one-line startup warning, and the old image ignores ANTHROPIC_DENIED_BETAS, so neither deploy order breaks. nearai/cvm-ansible-playbooks#683 admits today's two tokens as the runtime mitigation in the meantime; the variable is removed from the playbooks one promote cycle after this reaches production, so a rollback target is also a relaying image.

Validation

  • cargo fmt --all -- --check
  • cargo clippy -p api -p config --all-targets -- -D warnings
  • cargo test -p api --lib routes::anthropic (23 passed)
  • cargo test -p config --lib (61 passed)
  • git diff --check
  • staging and production probes with the full client header, an unknown token, a malformed token and the speed: "fast" body gate to follow on the deployed image

The native Messages routes validated every anthropic-beta token against a
static allowlist. Each client release adds dated tokens, so the first
unknown one 400s the whole request and the native lane stops serving
current clients. This is the third recurrence in three weeks (#970, #988,
#1068) and the allowlist buys nothing: Anthropic already answers a name it
does not recognise with its own 400, and that response relays unbilled.

Treat the header as transport instead. Tokens are syntax-checked (ASCII
letters, digits, '-', '_', '.'; 128 bytes per token, 64 distinct tokens),
deduplicated in order, and forwarded verbatim. Billing and product policy
stay where they already live — the body gates in reject_unsupported_features
reject speed=fast, non-standard service tiers, inference_geo, mcp_servers,
container, server-side fallbacks, typed Anthropic tools and one-hour cache
TTL — so a beta token alone cannot unlock a premium or server-side feature.

ANTHROPIC_ALLOWED_BETAS becomes ANTHROPIC_DENIED_BETAS: an operator
kill-switch for a future header-only premium beta, settable without a
release. The retired variable is ignored with a startup warning so the
deploy window where it is still set explains itself.

Fixes #988
Fixes #1068
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T14:18:28.117389Z 38cfd0c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Sep 16, 2026

Copy link
Copy Markdown

Review: relay anthropic-beta tokens

No prior human review threads on this PR (the Codex review was still running at the time of this pass), so nothing to build on — this is a fresh read of the diff.

No critical issues found. I verified the three load-bearing claims in the description against the code rather than taking them on trust:

  • Non-2xx relays without billing — confirmed. build_upstream_response gates every usage path on status.is_success() (crates/api/src/routes/anthropic.rs:983, :1022); a 400 from Anthropic on an unknown token falls through to Body::from_stream(upstream.body) with billing dropped unrecorded, and concurrent_slot released on return.
  • The header cannot change upstream auth or routing — confirmed. crates/inference_providers/src/non_attested/external/anthropic/mod.rs:98-127 builds the upstream HeaderMap from scratch with only content-type, our x-api-key, anthropic-version and anthropic-beta. Authorization is never forwarded, so an oauth-* token really is inert.
  • Body gates are the real enforcement — confirmed at crates/api/src/routes/anthropic.rs:673-710 plus the typed-tool loop; beta_header_alone_never_unlocks_a_body_gated_feature pins it. The transport's HeaderValue::from_str on the joined value (mod.rs:121) returns InvalidRequest rather than panicking, and the new charset bound makes that unreachable anyway.

Removal is clean: no stale ANTHROPIC_ALLOWED_BETAS / anthropic_allowed_betas references remain outside the deliberate startup warning and its test, no test asserts the retired "is not supported" wording for beta tokens, and eprintln! (not tracing) matches the convention already documented in crates/config/src/types.rs for pre-logging-init warnings. from_env is called once at startup (types.rs:79), so the warning does not repeat. The rolling-update story checks out in both directions.

Three non-blocking notes:

  • Response-shape residual risk is not discussed alongside the pricing one. A beta that Anthropic accepts but that relocates or renames the usage object turns the non-streaming path into a hard 502 (parse_non_stream_usage:997-1004), after upstream has already done and billed the work. The allowlist used to refuse that locally; ANTHROPIC_DENIED_BETAS is now the only mitigation. Worth a sentence in the module doc so the next responder reaches for the kill-switch instead of debugging a 502.
  • The kill-switch is global, not per-model, which is exactly why (as you note) context-1m-2025-08-07 cannot go in it — clients need it on 1M-suffixed models while it is premium-priced above 200K on claude-sonnet-4-5. Acknowledging that the documented mitigation does not cover the one real header-only premium beta; the per-model follow-up you mention is the right shape.
  • Boundary test nit. The comment says "One below each bound still passes," but the case uses exactly MAX_BETA_TOKEN_LEN (at the bound — correct for <=, just mislabelled), and 64 distinct tokens succeeding is untested while 65 failing is. Also cosmetic: denial compares with eq_ignore_ascii_case while dedup compares exactly, so foo,FOO consumes two of the 64 slots. Neither affects behaviour in practice.

Tests were not executed in this environment (no cargo access for this reviewer); the reported cargo test -p api --lib routes::anthropic / -p config --lib results are taken as stated.

@ironloopai

ironloopai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Review · Status

🟩 Completed

IronLoop completed the review and posted it to GitHub.

Result

Open submitted review →

Run details
  • Run: 694938d7-9110-4837-b249-32d5d3048cba
  • Base: main at 5c7cab7
  • Head: fix/anthropic-beta-passthrough at 38cfd0c
  • Created: 2026-09-16 14:21 UTC
  • Updated: 2026-09-16 14:24 UTC

Automatic trigger · attempt 1 of 3 · completed in 3m 33s

@github-actions

Copy link
Copy Markdown

OpenCodeReview: Review partially complete: 0 finding(s); 1 of 3 selected item(s) failed.

@lloydmak99 lloydmak99 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-contained change: the static anthropic-beta allowlist is replaced with syntax-validation + verbatim relay plus an operator denylist kill-switch, and ANTHROPIC_ALLOWED_BETASANTHROPIC_DENIED_BETAS is renamed cleanly. Core relay logic checks out; approving.

Non-blocking follow-up:

  • normalized_beta_header (crates/api/src/routes/anthropic.rs:855-903) relays any Anthropic-accepted beta that reshapes the usage object, which can 502 the non-streaming path (anthropic.rs:995-1004) after upstream has already billed the work. This is a documented tradeoff mitigated only by the ANTHROPIC_DENIED_BETAS kill-switch — fine to keep, worth watching.

Local checks: git diff --check passed; upstream header construction, non-2xx billing gate, body-gate enforcement, and config rename/parse verified by cross-reference. Focused cargo test (routes::anthropic, config) could not run locally (no cc linker), but CI lint/unit/integration/E2E jobs passed.

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review · Summary

🟢 No actionable findings

No actionable defects found in the pull request.

Validation
  • Change inspection — Reviewed the changed beta-token validation, denial configuration, upstream relay path, documentation, and cache-walk defaults.
  • Diff hygiene — The changed diff has no whitespace errors.
Review details
  • Run: 694938d7-9110-4837-b249-32d5d3048cba
  • Attempts: 1

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

Labels

None yet

Projects

None yet

2 participants