Skip to content

feat(providers): add Claude CLI subprocess transport - #6427

Closed
gwthm-in wants to merge 1 commit into
NousResearch:mainfrom
gwthm-in:feat/claude-cli-transport
Closed

feat(providers): add Claude CLI subprocess transport#6427
gwthm-in wants to merge 1 commit into
NousResearch:mainfrom
gwthm-in:feat/claude-cli-transport

Conversation

@gwthm-in

@gwthm-in gwthm-in commented Apr 9, 2026

Copy link
Copy Markdown

Summary

  • Adds a new claude_cli transport that routes inference through the locally installed claude CLI binary, letting users with a Claude Pro/Max subscription use Hermes without a separate API key
  • The CLI manages its own OAuth credentials — no ANTHROPIC_API_KEY needed
  • Works for all auxiliary tasks (context compression, title generation, etc.) and the main agent loop

Motivation

Users who have claude CLI installed and a Claude Pro/Max subscription have no way to use Hermes without also obtaining a separate Anthropic API key. The native anthropic provider calls api.anthropic.com directly and requires API credits. This transport bridges the gap by shelling out to the claude binary, which honours the subscription billing path — the same approach OpenClaw uses with --auth-choice claude-cli.

Changes

File Description
agent/claude_cli_adapter.py (new) ClaudeCliAdapter with a chat.completions-compatible interface. Tool calls via JSON-in-prompt encoding. Async shim via asyncio.to_thread.
hermes_cli/providers.py Register claude-cli in HERMES_OVERLAYS (transport="claude_cli", auth_type="external_process"), aliases, label, and TRANSPORT_TO_API_MODE entry.
hermes_cli/models.py Add claude-cli model list (opus-4-6, sonnet-4-6, haiku-4-5, etc.).
agent/auxiliary_client.py _try_claude_cli(), appended to _get_provider_chain() as last-resort fallback, resolve_provider_client() branch, _to_async_client() support.
run_agent.py Recognise claude_cli api_mode; initialise in __init__ and switch_model; dispatch in _interruptible_api_call and streaming path.

How tool calls work

The claude CLI's -p mode returns a final text response. To get structured tool-call decisions, tool definitions are injected into the system prompt as JSON and Claude is instructed to respond with:

{"tool_call": {"name": "tool_name", "arguments": {...}}}

The adapter parses this sentinel block and returns an OpenAI-compatible tool_calls array. This is reliable with Claude models but lacks native streaming tool events.

Usage

# Install the Claude CLI if not already present
npm install -g @anthropic-ai/claude-code

# Log in (one-time)
claude login

# Switch Hermes to use it
hermes model claude-cli
# or: hermes --provider claude-cli --model claude-sonnet-4-6 chat "Hello!"

Test plan

  • is_claude_cli_available() returns True when claude is on PATH
  • hermes model claude-cli lists available models without error
  • Single-turn text response works: hermes --provider claude-cli chat "2+2?"
  • Multi-turn context is preserved across turns
  • Tool call sentinel is correctly parsed (tool_calls populated in response)
  • Auto-detect chain falls through to claude-cli when no other provider is configured
  • hermes --provider claude-cli in async auxiliary context (context compression) works
  • Graceful error when claude CLI is not installed

🤖 Generated with Claude Code

Adds a new `claude_cli` transport that routes inference through the locally
installed `claude` CLI binary instead of calling api.anthropic.com directly.
This lets users with a Claude Pro/Max subscription use Hermes without a
separate API key — the CLI manages its own OAuth credentials.

Changes:
- `agent/claude_cli_adapter.py` (new): ClaudeCliAdapter with a
  chat.completions-compatible interface. Tool calls use JSON-in-prompt
  encoding (injects tool definitions into the system prompt and parses a
  `{"tool_call": {...}}` sentinel block from the response). Multi-turn
  history is preserved via <conversation_history> XML tags. Async shim
  provided via asyncio.to_thread. Includes is_claude_cli_available() for
  zero-cost availability check.
- `hermes_cli/providers.py`: Register "claude-cli" in HERMES_OVERLAYS with
  transport="claude_cli" and auth_type="external_process". Add aliases
  (claude_cli, claude-code-cli, claudecli), label override, and
  "claude_cli" → "claude_cli" in TRANSPORT_TO_API_MODE.
- `hermes_cli/models.py`: Add "claude-cli" model list
  (opus-4-6, sonnet-4-6, opus-4-5, sonnet-4-5, haiku-4-5, 3-7-sonnet).
