mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
test(model): simplify routable picker invariants
This commit is contained in:
parent
938c2622f6
commit
6503f36ab9
1 changed files with 25 additions and 109 deletions
|
|
@ -1,120 +1,36 @@
|
|||
"""Regression tests for picker filtering of unresolved providers.
|
||||
"""Picker rows must resolve to a runtime provider (#57503)."""
|
||||
|
||||
Bug — ``list_authenticated_providers()`` would emit ``/model`` picker rows for
|
||||
providers that ``PROVIDER_TO_MODELS_DEV`` knows about but that the runtime
|
||||
``resolve_provider()`` rejects as ``Unknown provider``. The reporter set
|
||||
``MISTRAL_API_KEY`` and saw a ``Mistral (72)`` row in the Telegram picker;
|
||||
selecting any model under it produced
|
||||
``Could not resolve credentials for provider 'Mistral': Unknown provider 'mistral'``
|
||||
(#57503).
|
||||
|
||||
The fix adds a resolve-gate in section 1 of ``list_authenticated_providers``:
|
||||
if a slug has no ``PROVIDER_REGISTRY`` entry, skip it. The picker now only
|
||||
shows providers that can actually be selected at runtime.
|
||||
|
||||
These tests pin that behavior in two directions:
|
||||
|
||||
1. With ``MISTRAL_API_KEY`` set, ``mistral`` MUST NOT appear in the picker
|
||||
(it has no ``PROVIDER_REGISTRY`` entry at the time of this fix).
|
||||
2. A provider that IS in ``PROVIDER_REGISTRY`` (e.g. ``deepseek``) and has
|
||||
its API key set MUST still appear, so the fix does not regress existing
|
||||
picker coverage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
def _reload():
|
||||
"""Force a fresh import of model_switch so module-level caches reset.
|
||||
|
||||
The picker caches ``PROVIDER_REGISTRY`` at import time. Each test must
|
||||
see a fresh module to avoid bleeding env vars between cases.
|
||||
"""
|
||||
for mod_name in [m for m in list(sys.modules) if m.startswith("hermes_cli.model_switch")]:
|
||||
sys.modules.pop(mod_name, None)
|
||||
return importlib.import_module("hermes_cli.model_switch")
|
||||
from hermes_cli.model_switch import list_authenticated_providers
|
||||
|
||||
|
||||
def _slug(rows):
|
||||
"""Return the set of provider slugs from picker rows."""
|
||||
return {row.get("slug") for row in rows}
|
||||
def _rows_with_env(monkeypatch, env_name: str, provider: str) -> list[dict]:
|
||||
monkeypatch.setenv(env_name, "test-key")
|
||||
with (
|
||||
patch(
|
||||
"agent.models_dev.fetch_models_dev",
|
||||
return_value={provider: {"env": [env_name], "name": provider.title()}},
|
||||
),
|
||||
patch(
|
||||
"agent.models_dev.PROVIDER_TO_MODELS_DEV",
|
||||
{provider: provider},
|
||||
),
|
||||
patch("hermes_cli.models.cached_provider_model_ids", return_value=["model-a"]),
|
||||
patch("hermes_cli.providers.HERMES_OVERLAYS", {}),
|
||||
):
|
||||
return list_authenticated_providers(max_models=5)
|
||||
|
||||
|
||||
def test_mistral_filtered_when_unregistered_but_api_key_set(monkeypatch):
|
||||
"""#57503 — MISTRAL_API_KEY set, mistral has no PROVIDER_REGISTRY entry.
|
||||
def test_models_dev_only_provider_is_not_selectable(monkeypatch):
|
||||
rows = _rows_with_env(monkeypatch, "MISTRAL_API_KEY", "mistral")
|
||||
|
||||
Picker must NOT emit a 'mistral' row. Users with a working
|
||||
custom_providers entry for Mistral no longer see the duplicate
|
||||
broken-from-models.dev entry cluttering the list.
|
||||
"""
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "test-mistral-key-shouldnt-crash")
|
||||
|
||||
model_switch = _reload()
|
||||
rows = model_switch.list_authenticated_providers(max_models=5)
|
||||
|
||||
slugs = _slug(rows)
|
||||
assert "mistral" not in slugs, (
|
||||
f"mistral leaked into /model picker despite being unregistered: {sorted(slugs)}"
|
||||
)
|
||||
assert all(row["slug"] != "mistral" for row in rows)
|
||||
|
||||
|
||||
def test_resolveable_provider_still_appears(monkeypatch):
|
||||
"""Sanity — a registered provider with a key must still surface.
|
||||
def test_registered_provider_remains_selectable(monkeypatch):
|
||||
rows = _rows_with_env(monkeypatch, "DEEPSEEK_API_KEY", "deepseek")
|
||||
|
||||
Guards against the new resolve-gate being too broad (e.g. nuking the
|
||||
happy path of every other provider while filtering mistral).
|
||||
"""
|
||||
monkeypatch.setenv("DEEPSEEK_API_KEY", "test-deepseek-key-shouldnt-crash")
|
||||
|
||||
model_switch = _reload()
|
||||
rows = model_switch.list_authenticated_providers(max_models=5)
|
||||
|
||||
slugs = _slug(rows)
|
||||
assert "deepseek" in slugs, (
|
||||
"deepseek disappeared from /model picker — resolve-gate is too broad. "
|
||||
f"slugs seen: {sorted(slugs)}"
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_gate_skips_models_dev_only_provider_without_creds(monkeypatch):
|
||||
"""A models-dev provider with no key and no PROVIDER_REGISTRY entry has
|
||||
no way to surface in the picker — verify we never emit such a row.
|
||||
|
||||
Keeps the fix idempotent: the previous ``if not has_creds: continue``
|
||||
gate already handled the no-credential case; we add a second gate on
|
||||
top that catches the WITH-credentials-but-no-registry case.
|
||||
"""
|
||||
# No MISTRAL_API_KEY, no other mistral-shaped env vars set
|
||||
for k in ("MISTRAL_API_KEY", "MISTRAL_BASE_URL"):
|
||||
monkeypatch.delenv(k, raising=False)
|
||||
|
||||
model_switch = _reload()
|
||||
rows = model_switch.list_authenticated_providers(max_models=5)
|
||||
|
||||
slugs = _slug(rows)
|
||||
assert "mistral" not in slugs
|
||||
|
||||
|
||||
def test_picker_skips_pconfig_none_does_not_break_other_section1_providers(monkeypatch, tmp_path):
|
||||
"""Coverage guard — providers that share a models.dev id with their
|
||||
canonical Hermes slug (e.g. ``gemini``, ``xai``, ``cohere``) must keep
|
||||
appearing. These ARE in ``PROVIDER_REGISTRY`` so the new gate should
|
||||
not touch them.
|
||||
"""
|
||||
# Pick a registry-backed provider that exists in models.dev and is
|
||||
# unlikely to be subject to the env-var duck-typing the picker uses
|
||||
# for unnamespaced keys.
|
||||
monkeypatch.setenv("XAI_API_KEY", "test-xai-key-shouldnt-crash")
|
||||
|
||||
model_switch = _reload()
|
||||
rows = model_switch.list_authenticated_providers(max_models=5)
|
||||
|
||||
slugs = _slug(rows)
|
||||
assert "xai" in slugs, (
|
||||
"xai disappeared — the resolve-gate ended up filtering registry-backed "
|
||||
"providers too. slugs seen: " + repr(sorted(slugs))
|
||||
)
|
||||
row = next(row for row in rows if row["slug"] == "deepseek")
|
||||
assert row["models"] == ["model-a"]
|
||||
assert row["total_models"] == 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue