Release YK (v0.51.681): Anthropic OAuth-token env detection parity (#4995, fixes #4770)

Release YK (v0.51.681): Anthropic OAuth-token env detection parity (#4995, fixes #4770)
This commit is contained in:
nesquena-hermes 2026-06-26 09:16:15 -07:00 committed by GitHub
commit 7763e92d09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 165 additions and 3 deletions

View file

@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.681] — 2026-06-26 — Release YK (Anthropic shows in the model picker when configured via OAuth token)
### Fixed
- **The model picker now detects Anthropic when it's configured through an OAuth token instead of `ANTHROPIC_API_KEY`.** WebUI's env-fallback provider detection only looked for `ANTHROPIC_API_KEY`, so an Anthropic setup using OAuth token env vars (`ANTHROPIC_TOKEN` / `CLAUDE_CODE_OAUTH_TOKEN`, which the shared agent already accepts) fell out of parity and the provider didn't surface. The fallback now derives the Anthropic env-var set from the shared agent provider registry (with a built-in fallback list), and env values are stripped before the availability check so a whitespace-only token can't false-positive the provider. Thanks @rodboev. (#4995, fixes #4770)
## [v0.51.680] — 2026-06-26 — Release YJ (session-index rebuild can't be clobbered by a late worker)
### Fixed

View file

@ -1171,6 +1171,37 @@ _PROVIDER_ALIASES = {
}
def _get_anthropic_fallback_env_vars() -> tuple[str, ...]:
"""Read Anthropic auth env vars from the shared agent registry when available."""
fallback = (
"ANTHROPIC_API_KEY",
"ANTHROPIC_TOKEN",
"CLAUDE_CODE_OAUTH_TOKEN",
)
try:
from hermes_cli.auth import PROVIDER_REGISTRY
anthropic = (
PROVIDER_REGISTRY.get("anthropic")
if isinstance(PROVIDER_REGISTRY, dict)
else None
)
env_vars = getattr(anthropic, "api_key_env_vars", None)
if not env_vars:
return fallback
out = []
for _var in env_vars:
if not isinstance(_var, str):
continue
_normalized = _var.strip()
if _normalized and _normalized not in out:
out.append(_normalized)
return tuple(out) if out else fallback
except Exception:
return fallback
def _resolve_provider_alias(name: str) -> str:
"""Return the canonical provider slug for *name*.
@ -5687,8 +5718,9 @@ def get_available_models(*, prefer_cache: bool = False, force_refresh: bool = Fa
except Exception:
logger.debug("Failed to parse hermes env file")
all_env = {**env_keys}
_anthropic_env_vars = _get_anthropic_fallback_env_vars()
for k in (
"ANTHROPIC_API_KEY",
*_anthropic_env_vars,
"OPENAI_API_KEY",
"OPENROUTER_API_KEY",
"GOOGLE_API_KEY",
@ -5707,10 +5739,10 @@ def get_available_models(*, prefer_cache: bool = False, force_refresh: bool = Fa
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
):
val = _thread_local_env_value(k)
val = _thread_local_env_value(k).strip()
if val:
all_env[k] = val
if all_env.get("ANTHROPIC_API_KEY"):
if any(all_env.get(env_var) for env_var in _anthropic_env_vars):
detected_providers.add("anthropic")
if all_env.get("OPENAI_API_KEY"):
# hermes-agent registers its OPENAI_API_KEY/OPENAI_BASE_URL provider

View file

@ -0,0 +1,124 @@
"""Regression tests for #4770 - Anthropic OAuth env vars in fallback detection."""
import builtins
import sys
import api.config as config
def _force_env_fallback(monkeypatch):
"""Force get_available_models() into the env-based fallback branch."""
real_import = builtins.__import__
monkeypatch.delitem(sys.modules, "hermes_cli.auth", raising=False)
monkeypatch.delitem(sys.modules, "hermes_cli", raising=False)
def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
if name in ("hermes_cli.models", "hermes_cli.auth"):
raise ImportError(name)
return real_import(name, globals, locals, fromlist, level)
monkeypatch.setattr(builtins, "__import__", fake_import)
def _clear_provider_env(monkeypatch):
for key in (
"ANTHROPIC_API_KEY",
"ANTHROPIC_TOKEN",
"CLAUDE_CODE_OAUTH_TOKEN",
"OPENAI_API_KEY",
"OPENROUTER_API_KEY",
"GOOGLE_API_KEY",
"GEMINI_API_KEY",
"GLM_API_KEY",
"KIMI_API_KEY",
"DEEPSEEK_API_KEY",
"XIAOMI_API_KEY",
"OPENCODE_ZEN_API_KEY",
"OPENCODE_GO_API_KEY",
"OPENCODE_API_KEY",
"MINIMAX_API_KEY",
"MINIMAX_CN_API_KEY",
"XAI_API_KEY",
"MISTRAL_API_KEY",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"LM_API_KEY",
"LM_BASE_URL",
):
monkeypatch.delenv(key, raising=False)
def _run_available_models_with_cfg(monkeypatch, tmp_path, cfg):
old_cfg = dict(config.cfg)
old_mtime = config._cfg_mtime
old_models_cache_path = config._models_cache_path
old_config_path_getter = config._get_config_path
monkeypatch.setattr(config, "_models_cache_path", tmp_path / "models_cache.json")
monkeypatch.setattr(config, "_get_config_path", lambda: tmp_path / "missing-config.yaml")
monkeypatch.setattr("api.profiles.get_active_hermes_home", lambda: tmp_path, raising=False)
config.cfg.clear()
config.cfg.update(cfg)
config._cfg_mtime = 0.0
config.invalidate_models_cache()
try:
return config.get_available_models()
finally:
config.cfg.clear()
config.cfg.update(old_cfg)
config._cfg_mtime = old_mtime
monkeypatch.setattr(config, "_models_cache_path", old_models_cache_path)
monkeypatch.setattr(config, "_get_config_path", old_config_path_getter)
config.invalidate_models_cache()
def _provider_groups(result):
return {group["provider_id"]: group for group in result["groups"]}
def test_anthropic_token_env_var_surfaces_anthropic_models(monkeypatch, tmp_path):
"""ANTHROPIC_TOKEN should detect the Anthropic provider through fallback."""
_clear_provider_env(monkeypatch)
_force_env_fallback(monkeypatch)
monkeypatch.setenv("ANTHROPIC_TOKEN", "token-only-value")
result = _run_available_models_with_cfg(monkeypatch, tmp_path, {"model": {}})
groups = _provider_groups(result)
assert "anthropic" in groups, (
"ANTHROPIC_TOKEN should surface the Anthropic picker provider when the "
"hermes_cli path is unavailable."
)
assert groups["anthropic"]["provider"] == "Anthropic"
assert groups["anthropic"]["models"], "Anthropic fallback should include model entries"
def test_whitespace_only_anthropic_oauth_env_vars_do_not_surface_anthropic(monkeypatch, tmp_path):
"""A whitespace-only ANTHROPIC_TOKEN / CLAUDE_CODE_OAUTH_TOKEN must NOT
false-positive the Anthropic provider (env values are stripped before the
availability check)."""
for ws_var in ("ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"):
_clear_provider_env(monkeypatch)
_force_env_fallback(monkeypatch)
monkeypatch.setenv(ws_var, " \t ")
result = _run_available_models_with_cfg(monkeypatch, tmp_path, {"model": {}})
groups = _provider_groups(result)
assert "anthropic" not in groups, (
f"a whitespace-only {ws_var} must not surface the Anthropic provider"
)
def test_claude_code_oauth_token_env_var_surfaces_anthropic_models(monkeypatch, tmp_path):
"""CLAUDE_CODE_OAUTH_TOKEN should detect the Anthropic provider through fallback."""
_clear_provider_env(monkeypatch)
_force_env_fallback(monkeypatch)
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "oauth-token-only-value")
result = _run_available_models_with_cfg(monkeypatch, tmp_path, {"model": {}})
groups = _provider_groups(result)
assert "anthropic" in groups, (
"CLAUDE_CODE_OAUTH_TOKEN should surface the Anthropic picker provider when "
"the hermes_cli path is unavailable."
)
assert groups["anthropic"]["provider"] == "Anthropic"
assert groups["anthropic"]["models"], "Anthropic fallback should include model entries"