feat: native Anthropic provider with Claude Code credential auto-discovery - #1097
Merged
Conversation
…overy Add Anthropic as a first-class inference provider, bypassing OpenRouter for direct API access. Uses the native Anthropic SDK with a full format adapter (same pattern as the codex_responses api_mode). ## Auth (three methods, priority order) 1. ANTHROPIC_API_KEY env var (regular API key, sk-ant-api-*) 2. ANTHROPIC_TOKEN / CLAUDE_CODE_OAUTH_TOKEN env var (setup-token, sk-ant-oat-*) 3. Auto-discovery from ~/.claude/.credentials.json (Claude Code subscription) - Reads Claude Code's OAuth credentials - Checks token expiry with 60s buffer - Setup tokens use Bearer auth + anthropic-beta: oauth-2025-04-20 header - Regular API keys use standard x-api-key header ## Changes by file ### New files - agent/anthropic_adapter.py — Client builder, message/tool/response format conversion, Claude Code credential reader, token resolver. Handles system prompt extraction, tool_use/tool_result blocks, thinking/reasoning, orphaned tool_use cleanup, cache_control. - tests/test_anthropic_adapter.py — 36 tests covering all adapter logic ### Modified files - pyproject.toml — Add anthropic>=0.39.0 dependency - hermes_cli/auth.py — Add 'anthropic' to PROVIDER_REGISTRY with three env vars, plus 'claude'/'claude-code' aliases - hermes_cli/models.py — Add model catalog, labels, aliases, provider order - hermes_cli/main.py — Add 'anthropic' to --provider CLI choices - hermes_cli/runtime_provider.py — Add Anthropic branch returning api_mode='anthropic_messages' (before generic api_key fallthrough) - hermes_cli/setup.py — Add Anthropic setup wizard with Claude Code credential auto-discovery, model selection, OpenRouter tools prompt - agent/auxiliary_client.py — Add claude-haiku-4-5 as aux model - agent/model_metadata.py — Add bare Claude model context lengths - run_agent.py — Add anthropic_messages api_mode: * Client init (Anthropic SDK instead of OpenAI) * API call dispatch (_anthropic_client.messages.create) * Response validation (content blocks) * finish_reason mapping (stop_reason -> finish_reason) * Token usage (input_tokens/output_tokens) * Response normalization (normalize_anthropic_response) * Client interrupt/rebuild * Prompt caching auto-enabled for native Anthropic - tests/test_run_agent.py — Update test_anthropic_base_url_accepted to expect native routing, add test_prompt_caching_native_anthropic
After studying clawdbot (OpenClaw) and OpenCode implementations: ## Beta headers - Add interleaved-thinking-2025-05-14 and fine-grained-tool-streaming-2025-05-14 as common betas (sent with ALL auth types, not just OAuth) - OAuth tokens additionally get oauth-2025-04-20 - API keys now also get the common betas (previously got none) ## Vision/image support - Add _convert_vision_content() to convert OpenAI multimodal format (image_url blocks) to Anthropic format (image blocks with base64/url source) - Handles both data: URIs (base64) and regular URLs ## Role alternation enforcement - Anthropic strictly rejects consecutive same-role messages (400 error) - Add post-processing step that merges consecutive user/assistant messages - Handles string, list, and mixed content types during merge ## Tool choice support - Add tool_choice parameter to build_anthropic_kwargs() - Maps OpenAI values: auto→auto, required→any, none→omit, name→tool ## Cache metrics tracking - Anthropic uses cache_read_input_tokens / cache_creation_input_tokens (different from OpenRouter's prompt_tokens_details.cached_tokens) - Add api_mode-aware branch in run_agent.py cache stats logging ## Credential refresh on 401 - On 401 error during anthropic_messages mode, re-read credentials via resolve_anthropic_token() (picks up refreshed Claude Code tokens) - Rebuild client if new token differs from current one - Follows same pattern as Codex/Nous 401 refresh handlers ## Tests - 44 adapter tests (8 new: vision conversion, role alternation, tool choice) - Updated beta header tests to verify new structure - Full suite: 3198 passed, 0 regressions
…ion aux
Feedback fixes:
1. Revert _convert_vision_content — vision is handled by the vision_analyze
tool, not by converting image blocks inline in conversation messages.
Removed the function and its tests.
2. Add Anthropic to 'hermes model' (cmd_model in main.py):
- Added to provider_labels dict
- Added to providers selection list
- Added _model_flow_anthropic() with Claude Code credential auto-detection,
API key prompting, and model selection from catalog.
3. Wire up Anthropic as a vision-capable auxiliary provider:
- Added _try_anthropic() to auxiliary_client.py using claude-sonnet-4
as the vision model (Claude natively supports multimodal)
- Added to the get_vision_auxiliary_client() auto-detection chain
(after OpenRouter/Nous, before Codex/custom)
Cache tracking note: the Anthropic cache metrics branch in run_agent.py
(cache_read_input_tokens / cache_creation_input_tokens) is in the correct
place — it's response-level parsing, same location as the existing
OpenRouter cache tracking. auxiliary_client.py has no cache tracking.
Both 'hermes model' and 'hermes setup model' now present a clear
two-option auth flow when no credentials are found:
1. Claude Pro/Max subscription (setup-token)
- Step-by-step instructions to run 'claude setup-token'
- User pastes the resulting sk-ant-oat01-... token
2. Anthropic API key (pay-per-token)
- Link to console.anthropic.com/settings/keys
- User pastes sk-ant-api03-... key
Also handles:
- Auto-detection of existing Claude Code creds (~/.claude/.credentials.json)
- Existing credentials shown with option to update
- Consistent UX between 'hermes model' and 'hermes setup model'
…2.x) The critical bug: read_claude_code_credentials() only looked at ~/.claude/.credentials.json, but Claude Code's native binary (v2.x, Bun-compiled) stores credentials in ~/.claude.json at the top level as 'primaryApiKey'. The .credentials.json file is only written by older npm-based installs. Now checks both locations in priority order: 1. ~/.claude.json → primaryApiKey (native binary, v2.x) 2. ~/.claude/.credentials.json → claudeAiOauth.accessToken (legacy) Verified live: hermes model → Anthropic → auto-detected credentials → claude-sonnet-4-20250514 → 'Hello there, how are you?' (5 words)
- Add _fetch_anthropic_models() to hermes_cli/models.py — hits the Anthropic /v1/models endpoint to get the live model catalog. Handles both API key and OAuth token auth headers. - Wire it into provider_model_ids() so both 'hermes model' and 'hermes setup model' show the live list instead of a stale static one. - Update static _PROVIDER_MODELS fallback with full current catalog: opus-4-6, sonnet-4-6, opus-4-5, sonnet-4-5, opus-4, sonnet-4, haiku-4-5 - Update model_metadata.py with context lengths for all current models. - Fix thinking parameter for 4.5+ models: use type='adaptive' instead of type='enabled' (Anthropic deprecated 'enabled' for newer models, warns at runtime). Detects model version from the model name string. Verified live: hermes model → Anthropic → auto-detected creds → shows 7 live models hermes chat --provider anthropic --model claude-opus-4-6 → works
Fixes from comprehensive code review and cross-referencing with clawdbot/OpenCode implementations: CRITICAL: - Add one-shot guard (anthropic_auth_retry_attempted) to prevent infinite 401 retry loops when credentials keep changing - Fix _is_oauth_token(): managed keys from ~/.claude.json are NOT regular API keys (don't start with sk-ant-api). Inverted the logic: only sk-ant-api* is treated as API key auth, everything else uses Bearer auth + oauth beta headers HIGH: - Wrap json.loads(args) in try/except in message conversion — malformed tool_call arguments no longer crash the entire conversation - Raise AuthError in runtime_provider when no Anthropic token found (was silently passing empty string, causing confusing API errors) - Remove broken _try_anthropic() from auxiliary vision chain — the centralized router creates an OpenAI client for api_key providers which doesn't work with Anthropic's Messages API MEDIUM: - Handle empty assistant message content — Anthropic rejects empty content blocks, now inserts '(empty)' placeholder - Fix setup.py existing_key logic — set to 'KEEP' sentinel instead of None to prevent falling through to the auth choice prompt - Add debug logging to _fetch_anthropic_models on failure Tests: 43 adapter tests (2 new for token detection), 3197 total passed
…mp=1 Remaining issues from deep scan: Adapter (agent/anthropic_adapter.py): - Add _sanitize_tool_id() — Anthropic requires IDs matching [a-zA-Z0-9_-], now strips invalid chars and ensures non-empty (both tool_use and tool_result) - Empty tool result content → '(no output)' placeholder (Anthropic rejects empty) - Set temperature=1 when thinking type='enabled' on older models (required) - normalize_model_name now case-insensitive for 'Anthropic/' prefix - Fix stale docstrings referencing only ~/.claude/.credentials.json Agent loop (run_agent.py): - Guard memory flush path (line ~2684) — was calling self.client.chat.completions which is None in anthropic_messages mode. Now routes through Anthropic client. - Guard summary generation path (line ~3171) — same crash when reaching iteration limit. Now builds proper Anthropic kwargs and normalizes response. - Guard retry summary path (line ~3200) — same fix for the summary retry loop. All three self.client.chat.completions.create() calls outside the main loop now have anthropic_messages branches to prevent NoneType crashes.
- quickstart.md: Add Anthropic to the provider comparison table - configuration.md: Add Anthropic to provider list table, add full 'Anthropic (Native)' section with three auth methods (API key, setup-token, Claude Code auto-detect), config.yaml example, and provider alias tip - environment-variables.md: Add ANTHROPIC_API_KEY, ANTHROPIC_TOKEN, CLAUDE_CODE_OAUTH_TOKEN to LLM Providers table; add 'anthropic' to HERMES_INFERENCE_PROVIDER values list
…c response format The memory flush path extracted tool_calls from the response assuming OpenAI format (response.choices[0].message.tool_calls). When using the Anthropic client directly (aux unavailable), the response is an Anthropic Message object which has no .choices attribute. Now uses normalize_anthropic_response() to extract tool_calls correctly.
angelburgosrosado
pushed a commit
to angelburgosrosado/hermes-agent
that referenced
this pull request
Apr 27, 2026
…c877bdeb feat: native Anthropic provider with Claude Code credential auto-discovery
02356abc
pushed a commit
to 02356abc/hermes-agent
that referenced
this pull request
May 14, 2026
…c877bdeb feat: native Anthropic provider with Claude Code credential auto-discovery
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…c877bdeb feat: native Anthropic provider with Claude Code credential auto-discovery
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.
Native Anthropic Provider with Claude Code Auto-Discovery
Adds Anthropic as a first-class inference provider using the native Anthropic SDK. 15 files, 10 commits, 43 adapter tests, 3197 total passing.
Auth (three methods, auto-resolved)
sk-ant-api-*)sk-ant-oat-*)~/.claude.json(native binary v2.x) or~/.claude/.credentials.json(legacy npm)hermes modelflow/v1/modelsendpoint (falls back to static list)Implementation details
anthropic_messagesapi_mode (same pattern ascodex_responses)interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14(all auth), plusoauth-2025-04-20(OAuth only)type=adaptive),type=enabled+temperature=1for oldercache_read_input_tokens/cache_creation_input_tokens)[a-zA-Z0-9_-]only, per Anthropic spec)sk-ant-api*uses x-api-key; everything else uses Bearer authself.clientcalls guarded: memory flush, summary generation, summary retryFiles changed
agent/anthropic_adapter.pytests/test_anthropic_adapter.pypyproject.tomlanthropic>=0.39.0dependencyhermes_cli/auth.pyhermes_cli/models.pyhermes_cli/main.py--providerchoice +_model_flow_anthropic()hermes_cli/runtime_provider.pyanthropic_messages+ AuthErrorhermes_cli/setup.pyagent/auxiliary_client.pyagent/model_metadata.pyrun_agent.pyanthropic_messagesbranchestests/test_run_agent.pywebsite/docs/getting-started/quickstart.mdwebsite/docs/user-guide/configuration.mdwebsite/docs/reference/environment-variables.mdVerified live
Test plan