Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions hermes_cli/model_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2176,6 +2176,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
"api_url": api_url,
"api_key": api_key,
"models": [],
"has_explicit_models": False,
"discover_models": discover,
"extra_headers": entry_extra_headers,
}
Expand All @@ -2201,6 +2202,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
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_norm = str(current_base_url or "").strip().rstrip("/").lower()
Expand Down Expand Up @@ -2262,11 +2264,13 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
# 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.
Expand All @@ -2278,7 +2282,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
should_probe = (
probe_custom_providers
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:
Expand Down
43 changes: 43 additions & 0 deletions tests/hermes_cli/test_model_switch_custom_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
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,
Expand Down Expand Up @@ -263,6 +270,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.
Expand Down
Loading