feat: claude-max — Claude subscription as a drop-in Anthropic endpoint (proxy) - #2
Open
snagnever wants to merge 10 commits into
Open
feat: claude-max — Claude subscription as a drop-in Anthropic endpoint (proxy)#2snagnever wants to merge 10 commits into
snagnever wants to merge 10 commits into
Conversation
…alidated Validates the claude-agent-sdk behaviors the claude-max proxy depends on: stream fidelity + non-standard fields to strip, mcp__hermes__ tool prefixing, blocking MCP handler across the tool_use response boundary (90s, no timeout), sequential parallel-tool execution, interrupt recovery, system-prompt isolation. Gate PASSED: primary blocking-handler tool bridge is viable, no fallback needed.
New claude-max provider (anthropic_messages against a local proxy base_url), static models catalog (max_input_tokens=200000, opus/sonnet/haiku + aliases), [claude-max] optional extra (claude-agent-sdk + aiohttp) + provider.claude_max lazy dep. Package hermes_cli.claude_max is import-light (constants + leaf catalog).
Origin server (not a forwarder): create_app with /v1/messages (stub until wired), /v1/models (static catalog), /health. Anthropic-shaped error JSON via middleware; loopback-only bind guard; python -m entrypoint for the auto-start manager; AppKey-typed app state. Tests via aiohttp TestClient.
…SE, errors) build_options_inputs (system join, tools→mcp__hermes__ allowed names, silently accept temperature/top_p); StreamTranslator + MessageAccumulator sanitize raw SDK events to the Anthropic spec (strip diagnostics/stop_details/caller/ context_management/iterations, echo requested model, unprefix tool_use names, assemble input_json_delta); map_result_error (auth/rate-limit/api heuristics).
…streaming ToolBridge: MCP handlers block on futures until the tool_result arrives on the next request; correlates (name,args)->tool_use_id, stores results delivered before sequential handlers run, abort_all on close/interrupt. ClaudeBridge: builds isolated tool-less options (tools=[], setting_sources=[], MCP timeout knobs, caller tools via create_sdk_mcp_server) and segments the persistent receive_response iterator into per-HTTP-response chunks (ends at tool_use message_stop or final ResultMessage). Self-contained fake SDK models the emit->await-handler->continue flow. 6 tests incl. full tool round-trip.
Fingerprint over normalized (system, messages) with cache_control/thinking signatures stripped and string-vs-block content unified. match(): Flow A (tool_result ids ⊆ a session's pending → deliver), Flow B (prior-prefix fingerprint match → query next user turn), Flow C (miss → new session, flatten prior history into a synthetic prompt). record_response updates history_fp + pending_tool_ids; TTL + LRU eviction close bridges. 9 tests.
handle_messages: registry.match → per-session lock → Flow A deliver / Flow B+C query → drive one response chunk. Peeks the first event to return an Anthropic error JSON (auth etc.) before committing to a stream; true SSE streaming via StreamTranslator or aggregation via MessageAccumulator; updates session history_fp + pending_tool_ids after each chunk; keeps the session on client disconnect (hermes reconnects with tool results). app.build_app wires the registry + real ClaudeBridge factory. 4 full-app tests incl. tool round-trip across two requests + 401 auth mapping.
manager.ensure_server_running: health-probe → preflight (claude CLI + SDK + aiohttp) → detached Popen(python -m ...server) → PID record + health poll; stop/status; own check_claude_binary. runtime_provider short-circuits claude-max (+aliases) to auto-start + return the anthropic_messages runtime (no key). providers overlay + label + aliases so /model lists 'Claude Max'; validate_requested_model skips the probe. hermes claude-max serve|start|stop| status subcommand. 8 tests; provider/picker regression green.
Live E2E against the real subscription: /v1/models, non-stream, anthropic-SDK streaming, and the full tool round-trip all work; hermes -z --provider claude-max auto-starts + reaches Claude. Two fixes surfaced by E2E: - correlate blocking handlers to tool_use ids by tool NAME FIFO (the stream's content_block_start has empty input; real args arrive via input_json_delta), which was causing the tool continuation to hang. - map subscription usage/quota exhaustion to a 429 rate_limit_error. Adds plugin README (claude-max vs claude-agent positioning, caveats) + cli-config example. 46 unit tests green.
The picker only surfaces providers with detectable credentials, but claude-max's auth is in the Claude Code CLI (keychain), not an env var — so it was filtered out. Detect availability via check_claude_binary and populate its model list from the profile's fallback_models. Hidden without the CLI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
claude-max: a local Anthropic-Messages-API-compatible HTTP server backed by the Claude Agent SDK + a Claude subscription, so hermes uses the subscription as seamlessly as api.anthropic.com — with hermes' own agent loop, tools, aux tasks, vision, streaming, and usage accounting all working unchanged.Branched off
main(independent of theclaude-agentdeep-integration runtime on the other branch/PR #1) so the two subscription approaches can be evaluated separately:claude-agent— hands the whole turn to a Claude Code subprocess (Claude Code owns the tool loop; hermes' own tools/aux limited).claude-max(this PR) — an API facade; hermes keeps its native agent loop and just sees an Anthropic endpoint.Usage
Selecting the provider auto-starts the proxy. Requires the
claudeCLI (logged in) +pip install 'hermes-agent[claude-max]'. Manual control:hermes claude-max serve|start|stop|status.Architecture (
hermes_cli/claude_max/)POST /v1/messages(stream + non-stream), staticGET /v1/models(max_input_tokens=200000 so probes don't hang),/health; Anthropic-shaped errors; loopback-only bind.ClaudeSDKClientper conversation. Request tools are registered as in-process MCP tools whose handlers block until hermes returns the matchingtool_resulton the next request → the stateless Anthropic tool loop drives the stateful SDK.tools=[]+setting_sources=[]isolate it; streaming reuses the SDK's raw Anthropic stream events (sanitized to spec).mcp__hermes__tool names); usage + error mapping.claudeCLI + SDK + aiohttp) → detachedpython -m …server→ PID + health poll.[claude-max]extra;runtime_providershort-circuit (auto-start + no-key anthropic_messages runtime); picker overlay/label/aliases; validation probe skip.Design de-risking (Task 0 spike, real SDK)
scripts/claude_max_spike.py+CLAUDE_MAX_SPIKE_FINDINGS.mdvalidated: raw StreamEvent fidelity + fields to strip,mcp__hermes__tool prefixing, blocking MCP handler across the tool_use response boundary (90s, no timeout viaMCP_TOOL_TIMEOUT), sequential parallel-tool execution, interrupt recovery, system-prompt isolation.Testing
/v1/models, non-stream completion, anthropic python SDK streaming + full tool round-trip (the exact transport hermes uses), andhermes -z --provider claude-maxauto-start reaching Claude. Two bugs were caught and fixed via E2E (tool-name correlation for the empty-input stream event; quota→429 mapping).Caveats
No
temperature/top_p(SDK doesn't expose them). Cold-start (proxy restart / compacted history) falls back to flattening prior turns (thinking continuity lost, cache metrics reset) — normal turn-by-turn stays pinned. Localhost-only, unauthenticated socket (loopback is the trust boundary). Subscription limits surface asrate_limit_error(429).🤖 Generated with Claude Code