diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 54d105b9de..c16ec76f2b 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -1436,7 +1436,9 @@ def _anthropic_catalog(normalized: str, force_refresh: bool) -> list[str]: live = _fetch_anthropic_models(base_url=cfg_base_url or None, api_key=cfg_api_key or None) curated = list(_PROVIDER_MODELS.get("anthropic", [])) if not live: - return curated + # A placeholder for the outage, not this account/proxy's catalog: the disk cache must + # never pin it over a same-credentials live row (#107391). + return CuratedFallbackModels(curated) # The live /v1/models dump lags newly-routed curated aliases (reachable before enumerated): # curated first, then live-only extras, so a fresh curated model never disappears. return live if cfg_base_url else _merge_unique(curated, live) diff --git a/tests/hermes_cli/test_model_cache_fallback_rows.py b/tests/hermes_cli/test_model_cache_fallback_rows.py index a989711fd8..7ad80493ef 100644 --- a/tests/hermes_cli/test_model_cache_fallback_rows.py +++ b/tests/hermes_cli/test_model_cache_fallback_rows.py @@ -123,3 +123,14 @@ def test_codex_cold_fallback_retries_after_short_ttl(monkeypatch, tmp_path): assert "gpt-6-astra" in mod.cached_provider_model_ids("openai-codex") assert "fallback" not in cache["openai-codex"] spawn.assert_not_called() + + +def test_anthropic_catalog_marks_the_curated_list_as_fallback_on_a_failed_live_fetch(): + # _anthropic_catalog used to `return curated` (a plain list) on a failed live fetch, so a + # transient outage on a proxy base_url cached the generic curated list over the account's/ + # proxy's real catalog for a full hour (same class of bug as #107391, just missed by 3fb8b3f6). + with patch.object(mod, "_get_model_config_dict", return_value={}), \ + patch.object(mod, "_fetch_anthropic_models", return_value=None): + rows = mod.provider_model_ids("anthropic") + assert isinstance(rows, mod.CuratedFallbackModels) + assert rows == list(mod._PROVIDER_MODELS["anthropic"])