- `agent/auxiliary_client.py`: Add _try_claude_cli(), append it to
  _get_provider_chain() as the last-resort fallback, handle "claude-cli"
  in resolve_provider_client(), and extend _to_async_client() to wrap
  ClaudeCliAdapter in AsyncClaudeCliAdapter.
- `run_agent.py`: Recognise "claude_cli" as a valid api_mode, initialise
  ClaudeCliAdapter in __init__ and switch_model(), and dispatch to it in
  _interruptible_api_call and the streaming path (non-streaming fallback).

Installation requirement:
  npm install -g @anthropic-ai/claude-code

Usage:
  hermes model claude-cli  # or --provider claude-cli

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API labels Apr 30, 2026
@benmont

benmont commented May 16, 2026

Copy link
Copy Markdown

Picked this up into my fork and hit three test failures worth flagging before this merges:

1. test_returns_four_entries — expects 4 entries in _get_provider_chain(), gets 5 after claude-cli is added. The assertion and expected list both need updating to include claude-cli.

2. Model auto-detection broken for Anthropic modelsclaude-cli is inserted into _PROVIDER_MODELS before anthropic, so detect_static_provider_for_model() returns claude-cli for model names like claude-opus-4-6 and short aliases like sonnet. Breaks test_anthropic_model_detected, test_short_alias_resolves_to_static_model, test_startup_runtime_resolves_short_alias_without_network, and test_startup_runtime_does_not_call_network_detector.

Fix: move the "claude-cli" entry in _PROVIDER_MODELS to after the "anthropic" entry. The detection loop iterates in insertion order and returns the first match, so anthropic needs to appear first for its model names.

umi-mat pushed a commit to umi-mat/yerx-hermes-agent that referenced this pull request Jun 26, 2026
… run on current main

The cherry-picked PR was authored against an older revision of main and missed
several integration points that have since been added to run_agent.py and the
provider registry. This commit makes the claude_cli api_mode actually reachable
end-to-end and pins the CLI flag to the modern Claude Code 2.1.x option name.

Build fixes:
  * hermes_cli/auth.py — register `claude-cli` in PROVIDER_REGISTRY so
    resolve_provider() doesn't reject it as unknown.
  * hermes_cli/runtime_provider.py — add a claude-cli branch so
    resolve_runtime_provider() returns api_mode="claude_cli" instead of
    silently falling back to openrouter.
  * run_agent.py — add claude_cli arms to _build_api_kwargs(), the
    validate_response path, the finish-reason resolver, and the main
    normalize-response path. The adapter already returns OpenAI-shaped
    choices, so these branches bypass the transport layer.
  * agent/claude_cli_adapter.py — `claude` 2.1.139 renamed `--system` to
    `--append-system-prompt`. Update the subprocess invocation.

Verified end-to-end: `hermes --provider claude-cli --model
claude-haiku-4-5-20251001 -z "Reply with exactly: PONG"` returns PONG.

Known limitation (not introduced by this commit): the adapter's
_TOOL_CALL_RE uses non-greedy `.*?` and fails to balance nested braces in
real tool-call payloads. Tool dispatch via the JSON sentinel is therefore
unreliable on real-world arguments; left for a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the substantial implementation work.

This automated hermes-sweeper review is closing this under the standing in-tree-provider-integration policy: vendor-specific inference transports must ship as standalone plugins rather than core provider/runtime integrations.

  • The PR adds a Claude CLI subprocess transport across agent/claude_cli_adapter.py, agent/auxiliary_client.py, hermes_cli/providers.py, hermes_cli/models.py, and run_agent.py.
  • AGENTS.md:815-836 documents the supported model-provider plugin discovery path, including user-installed providers at $HERMES_HOME/plugins/model-providers/<name>/.
  • The existing review comment correctly also flags integration issues in the submitted diff, including model-provider ordering and missing auxiliary-chain test updates: feat(providers): add Claude CLI subprocess transport #6427 (comment)

Please consider publishing this as a standalone model-provider plugin repository for installation into ~/.hermes/plugins/; this keeps the vendor transport independently maintainable while using Hermes's provider discovery surface.


Closed as not-planned per standing maintainer policy (in-tree-provider-integration). This is a design-direction decision, not a code-quality judgment — see the Contribution Rubric in AGENTS.md for what the project is looking for. If you believe this policy was misapplied to your change, comment here and a maintainer will take a look.

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

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have provider/anthropic Anthropic native Messages API sweeper:not-planned Sweeper: closed per standing maintainer policy (design direction) type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants