NousResearch/hermes-agent

Model list accuracy fixes for CLI and Telegram

Cluster 6756 · gateway. Data as of

Nothing open on GitHub. Fix merged in #14221 on 23 Apr 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #14221. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and pull requests address incorrect or incomplete model listings, including a missing models.dev fallback, merging additional provider entries, and ensuring the model picker shows the full catalog rather than a curated subset.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #14221 feat(/model): merge models.dev entries for lesser-loved providers

    merged to main

    files: hermes_cli/model_switch.py, hermes_cli/models.py, tests/hermes_cli/test_models_dev_preferred_merge.py, tests/hermes_cli/test_opencode_go_in_model_list.py

    Show the diff
    • hermes_cli/model_switch.py
      @@ -810,7 +810,10 @@ def list_authenticated_providers(
               get_provider_info as _mdev_pinfo,
           )
           from hermes_cli.auth import PROVIDER_REGISTRY
      -    from hermes_cli.models import OPENROUTER_MODELS, _PROVIDER_MODELS
      +    from hermes_cli.models import (
      +        OPENROUTER_MODELS, _PROVIDER_MODELS,
      +        _MODELS_DEV_PREFERRED, _merge_with_models_dev,
      +    )
       
           results: List[dict] = []
           seen_slugs: set = set()  # lowercase-normalized to catch case variants (#9545)
      @@ -856,8 +859,13 @@ def list_authenticated_providers(
               if not has_creds:
                   continue
       
      -        # Use curated list, falling back to models.dev if no curated list
      +        # Use curated list, falling back to models.dev if no curated list.
      +        # For preferred providers, merge models.dev entries into the curated
      +        # catalog so newly released models (e.g. mimo-v2.5-pro on opencode-go)
      +        # show up in the picker without requiring a Hermes release.
               model_ids = curated.get(hermes_id, [])
      +        if hermes_id in _MODELS_DEV_PREFERRED:
      +            model_ids = _merge_with_models_dev(hermes_id, model_ids)
               total = len(model_ids)
               top = model_ids[:max_models]
       
      @@ -961,6 +969,9 @@ def list_authenticated_providers(
       
               # Use curated list — look up by Hermes slug, fall back to overlay key
               model_ids = curated.get(hermes_slug, []) or curated.get(pid, [])
      +        # Merge with models.dev for preferred providers (same rationale as above).
      +        if hermes_slug in _MODELS_DEV_PREFERRED:
      +            model_ids = _merge_with_models_dev(hermes_slug, model_ids)
               total = len(model_ids)
               top = model_ids[:max_models]
       
    • hermes_cli/models.py
      @@ -1587,11 +1587,84 @@ def _resolve_copilot_catalog_api_key() -> str:
               return ""
       
       
      +# Providers where models.dev is treated as authoritative: curated static
      +# lists are kept only as an offline fallback and to capture custom additions
      +# the registry doesn't publish yet. Adding a provider here causes its
      +# curated list to be merged with fresh models.dev entries (fresh first, any
      +# curated-only names appended) for both the CLI and the gateway /model picker.
      +#
      +# DELIBERATELY EXCLUDED:
      +#   - "openrouter": curated list is already a hand-picked agentic subset of
      +#     OpenRouter's 400+ catalog. Blindly merging would dump everything.
      +#   - "nous": curated list and Portal /models endpoint are the source of
      +#     truth for the subscription tier.
      +# Also excluded: providers that already have dedicated live-endpoint
      +# branches below (copilot, anthropic, ai-gateway, ollama-cloud, custom,
      +# stepfun, openai-codex) — those paths handle freshness themselves.
      +_MODELS_DEV_PREFERRED: frozenset[str] = frozenset({
      +    "opencode-go",
      +    "opencode-zen",
      +    "deepseek",
      +    "kilocode",
      +    "fireworks",
      +    "mistral",
      +    "togetherai",
      +    "cohere",
      +    "perplexity",
      +    "groq",
      +    "nvidia",
      +    "huggingface",
      +    "zai",
      +    "gemini",
      +    "google",
      +})
      +
      +
      +def _merge_with_models_dev(provider: str, curated: list[str]) -> list[str]:
      +    """Merge curated list with fresh models.dev entries for a preferred provider.
      +
      +    Returns models.dev entries first (in models.dev order), then any
      +    curated-only entries appended. Preserves case for curated fallbacks
      +    (e.g. ``MiniMax-M2.7``) while trusting models.dev for newer variants.
      +
      +    If models.dev is unreachable or returns nothing, the curated list is
      +    returned unchanged — this is the offline/CI fallback path.
      +    """
      +    try:
      +        from agent.models_dev import list_agentic_models
      +        mdev = list_agentic_models(provider)
      +    except Exception:
      +        mdev = []
      +
      +    if not mdev:
      +        return list(curated)
      +
      +    # Case-insensitive dedup while preserving order and curated casing.
      +    seen_lower: set[str] = set()
      +    merged: list[str] = []
      +    for mid in mdev:
      +        key = str(mid).lower()
      +        if key in seen_lower:
      +            continue
      +        seen_lower.add(key)
      +        merged.append(mid)
      +    for mid in curated:
      +        key = str(mid).lower()
      +        if key in seen_lower:
      +            continue
      +        seen_lower.add(key)
      +        merged.append(mid)
      +    return merged
      +
      +
       def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False) -> list[str]:
           """Return the best known model catalog for a provider.
       
           Tries live API endpoints for providers that support them (Codex, Nous),
      -    falling back to static lists.
      +    falling back to static lists. For providers in ``_MODELS_DEV_PREFERRED``
      +    (opencode-go/zen, xiaomi, deepseek, smaller inference providers, etc.),
      +    models.dev entries are merged on top of curated so new models released
      +    on the platform appear in ``/model`` without a Hermes release.
           """
           normalized = normalize_provider(provider)
           if normalized == "openrouter":
      @@ -1657,7 +1730,10 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
                   live = fetch_api_models(api_key, base_url)
                   if live:
                       return live
      -    return list(_PROVIDER_MODELS.get(normalized, []))
      +    curated_static = list(_PROVIDER_MODELS.get(normalized, []))
      +    if normalized in _MODELS_DEV_PREFERRED:
      +        return _merge_with_models_dev(normalized, curated_static)
      +    return curated_static
       
       
       def _fetch_anthropic_models(timeout: float = 5.0) -> Optional[list[str]]:
    • tests/hermes_cli/test_models_dev_preferred_merge.py
      @@ -0,0 +1,124 @@
      +"""Tests for the models.dev-preferred merge behavior in provider_model_ids
      +and list_authenticated_providers.
      +
      +These guard the contract:
      +
      +  * For providers in ``_MODELS_DEV_PREFERRED`` (opencode-go, opencode-zen,
      +    xiaomi, deepseek, smaller inference providers), both the CLI model
      +    picker path (``provider_model_ids``) and the gateway ``/model`` picker
      +    path (``list_authenticated_providers``) merge fresh models.dev entries
      +    on top of the curated static list.
      +  * OpenRouter and Nous Portal are NEVER merged — they keep their curated
      +    (OpenRouter) or live-Portal (Nous) semantics.
      +  * If models.dev is unreachable (offline / CI), the curated list is the
      +    fallback — no crash, no empty list.
      +
      +Merging is what lets new models (e.g. ``mimo-v2.5-pro`` on opencode-go)
      +appear in ``/model`` without a Hermes release.
      +"""
      +
      +import os
      +from unittest.mock import patch
      +
      +import pytest
      +
      +from hermes_cli.models import (
      +    _MODELS_DEV_PREFERRED,
      +    _merge_with_models_dev,
      +    provider_model_ids,
      +)
      +
      +
      +class TestMergeHelper:
      +    def test_merge_empty_mdev_returns_curated(self):
      +        """When models.dev returns nothing, curated list is preserved verbatim."""
      +        with patch("agent.models_dev.list_agentic_models", return_value=[]):
      +            out = _merge_with_models_dev("opencode-go", ["mimo-v2-pro", "kimi-k2.6"])
      +        assert out == ["mimo-v2-pro", "kimi-k2.6"]
      +
      +    def test_merge_mdev_raises_returns_curated(self):
      +        """Offline / broken models.dev must not break the catalog path."""
      +        def boom(_provider):
      +            raise RuntimeError("network down")
      +
      +        with patch("agent.models_dev.list_agentic_models", side_effect=boom):
      +            out = _merge_with_models_dev("opencode-go", ["mimo-v2-pro"])
      +        assert out == ["mimo-v2-pro"]
      +
      +    def test_merge_mdev_first_then_curated_extras(self):
      +        """models.dev entries come first; curated-only entries are appended."""
      +        mdev = ["mimo-v2.5-pro", "mimo-v2-pro", "kimi-k2.6"]
      +        curated = ["kimi-k2.6", "kimi-k2.5", "mimo-v2-pro"]  # kimi-k2.5 is curated-only
      +        with patch("agent.models_dev.list_agentic_models", return_value=mdev):
      +            out = _merge_with_models_dev("opencode-go", curated)
      +        # models.dev entries first (in order), then curated-only entries
      +        assert out == ["mimo-v2.5-pro", "mimo-v2-pro", "kimi-k2.6", "kimi-k2.5"]
      +
      +    def test_merge_case_insensitive_dedup(self):
      +        """Dedup is case-insensitive but preserves the first occurrence's casing."""
      +        mdev = ["MiniMax-M2.7"]
      +        curated = ["minimax-m2.7", "minimax-m2.5"]
      +        with patch("agent.models_dev.list_agentic_models", return_value=mdev):
      +            out = _merge_with_models_dev("minimax", curated)
      +        # models.dev casing wins since it came first
      +        assert out == ["MiniMax-M2.7", "minimax-m2.5"]
      +
      +
      +class TestProviderModelIdsPreferred:
      +    def test_opencode_go_is_preferred(self):
      +        assert "opencode-go" in _MODELS_DEV_PREFERRED
      +
      +    def test_opencode_go_includes_fresh_models_dev_entries(self):
      +        """provider_model_ids('opencode-go') adds models.dev entries on top."""
      +        mdev = ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro", "kimi-k2.6"]
      +        with patch("agent.models_dev.list_agentic_models", return_value=mdev):
      +            out = provider_model_ids("opencode-go")
      +        # Fresh models must surface (this is exactly the reported bug fix:
      +        # mimo-v2.5-pro should be pickable on opencode-go).
      +        assert "mimo-v2.5-pro" in out
      +        assert "mimo-v2.5" in out
      +        # Curated entries are still present.
      +        assert "mimo-v2-pro" in out
      +        assert "kimi-k2.6" in out
      +
      +    def test_opencode_go_offline_falls_back_to_curated(self):
      +        """Offline models.dev → curated-only list, no crash."""
      +        with patch("agent.models_dev.list_agentic_models", return_value=[]):
      +            out = provider_model_ids("opencode-go")
      +        # Curated floor (see hermes_cli/models.py _PROVIDER_MODELS["opencode-go"])
      +        assert "mimo-v2-pro" in out
      +        assert "kimi-k2.6" in out
      +
      +    def test_opencode_zen_includes_fresh_models(self):
      +        """opencode-zen follows the same pattern as opencode-go."""
      +        assert "opencode-zen" in _MODELS_DEV_PREFERRED
      +        mdev = ["claude-opus-4-7", "kimi-k2.6", "glm-5.1"]
      +        with patch("agent.models_dev.list_agentic_models", return_value=mdev):
      +            out = provider_model_ids("opencode-zen")
      +        assert "claude-opus-4-7" in out
      +        assert "kimi-k2.6" in out
      +
      +
      +class TestOpenRouterAndNousUnchanged:
      +    """Per Teknium: openrouter and nous are NEVER merged with models.dev."""
      +
      +    def test_openrouter_not_in_preferred_set(self):
      +        assert "openrouter" not in _MODELS_DEV_PREFERRED
      +
      +    def test_nous_not_in_preferred_set(self):
      +        assert "nous" not in _MODELS_DEV_PREFERRED
      +
      +    def test_openrouter_does_not_call_merge(self):
      +        """openrouter takes its own live path — merge helper must NOT run."""
      +        with patch(
      +            "hermes_cli.models._merge_with_models_dev",
      +            side_effect=AssertionError("merge should not be called for openrouter"),
      +        ):
      +            # Even if model_ids() fails for some other reason, we just care
      +            # that the merge path isn't invoked.
      +            try:
      +                provider_model_ids("openrouter")
      +            except AssertionError:
      +                raise
      +            except Exception:
      +                pass  # model_ids() may fail in the hermetic test env — that's fine.
    • tests/hermes_cli/test_opencode_go_in_model_list.py
      @@ -6,16 +6,41 @@
       from hermes_cli.model_switch import list_authenticated_providers
       
       
      +# Minimum set of models that must be present for opencode-go no matter
      +# whether the picker sourced its list from curated-only or curated+models.dev.
      +# The curated list in hermes_cli/models.py defines the floor; models.dev only
      +# ever adds names on top of it via _merge_with_models_dev.
      +_OPENCODE_GO_REQUIRED = {
      +    "kimi-k2.6",
      +    "kimi-k2.5",
      +    "glm-5.1",
      +    "glm-5",
      +    "mimo-v2-pro",
      +    "mimo-v2-omni",
      +    "minimax-m2.7",
      +    "minimax-m2.5",
      +}
      +
      +
       @patch.dict(os.environ, {"OPENCODE_GO_API_KEY": "[redacted:secret-assignment]"}, clear=False)
       def test_opencode_go_appears_when_api_key_set():
           """opencode-go should appear in list_authenticated_providers when OPENCODE_GO_API_KEY is set."""
      -    providers = list_authenticated_providers(current_provider="openrouter")
      -    
      +    providers = list_authenticated_providers(current_provider="openrouter", max_models=50)
      +
           # Find opencode-go in results
           opencode_go = next((p for p in providers if p["slug"] == "opencode-go"), None)
      -    
      +
           assert opencode_go is not None, "opencode-go should appear when OPENCODE_GO_API_KEY is set"
      -    assert opencode_go["models"] == ["kimi-k2.6", "kimi-k2.5", "glm-5.1", "glm-5", "mimo-v2-pro", "mimo-v2-omni", "minimax-m2.7", "minimax-m2.5"]
      +    # Behavior check: the curated floor must be present. The list may also
      +    # include extra models.dev entries (e.g. mimo-v2.5-pro) when the registry
      +    # is reachable — that's the whole point of the models.dev-preferred merge
      +    # introduced for opencode-go, so don't pin to an exact list here.
      +    present = set(opencode_go["models"])
      +    missing = _OPENCODE_GO_REQUIRED - present
      +    assert not missing, (
      +        f"opencode-go picker should include the curated floor; missing: {sorted(missing)}. "
      +        f"Got: {opencode_go['models']}"
      +    )
           # opencode-go can appear as "built-in" (from PROVIDER_TO_MODELS_DEV when
           # models.dev is reachable) or "hermes" (from HERMES_OVERLAYS fallback when
           # the API is unavailable, e.g. in CI).
      @@ -26,10 +51,10 @@ def test_opencode_go_not_appears_when_no_creds():
           """opencode-go should NOT appear when no credentials are set."""
           # Ensure OPENCODE_GO_API_KEY is not set
           env_without_key = {k: v for k, v in os.environ.items() if k != "OPENCODE_GO_API_KEY"}
      -    
      +
           with patch.dict(os.environ, env_without_key, clear=True):
               providers = list_authenticated_providers(current_provider="openrouter")
      -        
      +
               # opencode-go should not be in results
               opencode_go = next((p for p in providers if p["slug"] == "opencode-go"), None)
               assert opencode_go is None, "opencode-go should not appear without credentials"

1 PR closed without merging

From the close comments: done elsewhere 1

  • #6789 fix(cli): implement missing models.dev fallback for provider model li… · done elsewhere → #6756, #14221
Closed issues · 1
  • Closed#6756[Bug]:Model list is incorrect in Telegram
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#14221
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
23 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.