diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index 5349bc3643a..92427967464 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -2253,6 +2253,24 @@ def list_authenticated_providers( seen_slugs.add(slug.lower()) _section4_emitted_slugs.add(slug.lower()) + # Surface a custom / uncurated model the user selected via the CLI. + # Each row's model list is its curated/live catalog, so a model the user set + # with `/model /` would otherwise be invisible in + # every picker — the main model picker AND the MoA reference/aggregator slot + # pickers, which read these same rows. Inject it at the front of the current + # provider's row (matched by slug) so it is selectable and shown. Done as a + # post-pass so it covers every provider section uniformly, regardless of + # which branch emitted the row. + if current_model: + for _row in results: + if not _row.get("is_current"): + continue + _models = _row.get("models") or [] + if current_model not in _models: + _row["models"] = [current_model, *_models] + _row["total_models"] = _row.get("total_models", len(_models)) + 1 + break + # Sort: current provider first, then by model count descending results.sort(key=lambda r: (not r["is_current"], -r["total_models"])) diff --git a/tests/hermes_cli/test_user_providers_model_switch.py b/tests/hermes_cli/test_user_providers_model_switch.py index a4f201a060b..7cff7e89692 100644 --- a/tests/hermes_cli/test_user_providers_model_switch.py +++ b/tests/hermes_cli/test_user_providers_model_switch.py @@ -1122,3 +1122,63 @@ def test_section3_skips_probe_when_no_key_but_explicit_models(monkeypatch): row = next(p for p in providers if p["slug"] == "public-subset") assert row["models"] == ["only-a", "only-b"] assert row["total_models"] == 2 + + +def test_current_custom_model_is_surfaced_in_builtin_provider_row(monkeypatch): + """A custom/uncurated model selected via the CLI must appear in its + provider's picker row. + + Regression: selecting `/model openrouter/` left the model + invisible in every picker (main model picker AND the MoA reference/aggregator + slot pickers, which read these rows), because the row only carried the + curated catalog. The current model is now injected at the front of the + current provider's list. + """ + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + monkeypatch.setenv("OPENROUTER_API_KEY", "sk-test") + # Pin a small curated catalog so the assertion is deterministic. + monkeypatch.setattr( + "hermes_cli.models.cached_provider_model_ids", + lambda slug, **kw: ["anthropic/claude-opus-4.8", "openai/gpt-5.5"] + if slug == "openrouter" + else [], + ) + + custom = "some-vendor/totally-custom-model-v9" + providers = list_authenticated_providers( + current_provider="openrouter", + current_model=custom, + user_providers={}, + custom_providers=[], + ) + + row = next(p for p in providers if p["slug"] == "openrouter") + assert custom in row["models"], row["models"] + assert row["models"][0] == custom # injected at the front + assert row["total_models"] == 3 + + +def test_current_custom_model_not_leaked_into_other_provider_rows(monkeypatch): + """The current model is only injected into the CURRENT provider's row, + never into other providers (which can't serve it).""" + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + monkeypatch.setenv("OPENROUTER_API_KEY", "sk-test") + monkeypatch.setenv("NOUS_API_KEY", "sk-test") + monkeypatch.setattr( + "hermes_cli.models.cached_provider_model_ids", + lambda slug, **kw: ["curated/one"], + ) + + custom = "some-vendor/totally-custom-model-v9" + providers = list_authenticated_providers( + current_provider="openrouter", + current_model=custom, + user_providers={}, + custom_providers=[], + ) + + for row in providers: + if row["slug"] != "openrouter" and not row.get("is_current"): + assert custom not in row.get("models", []), f"leaked into {row['slug']}"