feat(agent): support Claude Code OAuth through Anthropic-compatible proxies - #21069
feat(agent): support Claude Code OAuth through Anthropic-compatible proxies#21069russellbrenner wants to merge 2 commits into
Conversation
…roxies
Lets Hermes preserve the Claude Code OAuth fingerprint (Authorization:
Bearer + claude-cli user-agent + OAuth betas + x-app: cli) when sending
through any non-anthropic.com URL. Required for proxying CC OAuth tokens
via LiteLLM, OpenRouter Anthropic-passthrough, or any transparent
Anthropic-compatible router that forwards OAuth headers upstream.
Default behaviour is unchanged. Opt in per-model or per-provider:
model:
passthrough_llm_headers: true
base_url: https://litellm.example.com/v1/anthropic
providers:
litellm-claude:
passthrough_llm_headers: true # narrowest scope wins
Provider-entry overrides model-section (resolution in
``resolve_passthrough_llm_headers``). Azure / Bedrock are auto-excluded
regardless of the flag (see ``_forces_x_api_key_auth``) since they
genuinely cannot accept Anthropic OAuth Bearer tokens.
Fixes 401/429/400 cascade observed when proxying Claude Code OAuth
through LiteLLM: previously ``_is_third_party_anthropic_endpoint``
short-circuited the OAuth client-construction branch on any non-
anthropic.com URL, sending the OAuth token as ``x-api-key`` without the
required identity headers — LiteLLM then forwarded the malformed
request upstream and Anthropic rejected (or rate-limited as anonymous).
Also adds ``anthropic.protocol.*`` config block so users can override
the default beta-header lists, Claude Code version, and user-agent
template without editing source. Useful when Anthropic rotates beta
names or a proxy requires extra betas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AI-Generated: true
Adds 27 unit tests exercising: - 8-cell auth matrix: (anthropic.com vs LiteLLM vs Azure vs Bedrock) x (API key vs OAuth) x (passthrough flag off vs on). Verifies the pre-existing safety gate is preserved by default and that the OAuth branch fires correctly for proxies opted into passthrough. - ``_forces_x_api_key_auth`` carve-out (Azure / Bedrock). - ``anthropic.protocol.*`` config overrides: common_betas replacement, oauth_only_betas replacement, extend_betas additive merge, claude_code_version override, user_agent template (with bad-template fallback). - ``resolve_passthrough_llm_headers``: per-provider vs model-section precedence (narrowest wins), defensive fallback to False on missing / malformed config or load_config() raising. All tests use unittest.mock; no network calls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> AI-Generated: true
There was a problem hiding this comment.
Pull request overview
Adds an opt-in mechanism to preserve Claude Code OAuth “fingerprint” headers when routing Anthropic Messages traffic through Anthropic-compatible proxy URLs, while keeping the existing safety default (don’t leak OAuth identity headers to arbitrary third-party endpoints). Also makes several Anthropic protocol details configurable via anthropic.protocol.*.
Changes:
- Add
passthrough_llm_headersconfig resolution (per-provider override over model-level default) and thread it through Anthropic client construction + runtime snapshot/restore paths. - Introduce
anthropic.protocol.*overrides for beta header sets and Claude Code user-agent/version composition. - Add a new focused test suite covering the auth matrix, carve-outs (Azure/Bedrock), and config override behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
run_agent.py |
Propagates the new passthrough flag through init/switch/fallback/restore/credential-refresh Anthropic client rebuild paths. |
agent/anthropic_adapter.py |
Adds passthrough_oauth support, Azure/Bedrock carve-out helper, and config-driven protocol override resolvers. |
agent/auxiliary_client.py |
Passes passthrough flag into Anthropic wrapping/build paths for auxiliary clients. |
cli-config.yaml.example |
Documents new config knobs for OAuth passthrough and anthropic.protocol.* overrides. |
tests/agent/test_anthropic_passthrough.py |
New tests for the passthrough “escape hatch”, carve-outs, and protocol/config override behavior. |
tests/run_agent/test_run_agent.py |
Updates an assertion to include the new passthrough_oauth kwarg in rebuild calls. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| """ | ||
| try: | ||
| from hermes_cli.config import load_config | ||
| cfg = load_config().get("anthropic") or {} |
| def _resolve_common_betas() -> list[str]: | ||
| """Return effective common-betas list (config override + default + extend).""" | ||
| cfg = _load_anthropic_protocol_config() | ||
| base = cfg.get("common_betas") | ||
| base = list(base) if isinstance(base, list) else list(_COMMON_BETAS_DEFAULT) | ||
| extend = cfg.get("extend_betas") | ||
| if isinstance(extend, list): | ||
| base = base + list(extend) | ||
| return base | ||
|
|
||
|
|
||
| def _resolve_oauth_only_betas() -> list[str]: | ||
| """Return effective OAuth-only-betas list (config override or default).""" | ||
| cfg = _load_anthropic_protocol_config() | ||
| override = cfg.get("oauth_only_betas") | ||
| return list(override) if isinstance(override, list) else list(_OAUTH_ONLY_BETAS_DEFAULT) |
| # Backward-compatibility aliases — older code reads these names directly. | ||
| # Tests, in particular, patch ``_COMMON_BETAS`` and ``_OAUTH_ONLY_BETAS``. | ||
| # Keep them in sync with the defaults; new call sites should prefer the | ||
| # resolver functions. | ||
| _COMMON_BETAS = _COMMON_BETAS_DEFAULT | ||
| _OAUTH_ONLY_BETAS = _OAUTH_ONLY_BETAS_DEFAULT | ||
|
|
| real_client = build_anthropic_client(api_key, base_url) | ||
| real_client = build_anthropic_client( | ||
| api_key, base_url, | ||
| passthrough_oauth=resolve_passthrough_llm_headers(), |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for addressing a real current-main gap: agent/anthropic_adapter.py:804-822 still routes third-party Anthropic endpoints to x-api-key before OAuth detection.
Problems
run_agent.py:1400-1403sets_is_anthropic_oauthwhenever passthrough is enabled, without the new_forces_x_api_key_auth(base_url)exclusion. The factory correctly keeps Azure/Bedrock on x-api-key, but this state still enables the OAuth request-body transforms (Claude Code identity andmcp_tool naming), contradicting the stated carve-out.tests/agent/test_anthropic_passthrough.py:111-127verifies only factory kwargs for Azure/Bedrock; it does not cover that lifecycle state.
Suggested changes
- Use one eligibility predicate for both client authentication and
_is_anthropic_oauth, including the Azure/Bedrock exclusion, across rebuild/fallback/restore paths. - Add AIAgent-level Azure and Bedrock tests with passthrough enabled that assert the OAuth state and Claude Code transforms remain disabled.
Automated hermes-sweeper review.
| self._passthrough_llm_headers = _passthrough | ||
| self._is_anthropic_oauth = ( | ||
| _is_oat(effective_key) | ||
| if (_is_native_anthropic or _passthrough) |
There was a problem hiding this comment.
_forces_x_api_key_auth(base_url) is not part of this predicate. With passthrough enabled for Azure or Bedrock, the factory uses x-api-key but _is_anthropic_oauth becomes true and enables Claude Code request-body transforms. Gate this state with the same Azure/Bedrock exclusion as client construction.
| assert headers["user-agent"].startswith("claude-cli/") | ||
| assert headers["x-app"] == "cli" | ||
|
|
||
| def test_azure_oauth_passthrough_blocked(self): |
There was a problem hiding this comment.
These cases validate only build_anthropic_client. Add an AIAgent-level regression test that Azure/Bedrock plus passthrough keeps _is_anthropic_oauth false and does not apply Claude Code system or mcp_ tool transforms.
GottZ
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Summary
Two PRs address Anthropic OAuth proxying, but on different request paths: #21069 adds opt-in OAuth-header passthrough for Hermes agent traffic sent through Anthropic-compatible routers, while #58647 adds an Anthropic OAuth upstream to the local subscription proxy. Neither diff fully resolves its intended path without the contributor-identified blockers.
Related pull requests
- #21069
related— (+603/-50) — keep open, changes required: The diff correctly adds opt-in Bearer and Claude Code identity-header forwarding while preserving the default third-party safety gate, but its runtime OAuth-state calculation does not apply the Azure/Bedrock exclusion used by the client factory, so OAuth body transforms can still activate on carved-out endpoints. This matches the contributor keep_open review on #21069 and requires a shared eligibility predicate plus lifecycle-level Azure/Bedrock tests. - #58647
related— (+234/-0) — keep open, changes required: The diff implements a distinct local-proxy adapter with OAuth headers and Claude Code system-prompt transformation, but it can select API-key pool entries and send them as Bearer, omits the native OAuth x-app header, and leaves an existing adapter-registry test and user-facing documentation unchanged. This matches the contributor keep_open review on #58647; OAuth-only credential selection, hermetic proxy tests, header parity, and documentation are required before merge.
Suggested consolidation
Do not consolidate or merge either PR yet: keep #21069 as the agent-to-compatible-proxy implementation and #58647 as the separate local subscription-proxy implementation. Address each contributor keep_open review independently; no PR can be closed as a duplicate because the diffs modify different proxy layers and serve complementary request directions.
Cross-PR triage: Reviewed 2 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 57 kB of PR diffs, 9 kB of issue/PR text, 5 kB of discussion (3 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.
What
Adds an opt-in
passthrough_llm_headersconfig flag so Hermes can preserve the Claude Code OAuth fingerprint (Authorization: Bearer+claude-cliuser-agent + OAuth betas +x-app: cli) when sending through any non-anthropic.comURL.Also makes the Anthropic protocol details (beta-header lists, Claude Code version, user-agent template) overridable via a new
anthropic.protocol.*config block. No behaviour change on default config.Why
Today,
_is_third_party_anthropic_endpointshort-circuits the OAuth client-construction branch for any non-anthropic.comURL. This is the right default — it stops Hermes leaking Anthropic OAuth tokens to MiniMax / Alibaba / other Anthropic-compatible providers that don't honour them.But it breaks the legitimate case of proxying Claude Code OAuth subscriptions through a transparent Anthropic-compatible router (LiteLLM in claude-code passthrough mode, OpenRouter Anthropic-passthrough, etc.). The OAuth token gets sent as
x-api-keywithout the required identity headers, the proxy can't classify it as a CC OAuth request, and the upstream call fails with 401, 429, or 400.This PR adds an opt-in escape hatch that is:
providers.<name>.passthrough_llm_headersoverridesmodel.passthrough_llm_headers._forces_x_api_key_auth).build_anthropic_client(auth + identity headers),build_anthropic_messages_kwargs(system prompt prefix,mcp_*tool prefix viais_oauth=True), and the runtime snapshot used by credential rotation / fallback / restore paths.Design
Three connected pieces:
build_anthropic_client(..., passthrough_oauth=False)— new kwarg. When True and the API key is OAuth-shaped (sk-ant-oat*,cc-*, JWT) and the URL is not Azure / Bedrock, the OAuth branch fires for non-anthropic.comURLs. Default False preserves current behaviour.resolve_passthrough_llm_headers(provider_name=None)— config reader. Resolution order:providers.<name>.passthrough_llm_headersthenmodel.passthrough_llm_headersthen False._forces_x_api_key_auth(base_url)— narrow carve-out for endpoints that genuinely cannot accept OAuth (Azure, Bedrock).Refactor portion (separate concern, motivated the patch): the pre-existing
_COMMON_BETAS,_OAUTH_ONLY_BETAS,_CLAUDE_CODE_VERSION_FALLBACK, and the inlineclaude-cli/{version} (external, cli)user-agent template are now overridable viaanthropic.protocol.*config without editing source. Constants kept as back-compat aliases (_COMMON_BETAS = _COMMON_BETAS_DEFAULT) so any external code importing them keeps working.Alternatives considered
litellm) — separate plugin work, doesn't address the underlying gate.Breaking changes
None. Default behaviour is preserved bit-for-bit. All existing call sites of
build_anthropic_clientcontinue to pass it withoutpassthrough_oauth=and get the same result they did before.How to test
Test suite:
Platforms tested
🤖 Generated with Claude Code (Opus 4.7)