fix: Anthropic OAuth — beta header, token refresh, config contamination, reauthentication (#1132)
Fixes Anthropic OAuth/subscription authentication end-to-end: Auth failures (401 errors): - Add missing 'claude-code-20250219' beta header for OAuth tokens. Both clawdbot and OpenCode include this alongside 'oauth-2025-04-20' — without it, Anthropic's API rejects OAuth tokens with 401 authentication errors. - Fix _fetch_anthropic_models() to use canonical beta headers from _COMMON_BETAS + _OAUTH_ONLY_BETAS instead of hardcoding. Token refresh: - Add _refresh_oauth_token() — when Claude Code credentials from ~/.claude/.credentials.json are expired but have a refresh token, automatically POST to console.anthropic.com/v1/oauth/token to get a new access token. Uses the same client_id as Claude Code / OpenCode. - Add _write_claude_code_credentials() — writes refreshed tokens back to ~/.claude/.credentials.json, preserving other fields. - resolve_anthropic_token() now auto-refreshes expired tokens before returning None. Config contamination: - Anthropic's _model_flow_anthropic() no longer saves base_url to config. Since resolve_runtime_provider() always hardcodes Anthropic's URL, the stale base_url was contaminating other providers when users switched without re-running 'hermes model' (e.g., Codex hitting api.anthropic.com). - _update_config_for_provider() now pops base_url when passed empty string. - Same fix in setup.py. Flow/UX (hermes model command): - CLAUDE_CODE_OAUTH_TOKEN env var now checked in credential detection - Reauthentication option when existing credentials found - run_oauth_setup_token() runs 'claude setup-token' as interactive subprocess, then auto-detects saved credentials - Clean has_creds/needs_auth flow in both main.py and setup.py Tests (14 new): - Beta header assertions for claude-code-20250219 - Token refresh: successful refresh with credential writeback, failed refresh returns None, no refresh token returns None - Credential writeback: new file creation, preserving existing fields - Auto-refresh integration in resolve_anthropic_token() - CLAUDE_CODE_OAUTH_TOKEN fallback, credential file auto-discovery - run_oauth_setup_token() (5 scenarios)
This commit is contained in:
parent
6ceae61a56
commit
d24bcad90b
6 changed files with 500 additions and 85 deletions
|
|
@ -1076,65 +1076,101 @@ def setup_model_provider(config: dict):
|
|||
from hermes_cli.auth import PROVIDER_REGISTRY
|
||||
pconfig = PROVIDER_REGISTRY["anthropic"]
|
||||
|
||||
# Check for Claude Code credential auto-discovery
|
||||
from agent.anthropic_adapter import read_claude_code_credentials, is_claude_code_token_valid
|
||||
# Check ALL credential sources
|
||||
import os as _os
|
||||
from agent.anthropic_adapter import (
|
||||
read_claude_code_credentials, is_claude_code_token_valid,
|
||||
run_oauth_setup_token,
|
||||
)
|
||||
cc_creds = read_claude_code_credentials()
|
||||
if cc_creds and is_claude_code_token_valid(cc_creds):
|
||||
print_success("Found valid Claude Code credentials (~/.claude/.credentials.json)")
|
||||
if prompt_yes_no("Use these credentials?", True):
|
||||
print_success("Using Claude Code subscription credentials")
|
||||
else:
|
||||
cc_creds = None
|
||||
cc_valid = bool(cc_creds and is_claude_code_token_valid(cc_creds))
|
||||
|
||||
existing_key = get_env_value("ANTHROPIC_API_KEY") or get_env_value("ANTHROPIC_TOKEN")
|
||||
existing_key = (
|
||||
get_env_value("ANTHROPIC_API_KEY")
|
||||
or get_env_value("ANTHROPIC_TOKEN")
|
||||
or _os.getenv("CLAUDE_CODE_OAUTH_TOKEN", "")
|
||||
)
|
||||
|
||||
if not (cc_creds and is_claude_code_token_valid(cc_creds)):
|
||||
has_creds = bool(existing_key) or cc_valid
|
||||
needs_auth = not has_creds
|
||||
|
||||
if has_creds:
|
||||
if existing_key:
|
||||
print_info(f"Current credentials: {existing_key[:12]}...")
|
||||
if not prompt_yes_no("Update credentials?", False):
|
||||
# User wants to keep existing — skip auth prompt entirely
|
||||
existing_key = "KEEP" # truthy sentinel to skip auth choice
|
||||
elif cc_valid:
|
||||
print_success("Found valid Claude Code credentials (auto-detected)")
|
||||
|
||||
if not existing_key and not (cc_creds and is_claude_code_token_valid(cc_creds)):
|
||||
auth_choices = [
|
||||
"Claude Pro/Max subscription (setup-token)",
|
||||
"Anthropic API key (pay-per-token)",
|
||||
]
|
||||
auth_idx = prompt_choice("Choose authentication method:", auth_choices, 0)
|
||||
auth_choices = [
|
||||
"Use existing credentials",
|
||||
"Reauthenticate (new OAuth login)",
|
||||
"Cancel",
|
||||
]
|
||||
choice_idx = prompt_choice("What would you like to do?", auth_choices, 0)
|
||||
if choice_idx == 1:
|
||||
needs_auth = True
|
||||
elif choice_idx == 2:
|
||||
pass # fall through to provider config
|
||||
|
||||
if auth_idx == 0:
|
||||
if needs_auth:
|
||||
auth_choices = [
|
||||
"Claude Pro/Max subscription (OAuth login)",
|
||||
"Anthropic API key (pay-per-token)",
|
||||
]
|
||||
auth_idx = prompt_choice("Choose authentication method:", auth_choices, 0)
|
||||
|
||||
if auth_idx == 0:
|
||||
# OAuth setup-token flow
|
||||
try:
|
||||
print()
|
||||
print_info("To get a setup-token from your Claude subscription:")
|
||||
print_info(" 1. Install Claude Code: npm install -g @anthropic-ai/claude-code")
|
||||
print_info(" 2. Run: claude setup-token")
|
||||
print_info(" 3. Open the URL it prints in your browser")
|
||||
print_info(" 4. Log in and click \"Authorize\"")
|
||||
print_info(" 5. Paste the auth code back into Claude Code")
|
||||
print_info(" 6. Copy the resulting sk-ant-oat01-... token")
|
||||
print_info("Running 'claude setup-token' — follow the prompts below.")
|
||||
print_info("A browser window will open for you to authorize access.")
|
||||
print()
|
||||
token = prompt("Paste setup-token here", password=True)
|
||||
token = run_oauth_setup_token()
|
||||
if token:
|
||||
save_env_value("ANTHROPIC_API_KEY", token)
|
||||
print_success("OAuth credentials saved")
|
||||
else:
|
||||
# Subprocess completed but no token auto-detected
|
||||
print()
|
||||
token = prompt("Paste setup-token here (if displayed above)", password=True)
|
||||
if token:
|
||||
save_env_value("ANTHROPIC_API_KEY", token)
|
||||
print_success("Setup-token saved")
|
||||
else:
|
||||
print_warning("Skipped — agent won't work without credentials")
|
||||
except FileNotFoundError:
|
||||
print()
|
||||
print_info("The 'claude' CLI is required for OAuth login.")
|
||||
print()
|
||||
print_info("To install: npm install -g @anthropic-ai/claude-code")
|
||||
print_info("Then run: claude setup-token")
|
||||
print_info("Or paste an existing setup-token below:")
|
||||
print()
|
||||
token = prompt("Setup-token (sk-ant-oat-...)", password=True)
|
||||
if token:
|
||||
save_env_value("ANTHROPIC_API_KEY", token)
|
||||
print_success("Setup-token saved")
|
||||
else:
|
||||
print_warning("Skipped — agent won't work without credentials")
|
||||
print_warning("Skipped — install Claude Code and re-run setup")
|
||||
else:
|
||||
print()
|
||||
print_info("Get an API key at: https://console.anthropic.com/settings/keys")
|
||||
print()
|
||||
api_key = prompt("API key (sk-ant-...)", password=True)
|
||||
if api_key:
|
||||
save_env_value("ANTHROPIC_API_KEY", api_key)
|
||||
print_success("API key saved")
|
||||
else:
|
||||
print()
|
||||
print_info("Get an API key at: https://console.anthropic.com/settings/keys")
|
||||
print()
|
||||
api_key = prompt("API key (sk-ant-api03-...)", password=True)
|
||||
if api_key:
|
||||
save_env_value("ANTHROPIC_API_KEY", api_key)
|
||||
print_success("API key saved")
|
||||
else:
|
||||
print_warning("Skipped — agent won't work without credentials")
|
||||
print_warning("Skipped — agent won't work without credentials")
|
||||
|
||||
# Clear custom endpoint vars if switching
|
||||
if existing_custom:
|
||||
save_env_value("OPENAI_BASE_URL", "")
|
||||
save_env_value("OPENAI_API_KEY", "")
|
||||
_update_config_for_provider("anthropic", pconfig.inference_base_url)
|
||||
_set_model_provider(config, "anthropic", pconfig.inference_base_url)
|
||||
# Don't save base_url for Anthropic — resolve_runtime_provider()
|
||||
# always hardcodes it. Stale base_urls contaminate other providers.
|
||||
_update_config_for_provider("anthropic", "")
|
||||
_set_model_provider(config, "anthropic")
|
||||
|
||||
# else: provider_idx == 9 (Keep current) — only shown when a provider already exists
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue