Skip to content

fix(claude_api): stop leaking CLI-detected anthropic-beta into client requests#60

Merged
CaddyGlow merged 1 commit intomainfrom
fix/claude-api-cli-beta-leak
Apr 13, 2026
Merged

fix(claude_api): stop leaking CLI-detected anthropic-beta into client requests#60
CaddyGlow merged 1 commit intomainfrom
fix/claude-api-cli-beta-leak

Conversation

@CaddyGlow
Copy link
Copy Markdown
Owner

Summary

When Claude Code CLI (or any external client) sends its own `anthropic-beta` header to the `/claude` endpoint, ccproxy was still merging in beta tags it had captured from the local `claude` CLI's own outbound requests during startup detection. Those CLI defaults — currently `context-1m-2025-08-07`, `advisor-tool-2026-03-01`, `effort-2025-11-24`, etc. — then ended up on every proxied request, including ones targeting models or accounts that don't support those betas.

Concretely, running:

```bash
ANTHROPIC_BASE_URL=http://127.0.0.1:8000/claude claude
```

…and triggering any small request against `claude-haiku-4-5-20251001` produced:

```json
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "The long context beta is not yet available for this subscription."
}
}
```

…because haiku does not support the 1M-context beta at all, even on accounts that do have long-context access for Sonnet. The client never asked for `context-1m-2025-08-07` — ccproxy injected it.

Root cause

`ccproxy/plugins/claude_api/adapter.py` (line ~91 onward) does the merge in two stages:

  1. First, merge required OAuth tags into the client's `anthropic-beta` — correct.
  2. Then, iterate CLI-detected headers from the detection cache and merge their `anthropic-beta` on top via `_merge_anthropic_beta(value, base=...)` — wrong when the client already provided its own beta.

PR #54/#56 ("merge client anthropic-beta tags with required OAuth tags") fixed step 1, but step 2 was a separate path that pre-dated it and kept smashing CLI-default betas into authoritative client requests. There was even a test asserting this broken behavior.

Fix

The client is authoritative for its own beta features. The CLI-detected `anthropic-beta` is now only used as a fallback when the client did not send one of its own. When the client provides any `anthropic-beta`, the outgoing request gets exactly:

```
client tags + claude-code-20250219 + oauth-2025-04-20
```

…and nothing else. Other CLI-detected headers (non-beta) still flow through unchanged.

Test plan

  • Replaced `test_prepare_provider_request_merges_cli_detected_beta` (which asserted the broken behavior) with two tests reflecting the new contract:
    • `..._cli_beta_used_when_client_omits_header` — fallback path: when the client sends no `anthropic-beta`, CLI-detected betas including `context-1m-2025-08-07` and `interleaved-thinking-2025-05-14` flow through.
    • `..._cli_beta_skipped_when_client_sends_header` — primary path: when the client sends its own `anthropic-beta`, CLI-only tags (`context-1m-2025-08-07`, `advisor-tool-2026-03-01`) are explicitly asserted not to leak in, while the client's tags + required OAuth tags do.
  • `uv run pytest tests/plugins/claude_api/unit/test_adapter.py -x -q` — 19 passed.
  • `uv run ruff check` / `uv run ruff format` / `uv run mypy ccproxy/plugins/claude_api/adapter.py` — clean.
  • `make pre-commit` (via the commit hook) — passed.

After this lands, `ANTHROPIC_BASE_URL=http://127.0.0.1:8000/claude claude` will stop hitting the long-context 400 on haiku requests, and accounts with mixed model access (Sonnet 1M + Haiku) will work correctly.

… requests

When an external client sends its own anthropic-beta header, the
adapter was still merging in beta tags captured from the local claude
CLI's outbound requests during detection. Those CLI defaults (e.g.
context-1m-2025-08-07, advisor-tool-2026-03-01) then ended up on every
proxied request — including ones targeting models or accounts that
do not support those betas, which Anthropic rejects with HTTP 400.

The client is authoritative for its own beta features. Now the
CLI-detected anthropic-beta is only used as a fallback when the
client did not send one of its own; otherwise we forward exactly
(client tags + the required OAuth tags) and nothing else.

Replaces the test that asserted the broken merge behavior with two
tests covering the fallback path and the new client-authoritative
path (verifying that CLI-only tags like context-1m-2025-08-07 do
not leak in).
Copilot AI review requested due to automatic review settings April 13, 2026 12:22
@CaddyGlow CaddyGlow merged commit f5c5f4e into main Apr 13, 2026
7 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes ClaudeAPIAdapter header handling so CLI-detected anthropic-beta tags captured during startup detection no longer leak into client-authoritative requests to the /claude endpoint, preventing invalid beta tags from being injected across unrelated models/accounts.

Changes:

  • Treat client-provided anthropic-beta as authoritative and skip merging CLI-detected beta tags when the client sends its own header.
  • Keep CLI-detected anthropic-beta as a fallback only when the client omits the header entirely.
  • Update unit tests to reflect the new “authoritative client / CLI fallback” contract.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
ccproxy/plugins/claude_api/adapter.py Adjusts anthropic-beta merge logic to prevent CLI-detected beta leakage when the client provides its own header.
tests/plugins/claude_api/unit/test_adapter.py Replaces the previous test asserting the broken behavior with two tests covering fallback vs. authoritative-client scenarios.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +91 to +93
client_beta = headers.get("anthropic-beta") or headers.get("Anthropic-Beta")
filtered_headers["anthropic-beta"] = self._merge_anthropic_beta(client_beta)
client_provided_beta = bool(client_beta)
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

client_provided_beta is computed via bool(client_beta), which treats an explicitly provided empty header value ("anthropic-beta": "") as “not provided”. In that case the code will fall back to CLI-detected betas and reintroduce the leakage this PR is trying to prevent. Consider tracking “client provided header” by key presence (case-insensitive) rather than value truthiness, so an empty value still suppresses CLI beta fallback.

Suggested change
client_beta = headers.get("anthropic-beta") or headers.get("Anthropic-Beta")
filtered_headers["anthropic-beta"] = self._merge_anthropic_beta(client_beta)
client_provided_beta = bool(client_beta)
client_beta_header_key = None
for key in headers:
if key.lower() == "anthropic-beta":
client_beta_header_key = key
if key == "anthropic-beta":
break
client_provided_beta = client_beta_header_key is not None
client_beta = (
headers[client_beta_header_key]
if client_beta_header_key is not None
else None
)
filtered_headers["anthropic-beta"] = self._merge_anthropic_beta(client_beta)

Copilot uses AI. Check for mistakes.
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