fix(model): probe no-key custom provider catalogs

This commit is contained in:
luyifan 2026-06-30 04:29:02 +08:00 committed by kshitij
parent 46cf87be16
commit 5f00f36ba9
2 changed files with 53 additions and 6 deletions

View file

@ -2213,6 +2213,7 @@ def list_authenticated_providers(
"api_url": api_url,
"api_key": api_key,
"models": [],
"has_explicit_models": False,
"discover_models": discover,
"extra_headers": entry_extra_headers,
}
@ -2238,6 +2239,7 @@ def list_authenticated_providers(
for model_id in _declared_model_ids(entry.get("models", {})):
if model_id not in groups[group_key]["models"]:
groups[group_key]["models"].append(model_id)
groups[group_key]["has_explicit_models"] = True
_section4_emitted_slugs: set = set()
_current_base_url_group_count = sum(
@ -2298,11 +2300,13 @@ def list_authenticated_providers(
# the (possibly partial) ``models:`` subset configured for
# context-length overrides with the full live catalog.
# This is the Bifrost / aggregator-gateway case.
# - Without an api_key but with an explicit ``models:`` list
# (or top-level ``model:``), the user is narrowing a public
# endpoint to a specific subset (e.g. ollama.com /v1/models
# returns 35 models but the user only wants 4). Preserve the
# explicit list and skip live discovery.
# - Without an api_key but with an explicit ``models:`` list,
# the user is narrowing a public endpoint to a specific subset
# (e.g. ollama.com /v1/models returns 35 models but the user
# only wants 4). Preserve the explicit list and skip live
# discovery. The singular ``model:`` field is only the current
# active selection and must not suppress discovery on local
# no-key endpoints.
# - Without an api_key AND no explicit models, fall through to
# live discovery so bare-endpoint custom providers (local
# llama.cpp / Ollama servers) still appear populated.
@ -2320,7 +2324,7 @@ def list_authenticated_providers(
should_probe = (
_can_probe_custom_provider(row_is_current=_grp_is_current)
and bool(api_url)
and (bool(api_key) or not grp["models"])
and (bool(api_key) or not grp.get("has_explicit_models"))
and grp.get("discover_models", True)
)
if should_probe:

View file

@ -5,11 +5,18 @@ shared slash-command pipeline (`/model` in CLI/gateway/Telegram) historically
only looked at `providers:`.
"""
import pytest
import hermes_cli.providers as providers_mod
from hermes_cli.model_switch import list_authenticated_providers, switch_model
from hermes_cli.providers import resolve_provider_full
@pytest.fixture(autouse=True)
def _no_live_custom_provider_model_fetch(monkeypatch):
monkeypatch.setattr("hermes_cli.models.fetch_api_models", lambda *args, **kwargs: [])
_MOCK_VALIDATION = {
"accepted": True,
"persist": True,
@ -329,6 +336,42 @@ def test_list_deduplicates_same_model_in_group(monkeypatch):
assert my_rows[0]["total_models"] == 2
def test_custom_provider_no_key_singular_model_still_probes_live_models(monkeypatch):
"""A singular ``model:`` is the active selection, not an explicit catalog.
No-key local endpoints such as Ollama and llama.cpp should still be probed
so /model matches the terminal ``hermes model`` flow.
"""
monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {})
monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {})
calls = []
def fake_fetch_api_models(api_key, base_url, **kwargs):
calls.append((api_key, base_url, kwargs))
return ["llama3", "mistral", "qwen3-coder"]
monkeypatch.setattr("hermes_cli.models.fetch_api_models", fake_fetch_api_models)
providers = list_authenticated_providers(
current_provider="openai-codex",
user_providers={},
custom_providers=[
{
"name": "Local Ollama",
"base_url": "http://localhost:11434/v1",
"model": "llama3",
}
],
max_models=50,
)
assert calls == [("", "http://localhost:11434/v1", {"headers": None})]
row = next(p for p in providers if p["name"] == "Local Ollama")
assert row["models"] == ["llama3", "mistral", "qwen3-coder"]
assert row["total_models"] == 3
def test_list_enumerates_dict_format_models_alongside_default(monkeypatch):
"""custom_providers entry with dict-format ``models:`` plus singular
``model:`` should surface the default and every dict key.