# NousResearch/hermes-agent · Duplicate model picker rows for Kimi provider

## Open now

- Nothing open on GitHub.

Multiple issues and fixes address duplicate or ambiguous Kimi entries appearing in the TUI/CLI model picker, caused by alias and canonical slugs mapping to the same provider identity.

## TL;DR

- 7 member(s): 0 open issue(s), 3 closed issue(s), 0 open PR(s), 2 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #12306. Verify it addresses your case.
- **Recommended:** read PR #12306 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.22`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #10526: Bug: Duplicate "Kimi For Coding" entries in TUI model picker
- #49439: Bug: /models picker shows duplicate Kimi sections (kimi + kimi-coding)
- #56839: Duplicate Kimi provider rows in /model picker when multiple Hermes slugs map to the same models.dev ID

## Prior & competing PRs

- **#12306: fix(tui): disambiguate /model picker rows when provider display names collide**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `ui-tui/src/__tests__/providers.test.ts`, `ui-tui/src/components/modelPicker.tsx`, `ui-tui/src/domain/providers.ts`
  - fix hunks:
  - `ui-tui/src/__tests__/providers.test.ts`

    ```diff
    @@ -0,0 +1,62 @@
    +import { describe, expect, it } from 'vitest'
    +
    +import { providerDisplayNames } from '../domain/providers.js'
    +
    +describe('providerDisplayNames', () => {
    +  it('returns bare names when all are unique', () => {
    +    expect(providerDisplayNames([{ name: 'Anthropic', slug: 'anthropic' }, { name: 'OpenAI', slug: 'openai' }])).toEqual(
    +      ['Anthropic', 'OpenAI']
    +    )
    +  })
    +
    +  it('appends slug to every collision so the disambiguation is symmetric', () => {
    +    expect(
    +      providerDisplayNames([
    +        { name: 'Kimi For Coding', slug: 'kimi-coding' },
    +        { name: 'Kimi For Coding', slug: 'kimi-coding-cn' }
    +      ])
    +    ).toEqual(['Kimi For Coding (kimi-coding)', 'Kimi For Coding (kimi-coding-cn)'])
    +  })
    +
    +  it('only disambiguates the colliding group', () => {
    +    expect(
    +      providerDisplayNames([
    +        { name: 'Anthropic', slug: 'anthropic' },
    +        { name: 'Foo', slug: 'foo-a' },
    +        { name: 'Foo', slug: 'foo-b' }
    +      ])
    +    ).toEqual(['Anthropic', 'Foo (foo-a)', 'Foo (foo-b)'])
    +  })
    +
    +  it('falls back to plain name if slug is empty', () => {
    +    expect(
    +      providerDisplayNames([
    +        { name: 'Foo', slug: '' },
    +        { name: 'Foo', slug: '' }
    +      ])
    +    ).toEqual(['Foo', 'Foo'])
    +  })
    +
    +  it('skips disambiguation when slug equals name', () => {
    +    expect(
    +      providerDisplayNames([
    +        { name: 'foo', slug: 'foo' },
    +        { name: 'foo', slug: 'foo' }
    +      ])
    +    ).toEqual(['foo', 'foo'])
    +  })
    +
    +  it('handles empty input', () => {
    +    expect(providerDisplayNames([])).toEqual([])
    +  })
    +
    +  it('preserves order', () => {
    +    const input = [
    +      { name: 'Z', slug: 'z' },
    +      { name: 'A', slug: 'a1' },
    +      { name: 'A', slug: 'a2' }
    +    ]
    +
    +    expect(providerDisplayNames(input)).toEqual(['Z', 'A (a1)', 'A (a2)'])
    +  })
    +})
    ```
  - `ui-tui/src/components/modelPicker.tsx`

    ```diff
    @@ -1,6 +1,7 @@
     import { Box, Text, useInput } from '@hermes/ink'
    -import { useEffect, useState } from 'react'
    +import { useEffect, useMemo, useState } from 'react'
     
    +import { providerDisplayNames } from '../domain/providers.js'
     import type { GatewayClient } from '../gatewayClient.js'
     import type { ModelOptionProvider, ModelOptionsResponse } from '../gatewayTypes.js'
     import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
    ```

    ```diff
    @@ -59,6 +60,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
     
       const provider = providers[providerIdx]
       const models = provider?.models ?? []
    +  const names = useMemo(() => providerDisplayNames(providers), [providers])
     
       useInput((ch, key) => {
         if (key.escape) {
    ```

    ```diff
    @@ -160,7 +162,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
     
       if (stage === 'provider') {
         const rows = providers.map(
    -      p => `${p.is_current ? '*' : ' '} ${p.name} · ${p.total_models ?? p.models?.length ?? 0} models`
    +      (p, i) => `${p.is_current ? '*' : ' '} ${names[i]} · ${p.total_models ?? p.models?.length ?? 0} models`
         )
     
         const { items, off } = visibleItems(rows, providerIdx)
    ```

    ```diff
    @@ -201,7 +203,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
             Select Model
           </Text>
     
    -      <Text color={t.color.dim}>{provider?.name || '(unknown provider)'}</Text>
    +      <Text color={t.color.dim}>{names[providerIdx] || '(unknown provider)'}</Text>
           {!models.length ? <Text color={t.color.dim}>no models listed for this provider</Text> : null}
           {provider?.warning ? <Text color={t.color.label}>warning: {provider.warning}</Text> : null}
           {off > 0 && <Text color={t.color.dim}> ↑ {off} more</Text>}
    ```
  - `ui-tui/src/domain/providers.ts`

    ```diff
    @@ -0,0 +1,17 @@
    +export const providerDisplayNames = (providers: readonly { name: string; slug: string }[]): string[] => {
    +  const counts = new Map<string, number>()
    +
    +  for (const p of providers) {
    +    counts.set(p.name, (counts.get(p.name) ?? 0) + 1)
    +  }
    +
    +  return providers.map(p => {
    +    const dup = (counts.get(p.name) ?? 0) > 1
    +
    +    if (!dup || !p.slug || p.slug === p.name) {
    +      return p.name
    +    }
    +
    +    return `${p.name} (${p.slug})`
    +  })
    +}
    ```
- **#68141: fix(picker): dedupe Kimi alias/canonical rows + split distinct CN profile (salvage of #49455, #53100, #53392)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `contributors/emails/[redacted:email]`, `hermes_cli/model_switch.py`, `hermes_cli/providers.py`, `tests/hermes_cli/test_kimi_cn_provider_listing.py`, `tests/hermes_cli/test_list_picker_providers.py`
  - fix hunks:
  - `contributors/emails/[redacted:email]`

    ```diff
    @@ -0,0 +1 @@
    +Almurat123
    ```
  - `hermes_cli/model_switch.py`

    ```diff
    @@ -1720,7 +1720,6 @@ def list_authenticated_providers(
     
         results: List[dict] = []
         seen_slugs: set = set()  # lowercase-normalized to catch case variants (#9545)
    -    seen_mdev_ids: set = set()  # prevent duplicate entries for aliases (e.g. kimi-coding + kimi-coding-cn)
         _current_provider_norm = str(current_provider or "").strip().lower()
         _current_base_url_norm = str(current_base_url or "").strip().rstrip("/").lower()
     
    ```

    ```diff
    @@ -1849,6 +1848,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
     
         # --- 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
    ```

    ```diff
    @@ -1866,10 +1866,30 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
                 and _alias_target in _AGG_PROVIDERS
             ):
                 continue
    -        # Skip aliases that map to the same models.dev provider (e.g.
    -        # kimi-coding and kimi-coding-cn both → kimi-for-coding).
    -        # The first one with valid credentials wins (#10526).
    -        if mdev_id in seen_mdev_ids:
    +        # Resolve the canonical provider profile name.  Skip hermes_ids
    +        # that are mere aliases resolving to a different canonical profile
    +        # (e.g. "kimi" and "moonshot" both → "kimi-coding").  Only process
    +        # entries whose hermes_id matches the canonical profile name so
    +        # distinct profiles (e.g. kimi-coding, kimi-coding-cn) each get
    +        # their own picker row.
    +        _canonical = hermes_id
    +        try:
    +            from providers import get_provider_profile as _gpp
    +            _prof = _gpp(hermes_id)
    +            if _prof is not None:
    +                _canonical = _prof.name
    +        except Exception:
    +            pass
    +        if _canonical != hermes_id:
    +            continue
    +
    +        # Skip duplicates: another entry with the same slug was already
    +        # emitted (e.g. two PROVIDER_TO_MODELS_DEV entries routing to the
    +        # same hermes_id).  Distinct canonical profiles that share a
    +        # models.dev ID (e.g. kimi-coding and kimi-coding-cn → kimi-for-coding)
    +        # are both allowed through since they have different slugs.
    +        slug = hermes_id
    +        if slug.lower() in seen_slugs:
                 continue
             if hermes_id.lower() in _excluded or mdev_id.lower() in _excluded:
                 continue
    ```

    ```diff
    @@ -1940,21 +1960,23 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
             else:
                 top = model_ids[:max_models] if max_models is not None else model_ids
     
    -        slug = hermes_id
             pinfo = _mdev_pinfo(mdev_id)
    -        display_name = pinfo.name if pinfo else mdev_id
    +        display_name = pconfig.name if pconfig and pconfig.name else (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,
                 "source": "built-in",
             })
             seen_slugs.add(slug.lower())
    -        seen_mdev_ids.add(mdev_id)
             _record_builtin_endpoint(slug)
     
         # --- 2. Check Hermes-only providers (nous, openai-codex, copilot, opencode-go) ---
    ```
  - `hermes_cli/providers.py`

    ```diff
    @@ -765,6 +765,36 @@ def resolve_provider_full(
             if user_pdef is not None:
                 return user_pdef
     
    +    # 0.5 Exact Hermes provider IDs must win over LOSSY alias collapsing.
    +    # Example: kimi-coding-cn should stay distinct from kimi-coding instead of
    +    # normalizing through the shared models.dev alias "kimi-for-coding".
    +    # A collapse is lossy only when MULTIPLE distinct registry providers
    +    # normalize to the same canonical name — resolving through the alias
    +    # would then lose which one the caller meant. Single-entry rewrites
    +    # (e.g. "copilot" → "github-copilot") are correct routing and must keep
    +    # resolving through the built-in chain below so overlay transports apply.
    +    if canonical != raw:
    +        try:
    +            from hermes_cli.auth import PROVIDER_REGISTRY as _AUTH_PROVIDER_REGISTRY
    +            _pcfg = _AUTH_PROVIDER_REGISTRY.get(raw)
    +            if _pcfg is not None:
    +                _collapsed_siblings = [
    +                    _rid
    +                    for _rid in _AUTH_PROVIDER_REGISTRY
    +                    if normalize_provider(_rid) == canonical
    +                ]
    +                if len(_collapsed_siblings) > 1:
    +                    return ProviderDef(
    +                        id=_pcfg.id,
    +                        name=_pcfg.name,
    +                        transport="openai_chat",
    +                        api_key_env_vars=[redacted:secret-assignment] or ()),
    +                        base_url=_pcfg.inference_base_url or "",
    +                        source="hermes-auth-registry",
    +                    )
    +        except Exception:
    +            pass
    +
         # 1. Built-in (models.dev + overlays)
         pdef = get_provider(canonical)
         if pdef is not None:
    ```
  - `tests/hermes_cli/test_kimi_cn_provider_listing.py`

    ```diff
    @@ -0,0 +1,176 @@
    +"""Test that kimi-coding and kimi-coding-cn both appear in the /model picker.
    +
    +Both providers share the same models.dev ID (kimi-for-coding) but are distinct
    +profiles with different API keys, base URLs, and endpoints.  The /model picker
    +must show both so users can pick the right endpoint for their key type.
    +
    +Regression: the original ``seen_mdev_ids`` dedup by mdev_id alone would skip
    +kimi-coding-cn after kimi-coding was emitted because both map to
    +``kimi-for-coding`` (#10526).  The fix deduplicates by
    +``(mdev_id, canonical_profile_name)`` instead, allowing distinct profiles
    +through.
    +"""
    +
    +import os
    +from unittest.mock import patch
    +
    +from hermes_cli.model_switch import (
    +    list_authenticated_providers,
    +    parse_model_flags,
    +    switch_model,
    +)
    +from hermes_cli.providers import resolve_provider_full
    +
    +
    +# -- Only KIMI_CN_API_KEY set ------------------------------------------------
    +
    +
    [redacted:email](os.environ, {"KIMI_CN_API_KEY": "sk-cn-fake"}, clear=False)
    +def test_kimi_cn_appears_when_only_cn_key_set():
    +    """kimi-coding-cn should appear when only KIMI_CN_API_KEY is set."""
    +    providers = list_authenticated_providers(current_provider="kimi-coding-cn")
    +
    +    # kimi-coding-cn must be listed (it has credentials)
    +    cn = next((p for p in providers if p["slug"] == "kimi-coding-cn"), None)
    +    assert cn is not None, (
    +        "kimi-coding-cn should appear when KIMI_CN_API_KEY is set"
    +    )
    +    assert cn["is_current"] is True
    +    assert cn["total_models"] > 0
    +
    +    # kimi-coding must NOT appear (no KIMI_API_KEY)
    +    intl = next((p for p in providers if p["slug"] == "kimi-coding"), None)
    +    assert intl is None, (
    +        "kimi-coding should NOT appear when only KIMI_CN_API_KEY is set"
    +    )
    +
    +
    +# -- Only KIMI_API_KEY set ---------------------------------------------------
    +
    +
    [redacted:email](os.environ, {"KIMI_API_KEY": "sk-intl-fake"}, clear=False)
    +def test_kimi_intl_appears_when_only_intl_key_set():
    +    """kimi-coding (international) should appear when only KIMI_API_KEY is set."""
    +    providers = list_authenticated_providers(current_provider="kimi-coding")
    +
    +    intl = next((p for p in providers if p["slug"] == "kimi-coding"), None)
    +    assert intl is not None, (
    +        "kimi-coding should appear when KIMI_API_KEY is set"
    +    )
    +    assert intl["is_current"] is True
    +
    +    # kimi-coding-cn must NOT appear (no KIMI_CN_API_KEY)
    +    cn = next((p for p in providers if p["slug"] == "kimi-coding-cn"), None)
    +    assert cn is None, (
    +        "kimi-coding-cn should NOT appear when only KIMI_API_KEY is set"
    +    )
    +
    +
    +# -- Both keys set -----------------------------------------------------------
    +
    [redacted:email](os.environ, {
    +    "KIMI_API_KEY": "sk-intl-fake",
    +    "KIMI_CN_API_KEY": "sk-cn-fake",
    +}, clear=False)
    +def test_both_kimi_providers_appear_when_both_keys_set():
    +    """Both kimi-coding and kimi-coding-cn should appear when both keys set.
    +
    +    They are distinct profiles with different env vars and endpoints.  The
    +    existing aliases (kimi, moonshot → kimi-coding; kimi-cn, moonshot-cn →
    +    kimi-coding-cn) must NOT create additional rows.
    +    """
    +    providers = list_authenticated_providers(current_provider="kimi-coding")
    +
    +    # Both profile slugs must appear
    +    intl = next((p for p in providers if p["slug"] == "kimi-coding"), None)
    +    assert intl is not None, "kimi-coding should appear when KIMI_API_KEY is set"
    +    assert intl["is_current"] is True
    +
    +    cn = next((p for p in providers if p["slug"] == "kimi-coding-cn"), None)
    +    assert cn is not None, (
    +        "kimi-coding-cn should appear when KIMI_CN_API_KEY is set"
    +    )
    +    assert cn["is_current"] is False  # `current_provider` is kimi-coding
    +
    +    # Exactly 2 Kimi entries — no duplicates for aliases (kimi, moonshot,
    +    # moonshot-cn, kimi-cn)
    +    kimi_slugs = [p["slug"] for p in providers if "kimi" in p["slug"] or "moonshot" in p["slug"]]
    +    assert len(kimi_slugs) == 2, (
    +        f"Expected exactly 2 Kimi entries (kimi-coding, kimi-coding-cn), "
    +        f"got {kimi_slugs}"
    +    )
    +
    +
    +# -- Both aliases deduped correctly ------------------------------------------
    +
    [redacted:email](os.environ, {
    +    "KIMI_API_KEY": "sk-intl-fake",
    +    "KIMI_CN_API_KEY": "sk-cn-fake",
    +}, clear=False)
    +def test_kimi_aliases_not_listed_separately():
    +    """Alias hermes_ids (kimi, moonshot) must NOT create phantom picker rows.
    +
    +    They resolve to the same canonical profile (kimi-coding) and should be
    +    deduped.  Only the canonical slug (kimi-coding) should appear.
    +    """
    +    providers = list_authenticated_providers(current_provider="kimi-coding-cn")
    +
    +    slugs = {p["slug"] for p in providers}
    +    # These alias slugs must NOT appear
    +    for bad_slug in ("kimi", "moonshot", "moonshot-cn", "kimi-cn"):
    +        assert bad_slug not in slugs, (
    +            f"Alias slug '{bad_slug}' must not appear in picker (resolved to "
    +            f"canonical profile)"
    +        )
    +
    +
    [redacted:email](os.environ, {
    +    "KIMI_API_KEY": "sk-intl-fake",
    +    "KIMI_CN_API_KEY": "sk-cn-fake",
    +}, clear=False)
    +def test_resolve_provider_full_preserves_kimi_cn_provider_identity():
    +    """Explicit kimi-coding-cn must not collapse to shared models.dev alias.
    +
    +    Regression: resolve_provider_full('kimi-coding-cn') used normalize_provider(),
    +    which mapped both kimi-coding and kimi-coding-cn to the models.dev alias
    +    'kimi-for-coding'. That silently rewired CN users to the international
    +    endpoint and KIMI_API_KEY.
    +    """
    +    pdef = resolve_provider_full("kimi-coding-cn", None, None)
    +    assert pdef is not None
    +    assert pdef.id == "kimi-coding-cn"
    +    assert pdef.base_url == "https://api.moonshot.cn/v1"
    +    assert pdef.api_key_env_vars == ("KIMI_CN_API_KEY",)
    +
    +
    [redacted:email](os.environ, {
    +    "KIMI_API_KEY": "sk-intl-fake",
    +    "KIMI_CN_API_KEY": "sk-cn-fake",
    +}, clear=False)
    +def test_switch_model_with_explicit_kimi_cn_provider_stays_on_cn_endpoint():
    +    """/model ... --provider kimi-coding-cn must stay on moonshot.cn.
    +
    +    This hits the real switch path used by gateway /model: parse flags first,
    +    then call switch_model() with explicit_provider. The result must not rewrite
    +    the target provider/base_url back to the international Kimi endpoint.
    +    """
    +    model_input, explicit_provider, *_ = parse_model_flags(
    +        "kimi-k2.6 —provider kimi-coding-cn"
    +    )
    +    result = switch_model(
    +        raw_input=model_input,
    +        current_provider="deepseek",
    +        current_model="deepseek-v4-flash",
    +        current_base_url="https://api.deepseek.com/v1",
    +        current_api_key="***",
    +        is_global=False,
    +        explicit_provider=explicit_provider,
    +        user_providers={},
    +        custom_providers=None,
    +    )
    +
    +    assert result.success is True
    +    assert result.target_provider == "kimi-coding-cn"
    +    assert result.new_model == "kimi-k2.6"
    +    assert result.base_url == "https://api.moonshot.cn/v1"
    +    assert result.api_key == "sk-cn-fake"
    ```
  - `tests/hermes_cli/test_list_picker_providers.py`

    ```diff
    @@ -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
    ```
- **#10634: fix(tui): disambiguate duplicate display names in model picker**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#10706`
- **#49455: fix(model): collapse kimi alias/canonical to one /model picker row**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#68141`

## Fix status

- Merged to main in PR #12306 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-20`.
- Subsystem: `hermes_cli`
