From b99e1e3bf65f56006a823e62de96a27e89a4f572 Mon Sep 17 00:00:00 2001 From: AIalliAI <285906080+AIalliAI@users.noreply.github.com> Date: Sat, 20 Jun 2026 06:17:11 +0000 Subject: [PATCH] fix(model): collapse kimi alias/canonical to one /model picker row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single Kimi credential surfaced two rows in the `/model` picker — the bare alias `kimi` (PROVIDER_TO_MODELS_DEV pass) and the canonical `kimi-coding` (CANONICAL_PROVIDERS cross-check, section 2b) — both backed by the same `kimi-for-coding` provider. `kimi`, `moonshot` and the canonical `kimi-coding` all map to one models.dev id (`kimi-for-coding`). The seen_mdev_ids guard collapses them to the first key in section 1, but that key is the bare alias, so 2b re-emits the canonical name as a second row. Emit the row under the canonical Hermes slug instead: resolve the alias via _PROVIDER_ALIASES (`kimi` -> `kimi-coding`) before appending, so 2b's seen_slugs check collapses the pair. This matches the picker's other alias rows (copilot, gemini) and the overlay slug-resolution contract, and keeps the surviving row resolvable to the real provider. A defensive seen_slugs guard prevents emitting a duplicate canonical row. Distinct providers keep their own row: `kimi-coding-cn` has its own KIMI_CN_API_KEY and is still emitted by section 2b. Regression tests assert the single-key case yields one `kimi-coding` row (fails on clean main, which shows both `kimi` and `kimi-coding`) and that the China endpoint is preserved. Fixes #49439 --- hermes_cli/model_switch.py | 25 ++++- .../hermes_cli/test_list_picker_providers.py | 100 ++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index a054fced7f8f8..268b9ecf15c1b 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1849,6 +1849,7 @@ def list_authenticated_providers( # --- 1. Check Hermes-mapped providers --- from hermes_cli.models import _AGGREGATOR_PROVIDERS as _AGG_PROVIDERS + from hermes_cli.models import _PROVIDER_ALIASES as _CANON_ALIASES from hermes_cli.providers import ALIASES as _PROVIDER_ALIAS_TABLE for hermes_id, mdev_id in PROVIDER_TO_MODELS_DEV.items(): # Skip vendor names that are merely aliases routing through an @@ -1940,14 +1941,34 @@ def list_authenticated_providers( else: top = model_ids[:max_models] if max_models is not None else model_ids - slug = hermes_id + # Emit under the CANONICAL Hermes slug, not the bare alias. A single + # credential can be reachable under several PROVIDER_TO_MODELS_DEV keys + # that all share one models.dev id (e.g. "kimi", "moonshot" and the + # canonical "kimi-coding" all map to "kimi-for-coding"). The + # seen_mdev_ids guard above already collapses them to the first key — + # but that first key is usually the bare alias ("kimi"), so emitting it + # verbatim leaves section 2b free to re-emit the canonical "kimi-coding" + # from CANONICAL_PROVIDERS: one key, two picker rows (#49439). Resolving + # to the canonical slug here lets 2b's seen_slugs check collapse the + # pair, matches the picker's other alias rows (copilot, gemini, …), and + # keeps the row resolvable to the real provider. + slug = _CANON_ALIASES.get(hermes_id.lower(), hermes_id) + if slug.lower() in seen_slugs: + # Canonical already emitted by an earlier alias in this pass; don't + # add a second row for the same provider. + seen_mdev_ids.add(mdev_id) + continue pinfo = _mdev_pinfo(mdev_id) display_name = pinfo.name if pinfo else mdev_id results.append({ "slug": slug, "name": display_name, - "is_current": slug == current_provider or mdev_id == current_provider, + "is_current": ( + slug == current_provider + or hermes_id == current_provider + or mdev_id == current_provider + ), "is_user_defined": False, "models": top, "total_models": total, diff --git a/tests/hermes_cli/test_list_picker_providers.py b/tests/hermes_cli/test_list_picker_providers.py index a2042a96db59c..56d759ce4b7a3 100644 --- a/tests/hermes_cli/test_list_picker_providers.py +++ b/tests/hermes_cli/test_list_picker_providers.py @@ -299,3 +299,103 @@ def test_current_custom_endpoint_passthrough_marks_current_row(monkeypatch): assert row["slug"] == "custom:ollama" assert row["is_current"] is True assert row["models"] == ["glm-5.1", "qwen3"] + + +# --------------------------------------------------------------------------- +# list_authenticated_providers: alias/canonical de-dup for Kimi (#49439) +# --------------------------------------------------------------------------- +# +# A single Kimi credential used to surface TWO picker rows: the alias slug +# "kimi" (emitted by the PROVIDER_TO_MODELS_DEV pass) plus its canonical +# "kimi-coding" (re-emitted by the CANONICAL_PROVIDERS cross-check pass), +# both backed by the same kimi-for-coding models.dev provider. The picker +# must list each authenticated credential exactly once, under the CANONICAL +# slug ("kimi-coding") — matching list_authenticated_providers' other alias +# rows and the overlay slug-resolution contract (see +# test_overlay_slug_resolution.py). + + +def _stub_kimi_discovery(monkeypatch, *, canonical): + """Isolate list_authenticated_providers to the Kimi alias family. + + Restricts the models.dev map / catalog / overlays / canonical list to + just the Kimi entries and stubs the model-id fetch so discovery stays + offline and deterministic. ``canonical`` is the CANONICAL_PROVIDERS list + the 2b cross-check pass should iterate. + """ + import agent.models_dev as md + import hermes_cli.models as hm + + kimi_map = { + "kimi": "kimi-for-coding", + "kimi-coding": "kimi-for-coding", + "moonshot": "kimi-for-coding", + "kimi-coding-cn": "kimi-for-coding", + } + monkeypatch.setattr(md, "PROVIDER_TO_MODELS_DEV", kimi_map) + monkeypatch.setattr( + md, "fetch_models_dev", + lambda *a, **k: { + "kimi-for-coding": {"name": "Kimi For Coding", "env": ["KIMI_API_KEY"]}, + }, + ) + + class _PInfo: + name = "Kimi For Coding" + + monkeypatch.setattr(md, "get_provider_info", lambda _pid: _PInfo()) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + monkeypatch.setattr(hm, "CANONICAL_PROVIDERS", canonical) + monkeypatch.setattr(hm, "cached_provider_model_ids", + lambda *a, **k: ["kimi-k2.6", "kimi-k2.5"]) + monkeypatch.setattr(hm, "clear_provider_models_cache", lambda *a, **k: None) + + +def test_single_kimi_credential_yields_one_canonical_row(monkeypatch): + """One Kimi key yields a single row under the canonical 'kimi-coding' slug.""" + import hermes_cli.models as hm + + _stub_kimi_discovery( + monkeypatch, + canonical=[hm.ProviderEntry("kimi-coding", "Kimi / Kimi Coding Plan", "desc")], + ) + monkeypatch.setenv("KIMI_API_KEY", "sk-test-kimi") + + rows = model_switch.list_authenticated_providers(max_models=10) + slugs = [r["slug"] for r in rows] + + # Exactly one Kimi / kimi-for-coding-backed row, under the canonical slug — + # not both the alias ("kimi") and its canonical ("kimi-coding"). + kimi_rows = [s for s in slugs if s in {"kimi", "kimi-coding"}] + assert kimi_rows == ["kimi-coding"], ( + f"expected a single canonical Kimi row, got: {slugs}" + ) + assert slugs.count("kimi-coding") == 1 + assert "kimi" not in slugs + + +def test_distinct_kimi_china_credential_still_listed(monkeypatch): + """A separate China (kimi-coding-cn) credential remains its own row. + + Negative-control guard: the de-dup must collapse only the alias/canonical + pair that share a credential, not legitimately distinct providers. + """ + import hermes_cli.models as hm + + _stub_kimi_discovery( + monkeypatch, + canonical=[ + hm.ProviderEntry("kimi-coding", "Kimi / Kimi Coding Plan", "desc"), + hm.ProviderEntry("kimi-coding-cn", "Kimi / Moonshot (China)", "desc"), + ], + ) + monkeypatch.setenv("KIMI_API_KEY", "sk-test-kimi") + monkeypatch.setenv("KIMI_CN_API_KEY", "sk-test-kimi-cn") + + rows = model_switch.list_authenticated_providers(max_models=10) + slugs = [r["slug"] for r in rows] + + assert "kimi-coding" in slugs # canonical global row + assert slugs.count("kimi-coding") == 1 + assert "kimi" not in slugs # alias collapsed into the canonical row + assert "kimi-coding-cn" in slugs # distinct China endpoint preserved