Skip to content

feat(provider): register claude-code as first-class subprocess provider (Claude Max OAuth via local CLI) #33462

Description

@zkhalpey

Summary

claude-code is currently registered as an alias of anthropic (hermes_cli/providers.py:282, plugins/model-providers/anthropic/__init__.py:44), so provider: claude-code in config.yaml routes to https://api.anthropic.com with API-key/OAuth-token auth. There is no path for the local claude --print subprocess transport that the official Claude Code CLI uses to authenticate against a flat-fee Claude Max plan (Keychain OAuth, no metered key, no API quota wall).

This forces users on Claude Max to either:

  1. Set provider: claude-code and silently hit api.anthropic.com with whatever ANTHROPIC_API_KEY/ANTHROPIC_TOKEN/CLAUDE_CODE_OAUTH_TOKEN is in env (metered billing, no Max benefit), or
  2. Stand up a local OpenAI-compatible reverse-proxy that shells out to claude --print per request, and point config.yaml at it via a custom claude-code-proxy provider block. This is the workaround most Max users converge on.

Every conversation turn currently logs:

agent.conversation_loop: conversation turn: model=claude-opus-4-7 provider=anthropic
agent.chat_completion_helpers: Streaming failed before delivery: Error code: 400 - "You're out of extra usage..."
agent.conversation_loop: API call failed provider=anthropic base_url=https://api.anthropic.com
agent.chat_completion_helpers: Fallback activated: claude-opus-4-7 → claude-opus-4-7 (claude-code-proxy)

i.e. the primary attempt is a useless 400 round-trip to api.anthropic.com, every turn, on every Max user's gateway.

Proposed change

Register claude-code as a first-class provider with its own profile (not an alias), backed by a subprocess driver around claude --print --output-format stream-json. Sketch:

claude_code = SubprocessProfile(
    name="claude-code",
    api_mode="claude_cli_stream_json",
    binary="claude",
    args=["--print", "--output-format", "stream-json", "--input-format", "stream-json"],
    auth_type="none",  # OAuth is handled by ~/.claude/.credentials.json
    auth_probe=lambda: shutil.which("claude") is not None,
    default_aux_model="claude-haiku-4-5",
)
register_provider(claude_code)

…plus a translator from Claude CLI stream-json → OpenAI SSE chunks with tool_calls deltas so the existing agent loop doesn't need changes.

Why this matters

  • Cost: Max users currently waste one failed api.anthropic.com request per turn; some never notice and end up on metered billing for the primary lane.
  • Auth hygiene: Removes the need to keep CLAUDE_CODE_OAUTH_TOKEN in env (which currently shadows Keychain OAuth and silently switches to metered).
  • UX: Today Max users get cold UX (chat-completions translation only). Native tool_use blocks and 1M-token context would Just Work if Hermes drove the CLI subprocess directly.
  • Fallback chain: Allows claude-code (Max OAuth, $0) → anthropic (metered API, capped) as a clean primary/fallback pair instead of users running a private fork or a reverse proxy.

Acceptance criteria

  • provider: claude-code in config.yaml resolves to a distinct provider (not aliased to anthropic).
  • Requests use the local claude CLI subprocess; api.anthropic.com is not contacted unless provider: anthropic is explicitly set.
  • Streaming, tool-use, and 1M context survive the translator.
  • hermes doctor reports claude-code as a separate provider with its own auth probe.
  • Anthropic plugin keeps claude / claude-oauth aliases (HTTP path), but claude-code is removed from that alias tuple.

Workaround until then

Add a custom proxy provider that shells out to claude --print:

providers:
  claude-code-proxy:
    api_mode: chat_completions
    base_url: http://127.0.0.1:18796/v1
    api_key: dummy-key
model:
  provider: claude-code-proxy
  default: claude-opus-4-7

Happy to upstream the subprocess provider + stream-json→SSE translator as a PR if there's interest.


Hermes version: v0.14.0 (2026.5.16)
Filed by: @zkhalpey

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low — cosmetic, nice to havecomp/agentCore agent runtime: loop, agent_init, prompt builder, context-compression, responses endpointprovider/anthropicAnthropic native Messages APIsweeper:not-plannedSweeper: closed per standing maintainer policy (design direction)type/featureNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions