feat(/model): merge models.dev entries for lesser-loved providers (#14221)
New and newer models from models.dev now surface automatically in
/model (both hermes model CLI and the gateway Telegram/Discord picker)
for a curated set of secondary providers — no Hermes release required
when the registry publishes a new model.
Primary user-visible fix: on OpenCode Go, typing '/model mimo-v2.5-pro'
no longer silently fuzzy-corrects to 'mimo-v2-pro'. The exact match
against the merged models.dev catalog wins.
Scope (opt-in frozenset _MODELS_DEV_PREFERRED in hermes_cli/models.py):
opencode-go, opencode-zen, deepseek, kilocode, fireworks, mistral,
togetherai, cohere, perplexity, groq, nvidia, huggingface, zai,
gemini, google.
Explicitly NOT merged:
- openrouter and nous (never): curated list is already a hand-picked
subset / Portal is source of truth.
- xai, xiaomi, minimax, minimax-cn, kimi-coding, kimi-coding-cn,
alibaba, qwen-oauth (per-project decision to keep curated-only).
- providers with dedicated live-endpoint paths (copilot, anthropic,
ai-gateway, ollama-cloud, custom, stepfun, openai-codex) — those
paths already handle freshness themselves.
Changes:
- hermes_cli/models.py: add _MODELS_DEV_PREFERRED + _merge_with_models_dev
helper. provider_model_ids() branches on the set at its curated-fallback
return. Merge is models.dev-first, curated-only extras appended,
case-insensitive dedup, graceful fallback when models.dev is offline.
- hermes_cli/model_switch.py: list_authenticated_providers() calls the
same merge in both its code paths (PROVIDER_TO_MODELS_DEV loop +
HERMES_OVERLAYS loop). Picker AND validation-fallback both see
fresh entries.
- tests/hermes_cli/test_models_dev_preferred_merge.py (new): 13 tests —
merge-helper unit tests (empty/raise/order/dedup), opencode-go/zen
behavior, openrouter+nous explicitly guarded from merge.
- tests/hermes_cli/test_opencode_go_in_model_list.py: converted from
snapshot-style assertion to a behavior-based floor check, so it
doesn't break when models.dev publishes additional opencode-go
entries.
Addresses a report from @pfanis via Telegram: newer Xiaomi variants
on OpenCode Go weren't appearing in the /model picker, and /model
was silently routing requests for new variants to older ones.
This commit is contained in:
@@ -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]
|
||||
|
||||
|
||||
@@ -1589,11 +1589,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":
|
||||
@@ -1659,7 +1732,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]]:
|
||||
|
||||
Reference in New Issue
Block a user