Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions ccproxy/plugins/claude_api/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ async def prepare_provider_request(

# Minimal beta tags required for OAuth-based Claude Code auth
filtered_headers["anthropic-version"] = "2023-06-01"
filtered_headers["anthropic-beta"] = self._merge_anthropic_beta(
headers.get("anthropic-beta") or headers.get("Anthropic-Beta")
)
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)
Comment on lines +91 to +93
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.

# Add CLI headers if available, but never allow overriding auth
cli_headers = self._collect_cli_headers()
Expand All @@ -106,6 +106,14 @@ async def prepare_provider_request(
)
continue
if lk == "anthropic-beta":
# Client is authoritative for its own beta features.
# Only fall back to CLI-detected betas when the client
# sent none — otherwise CLI defaults like
# context-1m-2025-08-07 leak into client requests that
# never asked for them and break model/account combos
# that don't support those betas.
if client_provided_beta:
continue
filtered_headers[lk] = self._merge_anthropic_beta(
value, base=filtered_headers.get("anthropic-beta")
)
Expand Down
62 changes: 56 additions & 6 deletions tests/plugins/claude_api/unit/test_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ async def test_prepare_provider_request_merges_client_beta(
assert "custom-tag" in beta_tags

@pytest.mark.asyncio
async def test_prepare_provider_request_merges_cli_detected_beta(
async def test_prepare_provider_request_cli_beta_used_when_client_omits_header(
self,
mock_detection_service: ClaudeAPIDetectionService,
mock_auth_manager: Mock,
mock_http_pool_manager: Mock,
) -> None:
"""CLI-detected beta tags from detection cache must flow through to upstream request."""
"""CLI-detected beta tags are used as a fallback when the client sends no anthropic-beta."""
mock_detection_service.get_detected_headers = Mock( # type: ignore[method-assign]
return_value=DetectedHeaders(
{
Expand All @@ -170,21 +170,71 @@ async def test_prepare_provider_request_merges_cli_detected_beta(
"max_tokens": 100,
}
).encode()
headers = {"content-type": "application/json"}

_, result_headers = await adapter.prepare_provider_request(
body, headers, "/v1/messages"
)

beta_tags = set(result_headers["anthropic-beta"].split(","))
assert "claude-code-20250219" in beta_tags
assert "oauth-2025-04-20" in beta_tags
assert "context-1m-2025-08-07" in beta_tags
assert "interleaved-thinking-2025-05-14" in beta_tags

@pytest.mark.asyncio
async def test_prepare_provider_request_cli_beta_skipped_when_client_sends_header(
self,
mock_detection_service: ClaudeAPIDetectionService,
mock_auth_manager: Mock,
mock_http_pool_manager: Mock,
) -> None:
"""When the client sends its own anthropic-beta the CLI-detected betas must not leak in.

Otherwise CLI defaults like ``context-1m-2025-08-07`` get injected into
every request and break model/account combos that don't support them
(e.g. haiku, or accounts without the long-context beta).
"""
mock_detection_service.get_detected_headers = Mock( # type: ignore[method-assign]
return_value=DetectedHeaders(
{
"anthropic-beta": "claude-code-20250219,context-1m-2025-08-07,advisor-tool-2026-03-01",
}
)
)
from ccproxy.plugins.claude_api.config import ClaudeAPISettings

adapter = ClaudeAPIAdapter(
detection_service=mock_detection_service,
config=ClaudeAPISettings(),
auth_manager=mock_auth_manager,
http_pool_manager=mock_http_pool_manager,
)

body = json.dumps(
{
"model": "claude-haiku-4-5-20251001",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 100,
}
).encode()
headers = {
"content-type": "application/json",
"anthropic-beta": "client-only-tag",
"anthropic-beta": "oauth-2025-04-20,prompt-caching-scope-2026-01-05",
}

_, result_headers = await adapter.prepare_provider_request(
body, headers, "/v1/messages"
)

beta_tags = set(result_headers["anthropic-beta"].split(","))
# Required OAuth tags + the client's own tags are present.
assert "claude-code-20250219" in beta_tags
assert "oauth-2025-04-20" in beta_tags
assert "context-1m-2025-08-07" in beta_tags
assert "interleaved-thinking-2025-05-14" in beta_tags
assert "client-only-tag" in beta_tags
assert "prompt-caching-scope-2026-01-05" in beta_tags
# CLI-detected betas the client did not request must NOT leak in.
assert "context-1m-2025-08-07" not in beta_tags
assert "advisor-tool-2026-03-01" not in beta_tags

def test_merge_anthropic_beta_helper(self) -> None:
"""_merge_anthropic_beta deduplicates and always includes required tags."""
Expand Down
Loading