Skip to content

feat(agent): support Claude Code OAuth through Anthropic-compatible proxies - #21069

Open
russellbrenner wants to merge 2 commits into
NousResearch:mainfrom
russellbrenner:feat/anthropic-oauth-passthrough
Open

feat(agent): support Claude Code OAuth through Anthropic-compatible proxies#21069
russellbrenner wants to merge 2 commits into
NousResearch:mainfrom
russellbrenner:feat/anthropic-oauth-passthrough

Conversation

@russellbrenner

@russellbrenner russellbrenner commented May 7, 2026

Copy link
Copy Markdown

What

Adds an opt-in passthrough_llm_headers config flag so Hermes can 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.

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_endpoint short-circuits the OAuth client-construction branch for any non-anthropic.com URL. 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-key without 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:

  • Off by default — pre-existing safety gate preserved.
  • Per-provider configurableproviders.<name>.passthrough_llm_headers overrides model.passthrough_llm_headers.
  • Carved out for Azure / Bedrock regardless of the flag (they technically cannot accept OAuth Bearer tokens — see _forces_x_api_key_auth).
  • Symmetric across the request lifecycle — flows through build_anthropic_client (auth + identity headers), build_anthropic_messages_kwargs (system prompt prefix, mcp_* tool prefix via is_oauth=True), and the runtime snapshot used by credential rotation / fallback / restore paths.

Design

Three connected pieces:

  1. 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.com URLs. Default False preserves current behaviour.
  2. resolve_passthrough_llm_headers(provider_name=None) — config reader. Resolution order: providers.<name>.passthrough_llm_headers then model.passthrough_llm_headers then False.
  3. _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 inline claude-cli/{version} (external, cli) user-agent template are now overridable via anthropic.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

  • Always trust OAuth-shape when seen — rejected. Would silently leak tokens to MiniMax / Alibaba on misconfiguration. Opt-in is correct.
  • Single global flag — rejected. Users running native Anthropic + a LiteLLM proxy in the same install need per-provider granularity.
  • New provider profile (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_client continue to pass it without passthrough_oauth= and get the same result they did before.

How to test

# ~/.hermes/config.yaml
model:
  default: claude-opus-4-7
  provider: anthropic
  base_url: https://litellm.example.com/v1/anthropic
  passthrough_llm_headers: true
hermes chat -q "ping" --model claude-opus-4-7
# Outgoing request should carry:
#   Authorization: Bearer <token>
#   anthropic-beta: ...,claude-code-20250219,oauth-2025-04-20
#   user-agent: claude-cli/<version> (external, cli)
#   x-app: cli

Test suite:

pytest tests/agent/test_anthropic_passthrough.py -v   # 27 new tests
pytest tests/agent tests/run_agent                    # no regressions

Platforms tested

  • macOS 14 (darwin)

🤖 Generated with Claude Code (Opus 4.7)


Open with GitKraken

russellbrenner and others added 2 commits May 7, 2026 16:50
…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
Copilot AI review requested due to automatic review settings May 7, 2026 06:52

Copilot AI 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.

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_headers config 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.

Comment on lines +304 to +307
"""
try:
from hermes_cli.config import load_config
cfg = load_config().get("anthropic") or {}
Comment on lines +314 to +329
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)
Comment on lines +280 to +286
# 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

Comment thread agent/auxiliary_client.py
real_client = build_anthropic_client(api_key, base_url)
real_client = build_anthropic_client(
api_key, base_url,
passthrough_oauth=resolve_passthrough_llm_headers(),
@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists labels May 7, 2026

@teknium1 teknium1 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.

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-1403 sets _is_anthropic_oauth whenever 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 and mcp_ tool naming), contradicting the stated carve-out.
  • tests/agent/test_anthropic_passthrough.py:111-127 verifies 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.

Comment thread run_agent.py
self._passthrough_llm_headers = _passthrough
self._is_anthropic_oauth = (
_is_oat(effective_key)
if (_is_native_anthropic or _passthrough)

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.

_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):

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.

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.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 13, 2026

@GottZ GottZ 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.

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.

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

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/anthropic Anthropic native Messages API sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants