fix: separate Anthropic OAuth tokens from API keys

Persist OAuth/setup tokens in ANTHROPIC_TOKEN instead of ANTHROPIC_API_KEY.
Reserve ANTHROPIC_API_KEY for regular Console API keys.

Changes:
- anthropic_adapter: reorder resolve_anthropic_token() priority —
  ANTHROPIC_TOKEN first, ANTHROPIC_API_KEY as legacy fallback
- config: add save_anthropic_oauth_token() / save_anthropic_api_key() helpers
  that clear the opposing slot to prevent priority conflicts
- config: show_config() prefers ANTHROPIC_TOKEN for display
- setup: OAuth login and pasted setup-tokens write to ANTHROPIC_TOKEN
- setup: API key entry writes to ANTHROPIC_API_KEY and clears ANTHROPIC_TOKEN
- main: same fixes in _run_anthropic_oauth_flow() and _model_flow_anthropic()
- main: _has_any_provider_configured() checks ANTHROPIC_TOKEN
- doctor: use _is_oauth_token() for correct auth method validation
- runtime_provider: updated error message
- run_agent: simplified client init to use resolve_anthropic_token()
- run_agent: updated 401 troubleshooting messages
- status: prefer ANTHROPIC_TOKEN in status display
- tests: updated priority test, added persistence helper tests

Cherry-picked from PR #1141 by kshitijk4poor, rebased onto current main
with unrelated changes (web_policy config, blocklist CLI) removed.

Co-authored-by: kshitijk4poor <kshitijk4poor@users.noreply.github.com>
This commit is contained in:
kshitijk4poor 2026-03-13 02:09:52 -07:00 committed by teknium1
parent f562d97f13
commit bb3f5ed32a
10 changed files with 114 additions and 43 deletions

View file

@ -1034,6 +1034,20 @@ def save_env_value(key: str, value: str):
pass
def save_anthropic_oauth_token(value: str, save_fn=None):
"""Persist an Anthropic OAuth/setup token and clear the API-key slot."""
writer = save_fn or save_env_value
writer("ANTHROPIC_TOKEN", value)
writer("ANTHROPIC_API_KEY", "")
def save_anthropic_api_key(value: str, save_fn=None):
"""Persist an Anthropic API key and clear the OAuth/setup-token slot."""
writer = save_fn or save_env_value
writer("ANTHROPIC_API_KEY", value)
writer("ANTHROPIC_TOKEN", "")
def get_env_value(key: str) -> Optional[str]:
"""Get a value from ~/.hermes/.env or environment."""
# Check environment first
@ -1081,7 +1095,6 @@ def show_config():
keys = [
("OPENROUTER_API_KEY", "OpenRouter"),
("ANTHROPIC_API_KEY", "Anthropic"),
("VOICE_TOOLS_OPENAI_KEY", "OpenAI (STT/TTS)"),
("FIRECRAWL_API_KEY", "Firecrawl"),
("BROWSERBASE_API_KEY", "Browserbase"),
@ -1091,6 +1104,8 @@ def show_config():
for env_key, name in keys:
value = get_env_value(env_key)
print(f" {name:<14} {redact_key(value)}")
anthropic_value = get_env_value("ANTHROPIC_TOKEN") or get_env_value("ANTHROPIC_API_KEY")
print(f" {'Anthropic':<14} {redact_key(anthropic_value)}")
# Model settings
print()

View file

@ -38,6 +38,7 @@ _PROVIDER_ENV_HINTS = (
"OPENROUTER_API_KEY",
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"ANTHROPIC_TOKEN",
"OPENAI_BASE_URL",
"GLM_API_KEY",
"ZAI_API_KEY",
@ -493,17 +494,22 @@ def run_doctor(args):
else:
check_warn("OpenRouter API", "(not configured)")
anthropic_key = os.getenv("ANTHROPIC_API_KEY")
anthropic_key = os.getenv("ANTHROPIC_TOKEN") or os.getenv("ANTHROPIC_API_KEY")
if anthropic_key:
print(" Checking Anthropic API...", end="", flush=True)
try:
import httpx
from agent.anthropic_adapter import _is_oauth_token, _COMMON_BETAS, _OAUTH_ONLY_BETAS
headers = {"anthropic-version": "2023-06-01"}
if _is_oauth_token(anthropic_key):
headers["Authorization"] = f"Bearer {anthropic_key}"
headers["anthropic-beta"] = ",".join(_COMMON_BETAS + _OAUTH_ONLY_BETAS)
else:
headers["x-api-key"] = anthropic_key
response = httpx.get(
"https://api.anthropic.com/v1/models",
headers={
"x-api-key": anthropic_key,
"anthropic-version": "2023-06-01"
},
headers=headers,
timeout=10
)
if response.status_code == 200:

View file

@ -86,7 +86,7 @@ def _has_any_provider_configured() -> bool:
from hermes_cli.auth import PROVIDER_REGISTRY
# Collect all provider env vars
provider_env_vars = {"OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_BASE_URL"}
provider_env_vars = {"OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "OPENAI_BASE_URL"}
for pconfig in PROVIDER_REGISTRY.values():
if pconfig.auth_type == "api_key":
provider_env_vars.update(pconfig.api_key_env_vars)
@ -1593,6 +1593,7 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
def _run_anthropic_oauth_flow(save_env_value):
"""Run the Claude OAuth setup-token flow. Returns True if credentials were saved."""
from agent.anthropic_adapter import run_oauth_setup_token
from hermes_cli.config import save_anthropic_oauth_token
try:
print()
@ -1601,7 +1602,7 @@ def _run_anthropic_oauth_flow(save_env_value):
print()
token = run_oauth_setup_token()
if token:
save_env_value("ANTHROPIC_API_KEY", token)
save_anthropic_oauth_token(token, save_fn=save_env_value)
print(" ✓ OAuth credentials saved.")
return True
@ -1615,7 +1616,7 @@ def _run_anthropic_oauth_flow(save_env_value):
print()
return False
if manual_token:
save_env_value("ANTHROPIC_API_KEY", manual_token)
save_anthropic_oauth_token(manual_token, save_fn=save_env_value)
print(" ✓ Setup-token saved.")
return True
@ -1642,7 +1643,7 @@ def _run_anthropic_oauth_flow(save_env_value):
print()
return False
if token:
save_env_value("ANTHROPIC_API_KEY", token)
save_anthropic_oauth_token(token, save_fn=save_env_value)
print(" ✓ Setup-token saved.")
return True
print(" Cancelled — install Claude Code and try again.")
@ -1656,17 +1657,20 @@ def _model_flow_anthropic(config, current_model=""):
PROVIDER_REGISTRY, _prompt_model_selection, _save_model_choice,
_update_config_for_provider, deactivate_provider,
)
from hermes_cli.config import get_env_value, save_env_value, load_config, save_config
from hermes_cli.config import (
get_env_value, save_env_value, load_config, save_config,
save_anthropic_api_key,
)
from hermes_cli.models import _PROVIDER_MODELS
pconfig = PROVIDER_REGISTRY["anthropic"]
# Check ALL credential sources
existing_key = (
get_env_value("ANTHROPIC_API_KEY")
or os.getenv("ANTHROPIC_API_KEY", "")
or get_env_value("ANTHROPIC_TOKEN")
get_env_value("ANTHROPIC_TOKEN")
or os.getenv("ANTHROPIC_TOKEN", "")
or get_env_value("ANTHROPIC_API_KEY")
or os.getenv("ANTHROPIC_API_KEY", "")
or os.getenv("CLAUDE_CODE_OAUTH_TOKEN", "")
)
cc_available = False
@ -1734,7 +1738,7 @@ def _model_flow_anthropic(config, current_model=""):
if not api_key:
print(" Cancelled.")
return
save_env_value("ANTHROPIC_API_KEY", api_key)
save_anthropic_api_key(api_key, save_fn=save_env_value)
print(" ✓ API key saved.")
else:

View file

@ -159,7 +159,7 @@ def resolve_runtime_provider(
token = resolve_anthropic_token()
if not token:
raise AuthError(
"No Anthropic credentials found. Set ANTHROPIC_API_KEY, "
"No Anthropic credentials found. Set ANTHROPIC_TOKEN or ANTHROPIC_API_KEY, "
"run 'claude setup-token', or authenticate with 'claude /login'."
)
return {

View file

@ -1074,6 +1074,7 @@ def setup_model_provider(config: dict):
print()
print_header("Anthropic Authentication")
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_cli.config import save_anthropic_api_key, save_anthropic_oauth_token
pconfig = PROVIDER_REGISTRY["anthropic"]
# Check ALL credential sources
@ -1086,8 +1087,8 @@ def setup_model_provider(config: dict):
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")
get_env_value("ANTHROPIC_TOKEN")
or get_env_value("ANTHROPIC_API_KEY")
or _os.getenv("CLAUDE_CODE_OAUTH_TOKEN", "")
)
@ -1127,14 +1128,14 @@ def setup_model_provider(config: dict):
print()
token = run_oauth_setup_token()
if token:
save_env_value("ANTHROPIC_API_KEY", token)
save_anthropic_oauth_token(token, save_fn=save_env_value)
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)
save_anthropic_oauth_token(token, save_fn=save_env_value)
print_success("Setup-token saved")
else:
print_warning("Skipped — agent won't work without credentials")
@ -1148,7 +1149,7 @@ def setup_model_provider(config: dict):
print()
token = prompt("Setup-token (sk-ant-oat-...)", password=True)
if token:
save_env_value("ANTHROPIC_API_KEY", token)
save_anthropic_oauth_token(token, save_fn=save_env_value)
print_success("Setup-token saved")
else:
print_warning("Skipped — install Claude Code and re-run setup")
@ -1158,7 +1159,7 @@ def setup_model_provider(config: dict):
print()
api_key = prompt("API key (sk-ant-...)", password=True)
if api_key:
save_env_value("ANTHROPIC_API_KEY", api_key)
save_anthropic_api_key(api_key, save_fn=save_env_value)
print_success("API key saved")
else:
print_warning("Skipped — agent won't work without credentials")

View file

@ -77,7 +77,6 @@ def show_status(args):
keys = {
"OpenRouter": "OPENROUTER_API_KEY",
"Anthropic": "ANTHROPIC_API_KEY",
"OpenAI": "OPENAI_API_KEY",
"Z.AI/GLM": "GLM_API_KEY",
"Kimi": "KIMI_API_KEY",
@ -98,6 +97,14 @@ def show_status(args):
display = redact_key(value) if not show_all else value
print(f" {name:<12} {check_mark(has_key)} {display}")
anthropic_value = (
get_env_value("ANTHROPIC_TOKEN")
or get_env_value("ANTHROPIC_API_KEY")
or ""
)
anthropic_display = redact_key(anthropic_value) if not show_all else anthropic_value
print(f" {'Anthropic':<12} {check_mark(bool(anthropic_value))} {anthropic_display}")
# =========================================================================
# Auth Providers (OAuth)
# =========================================================================