2026-03-24 07:08:07 -07:00
|
|
|
"""Shared model-switching logic for CLI and gateway /model commands.
|
|
|
|
|
|
|
|
|
|
Both the CLI (cli.py) and gateway (gateway/run.py) /model handlers
|
|
|
|
|
share the same core pipeline:
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
parse flags -> alias resolution -> provider resolution ->
|
|
|
|
|
credential resolution -> normalize model name ->
|
|
|
|
|
metadata lookup -> build result
|
2026-03-24 07:08:07 -07:00
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
This module ties together the foundation layers:
|
|
|
|
|
|
|
|
|
|
- ``agent.models_dev`` -- models.dev catalog, ModelInfo, ProviderInfo
|
|
|
|
|
- ``hermes_cli.providers`` -- canonical provider identity + overlays
|
|
|
|
|
- ``hermes_cli.model_normalize`` -- per-provider name formatting
|
|
|
|
|
|
|
|
|
|
Provider switching uses the ``--provider`` flag exclusively.
|
|
|
|
|
No colon-based ``provider:model`` syntax — colons are reserved for
|
|
|
|
|
OpenRouter variant suffixes (``:free``, ``:extended``, ``:fast``).
|
2026-03-24 07:08:07 -07:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
import logging
|
fix(cli): narrow Nous Hermes non-agentic warning to actual hermes-3/-4 models
The startup warning that Nous Research Hermes 3 & 4 models are not agentic
fired on any model whose name contained "hermes" anywhere, via a plain
substring check. That false-positived on unrelated local Modelfiles such
as `hermes-brain:qwen3-14b-ctx16k` — a tool-capable Qwen3 wrapper that
happens to live under a custom "hermes" tag namespace — making the warning
noise for legitimate setups.
Replace the substring check with a narrow regex anchored on `^`, `/`, or
`:` boundaries that only matches the real Hermes-3 / Hermes-4 chat family
(e.g. `NousResearch/Hermes-3-Llama-3.1-70B`, `hermes-4-405b`,
`openrouter/hermes3:70b`). Consolidate into a single helper
`is_nous_hermes_non_agentic()` in `hermes_cli.model_switch` so the CLI
and the canonical check don't drift, and route the duplicate inline site
in `cli.HermesCLI._print_warnings()` through the helper.
Add a parametrized test covering positive matches (real Hermes-3/-4
names) and a broad set of negatives (custom Modelfiles, Qwen/Claude/GPT,
older Nous-Hermes-2 families, bare "hermes", empty string, and the
"brain-hermes-3-impostor" boundary case).
2026-04-13 06:12:41 +02:00
|
|
|
import re
|
refactor: codebase-wide lint cleanup — unused imports, dead code, and inefficient patterns (#5821)
Comprehensive cleanup across 80 files based on automated (ruff, pyflakes, vulture)
and manual analysis of the entire codebase.
Changes by category:
Unused imports removed (~95 across 55 files):
- Removed genuinely unused imports from all major subsystems
- agent/, hermes_cli/, tools/, gateway/, plugins/, cron/
- Includes imports in try/except blocks that were truly unused
(vs availability checks which were left alone)
Unused variables removed (~25):
- Removed dead variables: connected, inner, channels, last_exc,
source, new_server_names, verify, pconfig, default_terminal,
result, pending_handled, temperature, loop
- Dropped unused argparse subparser assignments in hermes_cli/main.py
(12 instances of add_parser() where result was never used)
Dead code removed:
- run_agent.py: Removed dead ternary (None if False else None) and
surrounding unreachable branch in identity fallback
- run_agent.py: Removed write-only attribute _last_reported_tool
- hermes_cli/providers.py: Removed dead @property decorator on
module-level function (decorator has no effect outside a class)
- gateway/run.py: Removed unused MCP config load before reconnect
- gateway/platforms/slack.py: Removed dead SessionSource construction
Undefined name bugs fixed (would cause NameError at runtime):
- batch_runner.py: Added missing logger = logging.getLogger(__name__)
- tools/environments/daytona.py: Added missing Dict and Path imports
Unnecessary global statements removed (14):
- tools/terminal_tool.py: 5 functions declared global for dicts
they only mutated via .pop()/[key]=value (no rebinding)
- tools/browser_tool.py: cleanup thread loop only reads flag
- tools/rl_training_tool.py: 4 functions only do dict mutations
- tools/mcp_oauth.py: only reads the global
- hermes_time.py: only reads cached values
Inefficient patterns fixed:
- startswith/endswith tuple form: 15 instances of
x.startswith('a') or x.startswith('b') consolidated to
x.startswith(('a', 'b'))
- len(x)==0 / len(x)>0: 13 instances replaced with pythonic
truthiness checks (not x / bool(x))
- in dict.keys(): 5 instances simplified to in dict
- Redefined unused name: removed duplicate _strip_mdv2 import in
send_message_tool.py
Other fixes:
- hermes_cli/doctor.py: Replaced undefined logger.debug() with pass
- hermes_cli/config.py: Consolidated chained .endswith() calls
Test results: 3934 passed, 17 failed (all pre-existing on main),
19 skipped. Zero regressions.
2026-04-07 10:25:31 -07:00
|
|
|
from dataclasses import dataclass
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
from typing import List, NamedTuple, Optional
|
|
|
|
|
|
|
|
|
|
from hermes_cli.providers import (
|
2026-04-10 02:52:56 -07:00
|
|
|
custom_provider_slug,
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
determine_api_mode,
|
|
|
|
|
get_label,
|
|
|
|
|
is_aggregator,
|
|
|
|
|
resolve_provider_full,
|
|
|
|
|
)
|
|
|
|
|
from hermes_cli.model_normalize import (
|
|
|
|
|
normalize_model_for_provider,
|
|
|
|
|
)
|
|
|
|
|
from agent.models_dev import (
|
|
|
|
|
ModelCapabilities,
|
|
|
|
|
ModelInfo,
|
|
|
|
|
get_model_capabilities,
|
|
|
|
|
get_model_info,
|
|
|
|
|
list_provider_models,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2026-04-05 18:41:03 -07:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Non-agentic model warning
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_HERMES_MODEL_WARNING = (
|
|
|
|
|
"Nous Research Hermes 3 & 4 models are NOT agentic and are not designed "
|
|
|
|
|
"for use with Hermes Agent. They lack the tool-calling capabilities "
|
|
|
|
|
"required for agent workflows. Consider using an agentic model instead "
|
|
|
|
|
"(Claude, GPT, Gemini, DeepSeek, etc.)."
|
|
|
|
|
)
|
|
|
|
|
|
fix(cli): narrow Nous Hermes non-agentic warning to actual hermes-3/-4 models
The startup warning that Nous Research Hermes 3 & 4 models are not agentic
fired on any model whose name contained "hermes" anywhere, via a plain
substring check. That false-positived on unrelated local Modelfiles such
as `hermes-brain:qwen3-14b-ctx16k` — a tool-capable Qwen3 wrapper that
happens to live under a custom "hermes" tag namespace — making the warning
noise for legitimate setups.
Replace the substring check with a narrow regex anchored on `^`, `/`, or
`:` boundaries that only matches the real Hermes-3 / Hermes-4 chat family
(e.g. `NousResearch/Hermes-3-Llama-3.1-70B`, `hermes-4-405b`,
`openrouter/hermes3:70b`). Consolidate into a single helper
`is_nous_hermes_non_agentic()` in `hermes_cli.model_switch` so the CLI
and the canonical check don't drift, and route the duplicate inline site
in `cli.HermesCLI._print_warnings()` through the helper.
Add a parametrized test covering positive matches (real Hermes-3/-4
names) and a broad set of negatives (custom Modelfiles, Qwen/Claude/GPT,
older Nous-Hermes-2 families, bare "hermes", empty string, and the
"brain-hermes-3-impostor" boundary case).
2026-04-13 06:12:41 +02:00
|
|
|
# Match only the real Nous Research Hermes 3 / Hermes 4 chat families.
|
|
|
|
|
# The previous substring check (`"hermes" in name.lower()`) false-positived on
|
|
|
|
|
# unrelated local Modelfiles like ``hermes-brain:qwen3-14b-ctx16k`` that just
|
|
|
|
|
# happen to carry "hermes" in their tag but are fully tool-capable.
|
|
|
|
|
#
|
|
|
|
|
# Positive examples the regex must match:
|
|
|
|
|
# NousResearch/Hermes-3-Llama-3.1-70B, hermes-4-405b, openrouter/hermes3:70b
|
|
|
|
|
# Negative examples it must NOT match:
|
|
|
|
|
# hermes-brain:qwen3-14b-ctx16k, qwen3:14b, claude-opus-4-6
|
|
|
|
|
_NOUS_HERMES_NON_AGENTIC_RE = re.compile(
|
|
|
|
|
r"(?:^|[/:])hermes[-_ ]?[34](?:[-_.:]|$)",
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_nous_hermes_non_agentic(model_name: str) -> bool:
|
|
|
|
|
"""Return True if *model_name* is a real Nous Hermes 3/4 chat model.
|
|
|
|
|
|
|
|
|
|
Used to decide whether to surface the non-agentic warning at startup.
|
|
|
|
|
Callers in :mod:`cli.py` and here should go through this single helper
|
|
|
|
|
so the two sites don't drift.
|
|
|
|
|
"""
|
|
|
|
|
if not model_name:
|
|
|
|
|
return False
|
|
|
|
|
return bool(_NOUS_HERMES_NON_AGENTIC_RE.search(model_name))
|
|
|
|
|
|
2026-04-05 18:41:03 -07:00
|
|
|
|
|
|
|
|
def _check_hermes_model_warning(model_name: str) -> str:
|
fix(cli): narrow Nous Hermes non-agentic warning to actual hermes-3/-4 models
The startup warning that Nous Research Hermes 3 & 4 models are not agentic
fired on any model whose name contained "hermes" anywhere, via a plain
substring check. That false-positived on unrelated local Modelfiles such
as `hermes-brain:qwen3-14b-ctx16k` — a tool-capable Qwen3 wrapper that
happens to live under a custom "hermes" tag namespace — making the warning
noise for legitimate setups.
Replace the substring check with a narrow regex anchored on `^`, `/`, or
`:` boundaries that only matches the real Hermes-3 / Hermes-4 chat family
(e.g. `NousResearch/Hermes-3-Llama-3.1-70B`, `hermes-4-405b`,
`openrouter/hermes3:70b`). Consolidate into a single helper
`is_nous_hermes_non_agentic()` in `hermes_cli.model_switch` so the CLI
and the canonical check don't drift, and route the duplicate inline site
in `cli.HermesCLI._print_warnings()` through the helper.
Add a parametrized test covering positive matches (real Hermes-3/-4
names) and a broad set of negatives (custom Modelfiles, Qwen/Claude/GPT,
older Nous-Hermes-2 families, bare "hermes", empty string, and the
"brain-hermes-3-impostor" boundary case).
2026-04-13 06:12:41 +02:00
|
|
|
"""Return a warning string if *model_name* is a Nous Hermes 3/4 chat model."""
|
|
|
|
|
if is_nous_hermes_non_agentic(model_name):
|
2026-04-05 18:41:03 -07:00
|
|
|
return _HERMES_MODEL_WARNING
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Model aliases -- short names -> (vendor, family) with NO version numbers.
|
|
|
|
|
# Resolved dynamically against the live models.dev catalog.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class ModelIdentity(NamedTuple):
|
|
|
|
|
"""Vendor slug and family prefix used for catalog resolution."""
|
|
|
|
|
vendor: str
|
|
|
|
|
family: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MODEL_ALIASES: dict[str, ModelIdentity] = {
|
|
|
|
|
# Anthropic
|
|
|
|
|
"sonnet": ModelIdentity("anthropic", "claude-sonnet"),
|
|
|
|
|
"opus": ModelIdentity("anthropic", "claude-opus"),
|
|
|
|
|
"haiku": ModelIdentity("anthropic", "claude-haiku"),
|
|
|
|
|
"claude": ModelIdentity("anthropic", "claude"),
|
|
|
|
|
|
|
|
|
|
# OpenAI
|
|
|
|
|
"gpt5": ModelIdentity("openai", "gpt-5"),
|
|
|
|
|
"gpt": ModelIdentity("openai", "gpt"),
|
|
|
|
|
"codex": ModelIdentity("openai", "codex"),
|
|
|
|
|
"o3": ModelIdentity("openai", "o3"),
|
|
|
|
|
"o4": ModelIdentity("openai", "o4"),
|
|
|
|
|
|
|
|
|
|
# Google
|
|
|
|
|
"gemini": ModelIdentity("google", "gemini"),
|
|
|
|
|
|
|
|
|
|
# DeepSeek
|
|
|
|
|
"deepseek": ModelIdentity("deepseek", "deepseek-chat"),
|
|
|
|
|
|
|
|
|
|
# X.AI
|
|
|
|
|
"grok": ModelIdentity("x-ai", "grok"),
|
|
|
|
|
|
|
|
|
|
# Meta
|
|
|
|
|
"llama": ModelIdentity("meta-llama", "llama"),
|
|
|
|
|
|
|
|
|
|
# Qwen / Alibaba
|
|
|
|
|
"qwen": ModelIdentity("qwen", "qwen"),
|
|
|
|
|
|
|
|
|
|
# MiniMax
|
|
|
|
|
"minimax": ModelIdentity("minimax", "minimax"),
|
|
|
|
|
|
|
|
|
|
# Nvidia
|
|
|
|
|
"nemotron": ModelIdentity("nvidia", "nemotron"),
|
|
|
|
|
|
|
|
|
|
# Moonshot / Kimi
|
|
|
|
|
"kimi": ModelIdentity("moonshotai", "kimi"),
|
|
|
|
|
|
|
|
|
|
# Z.AI / GLM
|
|
|
|
|
"glm": ModelIdentity("z-ai", "glm"),
|
|
|
|
|
|
2026-04-22 13:28:01 +05:30
|
|
|
# Step Plan (StepFun)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
"step": ModelIdentity("stepfun", "step"),
|
2026-03-24 07:08:07 -07:00
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# Xiaomi
|
|
|
|
|
"mimo": ModelIdentity("xiaomi", "mimo"),
|
|
|
|
|
|
|
|
|
|
# Arcee
|
|
|
|
|
"trinity": ModelIdentity("arcee-ai", "trinity"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-04-05 10:58:44 -07:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Direct aliases — exact model+provider+base_url for endpoints that aren't
|
|
|
|
|
# in the models.dev catalog (e.g. Ollama Cloud, local servers).
|
|
|
|
|
# Checked BEFORE catalog resolution. Format:
|
|
|
|
|
# alias -> (model_id, provider, base_url)
|
|
|
|
|
# These can also be loaded from config.yaml ``model_aliases:`` section.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class DirectAlias(NamedTuple):
|
|
|
|
|
"""Exact model mapping that bypasses catalog resolution."""
|
|
|
|
|
model: str
|
|
|
|
|
provider: str
|
|
|
|
|
base_url: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Built-in direct aliases (can be extended via config.yaml model_aliases:)
|
|
|
|
|
_BUILTIN_DIRECT_ALIASES: dict[str, DirectAlias] = {}
|
|
|
|
|
|
|
|
|
|
# Merged dict (builtins + user config); populated by _load_direct_aliases()
|
|
|
|
|
DIRECT_ALIASES: dict[str, DirectAlias] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_direct_aliases() -> dict[str, DirectAlias]:
|
|
|
|
|
"""Load direct aliases from config.yaml ``model_aliases:`` section.
|
|
|
|
|
|
|
|
|
|
Config format::
|
|
|
|
|
|
|
|
|
|
model_aliases:
|
|
|
|
|
qwen:
|
|
|
|
|
model: "qwen3.5:397b"
|
|
|
|
|
provider: custom
|
|
|
|
|
base_url: "https://ollama.com/v1"
|
|
|
|
|
minimax:
|
|
|
|
|
model: "minimax-m2.7"
|
|
|
|
|
provider: custom
|
|
|
|
|
base_url: "https://ollama.com/v1"
|
|
|
|
|
"""
|
|
|
|
|
merged = dict(_BUILTIN_DIRECT_ALIASES)
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.config import load_config
|
|
|
|
|
cfg = load_config()
|
|
|
|
|
user_aliases = cfg.get("model_aliases")
|
|
|
|
|
if isinstance(user_aliases, dict):
|
|
|
|
|
for name, entry in user_aliases.items():
|
|
|
|
|
if not isinstance(entry, dict):
|
|
|
|
|
continue
|
|
|
|
|
model = entry.get("model", "")
|
|
|
|
|
provider = entry.get("provider", "custom")
|
|
|
|
|
base_url = entry.get("base_url", "")
|
|
|
|
|
if model:
|
|
|
|
|
merged[name.strip().lower()] = DirectAlias(
|
|
|
|
|
model=model, provider=provider, base_url=base_url,
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return merged
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_direct_aliases() -> None:
|
|
|
|
|
"""Lazy-load direct aliases on first use."""
|
|
|
|
|
global DIRECT_ALIASES
|
|
|
|
|
if not DIRECT_ALIASES:
|
|
|
|
|
DIRECT_ALIASES = _load_direct_aliases()
|
|
|
|
|
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Result dataclasses
|
|
|
|
|
# ---------------------------------------------------------------------------
|
2026-03-24 07:08:07 -07:00
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class ModelSwitchResult:
|
|
|
|
|
"""Result of a model switch attempt."""
|
|
|
|
|
|
|
|
|
|
success: bool
|
|
|
|
|
new_model: str = ""
|
|
|
|
|
target_provider: str = ""
|
|
|
|
|
provider_changed: bool = False
|
|
|
|
|
api_key: str = ""
|
|
|
|
|
base_url: str = ""
|
2026-04-02 09:36:24 -07:00
|
|
|
api_mode: str = ""
|
2026-03-24 07:08:07 -07:00
|
|
|
error_message: str = ""
|
|
|
|
|
warning_message: str = ""
|
|
|
|
|
provider_label: str = ""
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
resolved_via_alias: str = ""
|
|
|
|
|
capabilities: Optional[ModelCapabilities] = None
|
|
|
|
|
model_info: Optional[ModelInfo] = None
|
|
|
|
|
is_global: bool = False
|
2026-03-24 07:08:07 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class CustomAutoResult:
|
|
|
|
|
"""Result of switching to bare 'custom' provider with auto-detect."""
|
|
|
|
|
|
|
|
|
|
success: bool
|
|
|
|
|
model: str = ""
|
|
|
|
|
base_url: str = ""
|
|
|
|
|
api_key: str = ""
|
|
|
|
|
error_message: str = ""
|
|
|
|
|
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Flag parsing
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def parse_model_flags(raw_args: str) -> tuple[str, str, bool]:
|
|
|
|
|
"""Parse --provider and --global flags from /model command args.
|
|
|
|
|
|
|
|
|
|
Returns (model_input, explicit_provider, is_global).
|
|
|
|
|
|
|
|
|
|
Examples::
|
|
|
|
|
|
|
|
|
|
"sonnet" -> ("sonnet", "", False)
|
|
|
|
|
"sonnet --global" -> ("sonnet", "", True)
|
|
|
|
|
"sonnet --provider anthropic" -> ("sonnet", "anthropic", False)
|
|
|
|
|
"--provider my-ollama" -> ("", "my-ollama", False)
|
|
|
|
|
"sonnet --provider anthropic --global" -> ("sonnet", "anthropic", True)
|
|
|
|
|
"""
|
|
|
|
|
is_global = False
|
|
|
|
|
explicit_provider = ""
|
|
|
|
|
|
2026-04-10 06:38:27 -06:00
|
|
|
# Normalize Unicode dashes (Telegram/iOS auto-converts -- to em/en dash)
|
|
|
|
|
# A single Unicode dash before a flag keyword becomes "--"
|
|
|
|
|
import re as _re
|
|
|
|
|
raw_args = _re.sub(r'[\u2012\u2013\u2014\u2015](provider|global)', r'--\1', raw_args)
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# Extract --global
|
|
|
|
|
if "--global" in raw_args:
|
|
|
|
|
is_global = True
|
|
|
|
|
raw_args = raw_args.replace("--global", "").strip()
|
|
|
|
|
|
|
|
|
|
# Extract --provider <name>
|
|
|
|
|
parts = raw_args.split()
|
|
|
|
|
i = 0
|
|
|
|
|
filtered: list[str] = []
|
|
|
|
|
while i < len(parts):
|
|
|
|
|
if parts[i] == "--provider" and i + 1 < len(parts):
|
|
|
|
|
explicit_provider = parts[i + 1]
|
|
|
|
|
i += 2
|
|
|
|
|
else:
|
|
|
|
|
filtered.append(parts[i])
|
|
|
|
|
i += 1
|
|
|
|
|
|
|
|
|
|
model_input = " ".join(filtered).strip()
|
|
|
|
|
return (model_input, explicit_provider, is_global)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Alias resolution
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-04-23 23:18:33 +05:30
|
|
|
def _model_sort_key(model_id: str, prefix: str) -> tuple:
|
|
|
|
|
"""Sort key for model version preference.
|
|
|
|
|
|
|
|
|
|
Extracts version numbers after the family prefix and returns a sort key
|
|
|
|
|
that prefers higher versions. Suffix tokens (``pro``, ``omni``, etc.)
|
|
|
|
|
are used as tiebreakers, with common quality indicators ranked.
|
|
|
|
|
|
|
|
|
|
Examples (with prefix ``"mimo"``)::
|
|
|
|
|
|
|
|
|
|
mimo-v2.5-pro → (-2.5, 0, 'pro') # highest version wins
|
|
|
|
|
mimo-v2.5 → (-2.5, 1, '') # no suffix = lower than pro
|
|
|
|
|
mimo-v2-pro → (-2.0, 0, 'pro')
|
|
|
|
|
mimo-v2-omni → (-2.0, 1, 'omni')
|
|
|
|
|
mimo-v2-flash → (-2.0, 1, 'flash')
|
|
|
|
|
"""
|
|
|
|
|
# Strip the prefix (and optional "/" separator for aggregator slugs)
|
|
|
|
|
rest = model_id[len(prefix):]
|
|
|
|
|
if rest.startswith("/"):
|
|
|
|
|
rest = rest[1:]
|
|
|
|
|
rest = rest.lstrip("-").strip()
|
|
|
|
|
|
|
|
|
|
# Parse version and suffix from the remainder.
|
|
|
|
|
# "v2.5-pro" → version [2.5], suffix "pro"
|
|
|
|
|
# "-omni" → version [], suffix "omni"
|
|
|
|
|
# State machine: start → in_version → between → in_suffix
|
|
|
|
|
nums: list[float] = []
|
|
|
|
|
suffix_buf = ""
|
|
|
|
|
state = "start"
|
|
|
|
|
num_buf = ""
|
|
|
|
|
|
|
|
|
|
for ch in rest:
|
|
|
|
|
if state == "start":
|
|
|
|
|
if ch in "vV":
|
|
|
|
|
state = "in_version"
|
|
|
|
|
elif ch.isdigit():
|
|
|
|
|
state = "in_version"
|
|
|
|
|
num_buf += ch
|
|
|
|
|
elif ch in "-_.":
|
|
|
|
|
pass # skip separators before any content
|
|
|
|
|
else:
|
|
|
|
|
state = "in_suffix"
|
|
|
|
|
suffix_buf += ch
|
|
|
|
|
elif state == "in_version":
|
|
|
|
|
if ch.isdigit():
|
|
|
|
|
num_buf += ch
|
|
|
|
|
elif ch == ".":
|
|
|
|
|
if "." in num_buf:
|
|
|
|
|
# Second dot — flush current number, start new component
|
|
|
|
|
try:
|
|
|
|
|
nums.append(float(num_buf.rstrip(".")))
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
num_buf = ""
|
|
|
|
|
else:
|
|
|
|
|
num_buf += ch
|
|
|
|
|
elif ch in "-_.":
|
|
|
|
|
if num_buf:
|
|
|
|
|
try:
|
|
|
|
|
nums.append(float(num_buf.rstrip(".")))
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
num_buf = ""
|
|
|
|
|
state = "between"
|
|
|
|
|
else:
|
|
|
|
|
if num_buf:
|
|
|
|
|
try:
|
|
|
|
|
nums.append(float(num_buf.rstrip(".")))
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
num_buf = ""
|
|
|
|
|
state = "in_suffix"
|
|
|
|
|
suffix_buf += ch
|
|
|
|
|
elif state == "between":
|
|
|
|
|
if ch.isdigit():
|
|
|
|
|
state = "in_version"
|
|
|
|
|
num_buf = ch
|
|
|
|
|
elif ch in "vV":
|
|
|
|
|
state = "in_version"
|
|
|
|
|
elif ch in "-_.":
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
state = "in_suffix"
|
|
|
|
|
suffix_buf += ch
|
|
|
|
|
elif state == "in_suffix":
|
|
|
|
|
suffix_buf += ch
|
|
|
|
|
|
|
|
|
|
# Flush remaining buffer (strip trailing dots — "5.4." → "5.4")
|
|
|
|
|
if num_buf and state == "in_version":
|
|
|
|
|
try:
|
|
|
|
|
nums.append(float(num_buf.rstrip(".")))
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
suffix = suffix_buf.lower().strip("-_.")
|
|
|
|
|
suffix = suffix.strip()
|
|
|
|
|
|
|
|
|
|
# Negate versions so higher → sorts first
|
|
|
|
|
version_key = tuple(-n for n in nums)
|
|
|
|
|
|
|
|
|
|
# Suffix quality ranking: pro/max > (no suffix) > omni/flash/mini/lite
|
|
|
|
|
# Lower number = preferred
|
|
|
|
|
_SUFFIX_RANK = {"pro": 0, "max": 0, "plus": 0, "turbo": 0}
|
|
|
|
|
suffix_rank = _SUFFIX_RANK.get(suffix, 1)
|
|
|
|
|
|
|
|
|
|
return version_key + (suffix_rank, suffix)
|
|
|
|
|
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
def resolve_alias(
|
|
|
|
|
raw_input: str,
|
|
|
|
|
current_provider: str,
|
|
|
|
|
) -> Optional[tuple[str, str, str]]:
|
|
|
|
|
"""Resolve a short alias against the current provider's catalog.
|
|
|
|
|
|
|
|
|
|
Looks up *raw_input* in :data:`MODEL_ALIASES`, then searches the
|
2026-04-23 23:18:33 +05:30
|
|
|
current provider's models.dev catalog for the model whose ID starts
|
|
|
|
|
with ``vendor/family`` (or just ``family`` for non-aggregator
|
|
|
|
|
providers) and has the **highest version**.
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
``(provider, resolved_model_id, alias_name)`` if a match is
|
|
|
|
|
found on the current provider, or ``None`` if the alias doesn't
|
|
|
|
|
exist or no matching model is available.
|
|
|
|
|
"""
|
|
|
|
|
key = raw_input.strip().lower()
|
2026-04-05 10:58:44 -07:00
|
|
|
|
|
|
|
|
# Check direct aliases first (exact model+provider+base_url mappings)
|
|
|
|
|
_ensure_direct_aliases()
|
|
|
|
|
direct = DIRECT_ALIASES.get(key)
|
|
|
|
|
if direct is not None:
|
|
|
|
|
return (direct.provider, direct.model, key)
|
|
|
|
|
|
|
|
|
|
# Reverse lookup: match by model ID so full names (e.g. "kimi-k2.5",
|
|
|
|
|
# "glm-4.7") route through direct aliases instead of falling through
|
|
|
|
|
# to the catalog/OpenRouter.
|
|
|
|
|
for alias_name, da in DIRECT_ALIASES.items():
|
|
|
|
|
if da.model.lower() == key:
|
|
|
|
|
return (da.provider, da.model, alias_name)
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
identity = MODEL_ALIASES.get(key)
|
|
|
|
|
if identity is None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
vendor, family = identity
|
|
|
|
|
|
2026-04-23 23:18:33 +05:30
|
|
|
# Build catalog from models.dev, then merge in static _PROVIDER_MODELS
|
|
|
|
|
# entries that models.dev may be missing (e.g. newly added models not
|
|
|
|
|
# yet synced to the registry).
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
catalog = list_provider_models(current_provider)
|
2026-04-23 23:18:33 +05:30
|
|
|
try:
|
|
|
|
|
from hermes_cli.models import _PROVIDER_MODELS
|
|
|
|
|
static = _PROVIDER_MODELS.get(current_provider, [])
|
|
|
|
|
if static:
|
|
|
|
|
seen = {m.lower() for m in catalog}
|
|
|
|
|
for m in static:
|
|
|
|
|
if m.lower() not in seen:
|
|
|
|
|
catalog.append(m)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
|
|
|
|
# For aggregators, models are vendor/model-name format
|
|
|
|
|
aggregator = is_aggregator(current_provider)
|
|
|
|
|
|
2026-04-23 23:18:33 +05:30
|
|
|
if aggregator:
|
|
|
|
|
prefix = f"{vendor}/{family}".lower()
|
|
|
|
|
matches = [
|
|
|
|
|
mid for mid in catalog
|
|
|
|
|
if mid.lower().startswith(prefix)
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
family_lower = family.lower()
|
|
|
|
|
matches = [
|
|
|
|
|
mid for mid in catalog
|
|
|
|
|
if mid.lower().startswith(family_lower)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
if not matches:
|
|
|
|
|
return None
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
2026-04-23 23:18:33 +05:30
|
|
|
# Sort by version descending — prefer the latest/highest version
|
|
|
|
|
prefix_for_sort = f"{vendor}/{family}" if aggregator else family
|
|
|
|
|
matches.sort(key=lambda m: _model_sort_key(m, prefix_for_sort))
|
|
|
|
|
return (current_provider, matches[0], key)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
|
|
|
|
|
2026-04-06 15:20:06 +02:00
|
|
|
def get_authenticated_provider_slugs(
|
|
|
|
|
current_provider: str = "",
|
|
|
|
|
user_providers: dict = None,
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
custom_providers: list | None = None,
|
2026-04-06 15:20:06 +02:00
|
|
|
) -> list[str]:
|
|
|
|
|
"""Return slugs of providers that have credentials.
|
|
|
|
|
|
|
|
|
|
Uses ``list_authenticated_providers()`` which is backed by the models.dev
|
|
|
|
|
in-memory cache (1 hr TTL) — no extra network cost.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
providers = list_authenticated_providers(
|
|
|
|
|
current_provider=current_provider,
|
|
|
|
|
user_providers=user_providers,
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
custom_providers=custom_providers,
|
2026-04-06 15:20:06 +02:00
|
|
|
max_models=0,
|
|
|
|
|
)
|
|
|
|
|
return [p["slug"] for p in providers]
|
|
|
|
|
except Exception:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
def _resolve_alias_fallback(
|
|
|
|
|
raw_input: str,
|
2026-04-06 15:20:06 +02:00
|
|
|
authenticated_providers: list[str] = (),
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
) -> Optional[tuple[str, str, str]]:
|
2026-04-06 15:20:06 +02:00
|
|
|
"""Try to resolve an alias on the user's authenticated providers.
|
|
|
|
|
|
|
|
|
|
Falls back to ``("openrouter", "nous")`` only when no authenticated
|
|
|
|
|
providers are supplied (backwards compat for non-interactive callers).
|
|
|
|
|
"""
|
|
|
|
|
providers = authenticated_providers or ("openrouter", "nous")
|
|
|
|
|
for provider in providers:
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
result = resolve_alias(raw_input, provider)
|
|
|
|
|
if result is not None:
|
|
|
|
|
return result
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Core model-switching pipeline
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-03-24 07:08:07 -07:00
|
|
|
def switch_model(
|
|
|
|
|
raw_input: str,
|
|
|
|
|
current_provider: str,
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
current_model: str,
|
2026-03-24 07:08:07 -07:00
|
|
|
current_base_url: str = "",
|
|
|
|
|
current_api_key: str = "",
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
is_global: bool = False,
|
|
|
|
|
explicit_provider: str = "",
|
|
|
|
|
user_providers: dict = None,
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
custom_providers: list | None = None,
|
2026-03-24 07:08:07 -07:00
|
|
|
) -> ModelSwitchResult:
|
|
|
|
|
"""Core model-switching pipeline shared between CLI and gateway.
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
Resolution chain:
|
|
|
|
|
|
|
|
|
|
If --provider given:
|
|
|
|
|
a. Resolve provider via resolve_provider_full()
|
|
|
|
|
b. Resolve credentials
|
|
|
|
|
c. If model given, resolve alias on target provider or use as-is
|
|
|
|
|
d. If no model, auto-detect from endpoint
|
|
|
|
|
|
|
|
|
|
If no --provider:
|
|
|
|
|
a. Try alias resolution on current provider
|
|
|
|
|
b. If alias exists but not on current provider -> fallback
|
|
|
|
|
c. On aggregator, try vendor/model slug conversion
|
|
|
|
|
d. Aggregator catalog search
|
|
|
|
|
e. detect_provider_for_model() as last resort
|
|
|
|
|
f. Resolve credentials
|
|
|
|
|
g. Normalize model name for target provider
|
|
|
|
|
|
|
|
|
|
Finally:
|
|
|
|
|
h. Get full model metadata from models.dev
|
|
|
|
|
i. Build result
|
2026-03-24 07:08:07 -07:00
|
|
|
|
|
|
|
|
Args:
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
raw_input: The model name (after flag parsing).
|
2026-03-24 07:08:07 -07:00
|
|
|
current_provider: The currently active provider.
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
current_model: The currently active model name.
|
|
|
|
|
current_base_url: The currently active base URL.
|
2026-03-24 07:08:07 -07:00
|
|
|
current_api_key: The currently active API key.
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
is_global: Whether to persist the switch.
|
|
|
|
|
explicit_provider: From --provider flag (empty = no explicit provider).
|
|
|
|
|
user_providers: The ``providers:`` dict from config.yaml (for user endpoints).
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
custom_providers: The ``custom_providers:`` list from config.yaml.
|
2026-03-24 07:08:07 -07:00
|
|
|
|
|
|
|
|
Returns:
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
ModelSwitchResult with all information the caller needs.
|
2026-03-24 07:08:07 -07:00
|
|
|
"""
|
|
|
|
|
from hermes_cli.models import (
|
2026-04-16 13:45:24 +05:30
|
|
|
copilot_model_api_mode,
|
2026-03-24 07:08:07 -07:00
|
|
|
detect_provider_for_model,
|
|
|
|
|
validate_requested_model,
|
2026-04-02 09:36:24 -07:00
|
|
|
opencode_model_api_mode,
|
2026-03-24 07:08:07 -07:00
|
|
|
)
|
|
|
|
|
from hermes_cli.runtime_provider import resolve_runtime_provider
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
resolved_alias = ""
|
|
|
|
|
new_model = raw_input.strip()
|
|
|
|
|
target_provider = current_provider
|
|
|
|
|
|
|
|
|
|
# =================================================================
|
|
|
|
|
# PATH A: Explicit --provider given
|
|
|
|
|
# =================================================================
|
|
|
|
|
if explicit_provider:
|
|
|
|
|
# Resolve the provider
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
pdef = resolve_provider_full(
|
|
|
|
|
explicit_provider,
|
|
|
|
|
user_providers,
|
|
|
|
|
custom_providers,
|
|
|
|
|
)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
if pdef is None:
|
2026-04-05 23:31:20 -07:00
|
|
|
_switch_err = (
|
|
|
|
|
f"Unknown provider '{explicit_provider}'. "
|
|
|
|
|
f"Check 'hermes model' for available providers, or define it "
|
|
|
|
|
f"in config.yaml under 'providers:'."
|
|
|
|
|
)
|
|
|
|
|
# Check for common config issues that cause provider resolution failures
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.config import validate_config_structure
|
|
|
|
|
_cfg_issues = validate_config_structure()
|
|
|
|
|
if _cfg_issues:
|
|
|
|
|
_switch_err += "\n\nRun 'hermes doctor' — config issues detected:"
|
|
|
|
|
for _ci in _cfg_issues[:3]:
|
|
|
|
|
_switch_err += f"\n • {_ci.message}"
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
return ModelSwitchResult(
|
|
|
|
|
success=False,
|
|
|
|
|
is_global=is_global,
|
2026-04-05 23:31:20 -07:00
|
|
|
error_message=_switch_err,
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
)
|
2026-03-24 07:08:07 -07:00
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
target_provider = pdef.id
|
|
|
|
|
|
|
|
|
|
# If no model specified, try auto-detect from endpoint
|
|
|
|
|
if not new_model:
|
|
|
|
|
if pdef.base_url:
|
|
|
|
|
from hermes_cli.runtime_provider import _auto_detect_local_model
|
|
|
|
|
detected = _auto_detect_local_model(pdef.base_url)
|
|
|
|
|
if detected:
|
|
|
|
|
new_model = detected
|
|
|
|
|
else:
|
|
|
|
|
return ModelSwitchResult(
|
|
|
|
|
success=False,
|
|
|
|
|
target_provider=target_provider,
|
|
|
|
|
provider_label=pdef.name,
|
|
|
|
|
is_global=is_global,
|
|
|
|
|
error_message=(
|
|
|
|
|
f"No model detected on {pdef.name} ({pdef.base_url}). "
|
|
|
|
|
f"Specify the model explicitly: /model <model-name> --provider {explicit_provider}"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
return ModelSwitchResult(
|
|
|
|
|
success=False,
|
|
|
|
|
target_provider=target_provider,
|
|
|
|
|
provider_label=pdef.name,
|
|
|
|
|
is_global=is_global,
|
|
|
|
|
error_message=(
|
|
|
|
|
f"Provider '{pdef.name}' has no base URL configured. "
|
|
|
|
|
f"Specify a model: /model <model-name> --provider {explicit_provider}"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Resolve alias on the TARGET provider
|
|
|
|
|
alias_result = resolve_alias(new_model, target_provider)
|
|
|
|
|
if alias_result is not None:
|
|
|
|
|
_, new_model, resolved_alias = alias_result
|
|
|
|
|
|
|
|
|
|
# =================================================================
|
|
|
|
|
# PATH B: No explicit provider — resolve from model input
|
|
|
|
|
# =================================================================
|
|
|
|
|
else:
|
|
|
|
|
# --- Step a: Try alias resolution on current provider ---
|
|
|
|
|
alias_result = resolve_alias(raw_input, current_provider)
|
|
|
|
|
|
|
|
|
|
if alias_result is not None:
|
|
|
|
|
target_provider, new_model, resolved_alias = alias_result
|
|
|
|
|
logger.debug(
|
|
|
|
|
"Alias '%s' resolved to %s on %s",
|
|
|
|
|
resolved_alias, new_model, target_provider,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# --- Step b: Alias exists but not on current provider -> fallback ---
|
|
|
|
|
key = raw_input.strip().lower()
|
|
|
|
|
if key in MODEL_ALIASES:
|
2026-04-06 15:20:06 +02:00
|
|
|
authed = get_authenticated_provider_slugs(
|
|
|
|
|
current_provider=current_provider,
|
|
|
|
|
user_providers=user_providers,
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
custom_providers=custom_providers,
|
2026-04-06 15:20:06 +02:00
|
|
|
)
|
|
|
|
|
fallback_result = _resolve_alias_fallback(raw_input, authed)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
if fallback_result is not None:
|
|
|
|
|
target_provider, new_model, resolved_alias = fallback_result
|
|
|
|
|
logger.debug(
|
|
|
|
|
"Alias '%s' resolved via fallback to %s on %s",
|
|
|
|
|
resolved_alias, new_model, target_provider,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
identity = MODEL_ALIASES[key]
|
|
|
|
|
return ModelSwitchResult(
|
|
|
|
|
success=False,
|
|
|
|
|
is_global=is_global,
|
|
|
|
|
error_message=(
|
|
|
|
|
f"Alias '{key}' maps to {identity.vendor}/{identity.family} "
|
|
|
|
|
f"but no matching model was found in any provider catalog. "
|
|
|
|
|
f"Try specifying the full model name."
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# --- Step c: On aggregator, convert vendor:model to vendor/model ---
|
2026-04-08 19:58:16 -07:00
|
|
|
# Only convert when there's no slash — a slash means the name
|
|
|
|
|
# is already in vendor/model format and the colon is a variant
|
|
|
|
|
# tag (:free, :extended, :fast) that must be preserved.
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
colon_pos = raw_input.find(":")
|
2026-04-08 19:58:16 -07:00
|
|
|
if colon_pos > 0 and "/" not in raw_input and is_aggregator(current_provider):
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
left = raw_input[:colon_pos].strip().lower()
|
|
|
|
|
right = raw_input[colon_pos + 1:].strip()
|
|
|
|
|
if left and right:
|
|
|
|
|
# Colons become slashes for aggregator slugs
|
|
|
|
|
new_model = f"{left}/{right}"
|
|
|
|
|
logger.debug(
|
|
|
|
|
"Converted vendor:model '%s' to aggregator slug '%s'",
|
|
|
|
|
raw_input, new_model,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# --- Step d: Aggregator catalog search ---
|
|
|
|
|
if is_aggregator(target_provider) and not resolved_alias:
|
|
|
|
|
catalog = list_provider_models(target_provider)
|
|
|
|
|
if catalog:
|
|
|
|
|
new_model_lower = new_model.lower()
|
|
|
|
|
for mid in catalog:
|
|
|
|
|
if mid.lower() == new_model_lower:
|
|
|
|
|
new_model = mid
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
for mid in catalog:
|
|
|
|
|
if "/" in mid:
|
|
|
|
|
_, bare = mid.split("/", 1)
|
|
|
|
|
if bare.lower() == new_model_lower:
|
|
|
|
|
new_model = mid
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# --- Step e: detect_provider_for_model() as last resort ---
|
|
|
|
|
_base = current_base_url or ""
|
|
|
|
|
is_custom = current_provider in ("custom", "local") or (
|
|
|
|
|
"localhost" in _base or "127.0.0.1" in _base
|
|
|
|
|
)
|
2026-03-24 07:08:07 -07:00
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
if (
|
|
|
|
|
target_provider == current_provider
|
|
|
|
|
and not is_custom
|
|
|
|
|
and not resolved_alias
|
|
|
|
|
):
|
|
|
|
|
detected = detect_provider_for_model(new_model, current_provider)
|
|
|
|
|
if detected:
|
|
|
|
|
target_provider, new_model = detected
|
|
|
|
|
|
|
|
|
|
# =================================================================
|
|
|
|
|
# COMMON PATH: Resolve credentials, normalize, get metadata
|
|
|
|
|
# =================================================================
|
2026-03-24 07:08:07 -07:00
|
|
|
|
|
|
|
|
provider_changed = target_provider != current_provider
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
provider_label = get_label(target_provider)
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
if target_provider.startswith("custom:"):
|
|
|
|
|
custom_pdef = resolve_provider_full(
|
|
|
|
|
target_provider,
|
|
|
|
|
user_providers,
|
|
|
|
|
custom_providers,
|
|
|
|
|
)
|
|
|
|
|
if custom_pdef is not None:
|
|
|
|
|
provider_label = custom_pdef.name
|
2026-03-24 07:08:07 -07:00
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# --- Resolve credentials ---
|
2026-03-24 07:08:07 -07:00
|
|
|
api_key = current_api_key
|
|
|
|
|
base_url = current_base_url
|
2026-04-02 09:36:24 -07:00
|
|
|
api_mode = ""
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
|
|
|
|
if provider_changed or explicit_provider:
|
2026-03-24 07:08:07 -07:00
|
|
|
try:
|
fix(opencode): derive api_mode from target model, not stale config default (#15106)
/model kimi-k2.6 on opencode-zen (or glm-5.1 on opencode-go) returned OpenCode's
website 404 HTML page when the user's persisted model.default was a Claude or
MiniMax model. The switched-to chat_completions request hit
https://opencode.ai/zen (or /zen/go) with no /v1 suffix.
Root cause: resolve_runtime_provider() computed api_mode from
model_cfg.get('default') instead of the model being requested. With a Claude
default, it resolved api_mode=anthropic_messages, stripped /v1 from base_url
(required for the Anthropic SDK), then switch_model()'s opencode_model_api_mode
override flipped api_mode back to chat_completions without restoring /v1.
Fix: thread an optional target_model kwarg through resolve_runtime_provider
and _resolve_runtime_from_pool_entry. When the caller is performing an explicit
mid-session model switch (i.e. switch_model()), the target model drives both
api_mode selection and the conditional /v1 strip. Other callers (CLI init,
gateway init, cron, ACP, aux client, delegate, account_usage, tui_gateway) pass
nothing and preserve the existing config-default behavior.
Regression tests added in test_model_switch_opencode_anthropic.py use the REAL
resolver (not a mock) to guard the exact Quentin-repro scenario. Existing tests
that mocked resolve_runtime_provider with 'lambda requested:' had their mock
signatures widened to '**kwargs' to accept the new kwarg.
2026-04-24 04:58:46 -07:00
|
|
|
runtime = resolve_runtime_provider(
|
|
|
|
|
requested=target_provider,
|
|
|
|
|
target_model=new_model,
|
|
|
|
|
)
|
2026-03-24 07:08:07 -07:00
|
|
|
api_key = runtime.get("api_key", "")
|
|
|
|
|
base_url = runtime.get("base_url", "")
|
2026-04-02 09:36:24 -07:00
|
|
|
api_mode = runtime.get("api_mode", "")
|
2026-03-24 07:08:07 -07:00
|
|
|
except Exception as e:
|
|
|
|
|
return ModelSwitchResult(
|
|
|
|
|
success=False,
|
|
|
|
|
target_provider=target_provider,
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
provider_label=provider_label,
|
|
|
|
|
is_global=is_global,
|
2026-03-24 07:08:07 -07:00
|
|
|
error_message=(
|
|
|
|
|
f"Could not resolve credentials for provider "
|
|
|
|
|
f"'{provider_label}': {e}"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
try:
|
fix(opencode): derive api_mode from target model, not stale config default (#15106)
/model kimi-k2.6 on opencode-zen (or glm-5.1 on opencode-go) returned OpenCode's
website 404 HTML page when the user's persisted model.default was a Claude or
MiniMax model. The switched-to chat_completions request hit
https://opencode.ai/zen (or /zen/go) with no /v1 suffix.
Root cause: resolve_runtime_provider() computed api_mode from
model_cfg.get('default') instead of the model being requested. With a Claude
default, it resolved api_mode=anthropic_messages, stripped /v1 from base_url
(required for the Anthropic SDK), then switch_model()'s opencode_model_api_mode
override flipped api_mode back to chat_completions without restoring /v1.
Fix: thread an optional target_model kwarg through resolve_runtime_provider
and _resolve_runtime_from_pool_entry. When the caller is performing an explicit
mid-session model switch (i.e. switch_model()), the target model drives both
api_mode selection and the conditional /v1 strip. Other callers (CLI init,
gateway init, cron, ACP, aux client, delegate, account_usage, tui_gateway) pass
nothing and preserve the existing config-default behavior.
Regression tests added in test_model_switch_opencode_anthropic.py use the REAL
resolver (not a mock) to guard the exact Quentin-repro scenario. Existing tests
that mocked resolve_runtime_provider with 'lambda requested:' had their mock
signatures widened to '**kwargs' to accept the new kwarg.
2026-04-24 04:58:46 -07:00
|
|
|
runtime = resolve_runtime_provider(
|
|
|
|
|
requested=current_provider,
|
|
|
|
|
target_model=new_model,
|
|
|
|
|
)
|
2026-03-24 07:08:07 -07:00
|
|
|
api_key = runtime.get("api_key", "")
|
|
|
|
|
base_url = runtime.get("base_url", "")
|
2026-04-02 09:36:24 -07:00
|
|
|
api_mode = runtime.get("api_mode", "")
|
2026-03-24 07:08:07 -07:00
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
2026-04-05 10:58:44 -07:00
|
|
|
# --- Direct alias override: use exact base_url from the alias if set ---
|
|
|
|
|
if resolved_alias:
|
|
|
|
|
_ensure_direct_aliases()
|
|
|
|
|
_da = DIRECT_ALIASES.get(resolved_alias)
|
|
|
|
|
if _da is not None and _da.base_url:
|
|
|
|
|
base_url = _da.base_url
|
2026-04-18 23:01:53 +08:00
|
|
|
api_mode = "" # clear so determine_api_mode re-detects from URL
|
2026-04-05 10:58:44 -07:00
|
|
|
if not api_key:
|
|
|
|
|
api_key = "no-key-required"
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# --- Normalize model name for target provider ---
|
|
|
|
|
new_model = normalize_model_for_provider(new_model, target_provider)
|
|
|
|
|
|
|
|
|
|
# --- Validate ---
|
2026-03-24 07:08:07 -07:00
|
|
|
try:
|
|
|
|
|
validation = validate_requested_model(
|
|
|
|
|
new_model,
|
|
|
|
|
target_provider,
|
|
|
|
|
api_key=api_key,
|
|
|
|
|
base_url=base_url,
|
2026-04-20 17:47:00 +08:00
|
|
|
api_mode=api_mode or None,
|
2026-03-24 07:08:07 -07:00
|
|
|
)
|
2026-04-13 14:57:42 -05:00
|
|
|
except Exception as e:
|
2026-03-24 07:08:07 -07:00
|
|
|
validation = {
|
2026-04-13 14:57:42 -05:00
|
|
|
"accepted": False,
|
|
|
|
|
"persist": False,
|
2026-03-24 07:08:07 -07:00
|
|
|
"recognized": False,
|
2026-04-13 14:57:42 -05:00
|
|
|
"message": f"Could not validate `{new_model}`: {e}",
|
2026-03-24 07:08:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if not validation.get("accepted"):
|
|
|
|
|
msg = validation.get("message", "Invalid model")
|
|
|
|
|
return ModelSwitchResult(
|
|
|
|
|
success=False,
|
|
|
|
|
new_model=new_model,
|
|
|
|
|
target_provider=target_provider,
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
provider_label=provider_label,
|
|
|
|
|
is_global=is_global,
|
2026-03-24 07:08:07 -07:00
|
|
|
error_message=msg,
|
|
|
|
|
)
|
|
|
|
|
|
fix: auto-correct close model name matches in /model validation (#9424)
* feat(skills): add fitness-nutrition skill to optional-skills
Cherry-picked from PR #9177 by @haileymarshall.
Adds a fitness and nutrition skill for gym-goers and health-conscious users:
- Exercise search via wger API (690+ exercises, free, no auth)
- Nutrition lookup via USDA FoodData Central (380K+ foods, DEMO_KEY fallback)
- Offline body composition calculators (BMI, TDEE, 1RM, macros, body fat %)
- Pure stdlib Python, no pip dependencies
Changes from original PR:
- Moved from skills/ to optional-skills/health/ (correct location)
- Fixed BMR formula in FORMULAS.md (removed confusing -5+10, now just +5)
- Fixed author attribution to match PR submitter
- Marked USDA_API_KEY as optional (DEMO_KEY works without signup)
Also adds optional env var support to the skill readiness checker:
- New 'optional: true' field in required_environment_variables entries
- Optional vars are preserved in metadata but don't block skill readiness
- Optional vars skip the CLI capture prompt flow
- Skills with only optional missing vars show as 'available' not 'setup_needed'
* fix: auto-correct close model name matches in /model validation
When a user types a model name with a minor typo (e.g. gpt5.3-codex instead
of gpt-5.3-codex), the validation now auto-corrects to the closest match
instead of accepting the wrong name with a warning.
Uses difflib get_close_matches with cutoff=0.9 to avoid false corrections
(e.g. gpt-5.3 should not silently become gpt-5.4). Applied consistently
across all three validation paths: codex provider, custom endpoints, and
generic API-probed providers.
The validate_requested_model() return dict gains an optional corrected_model
key that switch_model() applies before building the result.
Reported by Discord user — /model gpt5.3-codex was accepted with a warning
but would fail at the API level.
---------
Co-authored-by: haileymarshall <haileymarshall@users.noreply.github.com>
2026-04-13 23:09:39 -07:00
|
|
|
# Apply auto-correction if validation found a closer match
|
|
|
|
|
if validation.get("corrected_model"):
|
|
|
|
|
new_model = validation["corrected_model"]
|
|
|
|
|
|
2026-04-16 13:45:24 +05:30
|
|
|
# --- Copilot api_mode override ---
|
|
|
|
|
if target_provider in {"copilot", "github-copilot"}:
|
|
|
|
|
api_mode = copilot_model_api_mode(new_model, api_key=api_key)
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# --- OpenCode api_mode override ---
|
2026-04-16 13:45:24 +05:30
|
|
|
if target_provider in {"opencode-zen", "opencode-go", "opencode"}:
|
2026-04-02 09:36:24 -07:00
|
|
|
api_mode = opencode_model_api_mode(target_provider, new_model)
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# --- Determine api_mode if not already set ---
|
|
|
|
|
if not api_mode:
|
|
|
|
|
api_mode = determine_api_mode(target_provider, base_url)
|
|
|
|
|
|
fix(opencode): strip /v1 from base_url on mid-session /model switch to Anthropic-routed models (#11286)
PR #4918 fixed the double-/v1 bug at fresh agent init by stripping the
trailing /v1 from OpenCode base URLs when api_mode is anthropic_messages
(so the Anthropic SDK's own /v1/messages doesn't land on /v1/v1/messages).
The same logic was missing from the /model mid-session switch path.
Repro: start a session on opencode-go with GLM-5 (or any chat_completions
model), then `/model minimax-m2.7`. switch_model() correctly sets
api_mode=anthropic_messages via opencode_model_api_mode(), but base_url
passes through as https://opencode.ai/zen/go/v1. The Anthropic SDK then
POSTs to https://opencode.ai/zen/go/v1/v1/messages, which returns the
OpenCode website 404 HTML page (title 'Not Found | opencode').
Same bug affects `/model claude-sonnet-4-6` on opencode-zen.
Verified upstream: POST /v1/messages returns clean JSON 401 with x-api-key
auth (route works), while POST /v1/v1/messages returns the exact HTML 404
users reported.
Fix mirrors runtime_provider.resolve_runtime_provider:
- hermes_cli/model_switch.py::switch_model() strips /v1 after the OpenCode
api_mode override when the resolved mode is anthropic_messages.
- run_agent.py::AIAgent.switch_model() applies the same strip as
defense-in-depth, so any direct caller can't reintroduce the double-/v1.
Tests: 9 new regression tests in tests/hermes_cli/test_model_switch_opencode_anthropic.py
covering minimax on opencode-go, claude on opencode-zen, chat_completions
(GLM/Kimi/Gemini) keeping /v1 intact, codex_responses (GPT) keeping /v1
intact, trailing-slash handling, and the agent-level defense-in-depth.
2026-04-16 19:41:41 -07:00
|
|
|
# OpenCode base URLs end with /v1 for OpenAI-compatible models, but the
|
|
|
|
|
# Anthropic SDK prepends its own /v1/messages to the base_url. Strip the
|
|
|
|
|
# trailing /v1 so the SDK constructs the correct path (e.g.
|
|
|
|
|
# https://opencode.ai/zen/go/v1/messages instead of .../v1/v1/messages).
|
|
|
|
|
# Mirrors the same logic in hermes_cli.runtime_provider.resolve_runtime_provider;
|
|
|
|
|
# without it, /model switches into an anthropic_messages-routed OpenCode
|
|
|
|
|
# model (e.g. `/model minimax-m2.7` on opencode-go, `/model claude-sonnet-4-6`
|
|
|
|
|
# on opencode-zen) hit a double /v1 and returned OpenCode's website 404 page.
|
|
|
|
|
if (
|
|
|
|
|
api_mode == "anthropic_messages"
|
|
|
|
|
and target_provider in {"opencode-zen", "opencode-go"}
|
|
|
|
|
and isinstance(base_url, str)
|
|
|
|
|
and base_url
|
|
|
|
|
):
|
|
|
|
|
base_url = re.sub(r"/v1/?$", "", base_url)
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# --- Get capabilities (legacy) ---
|
|
|
|
|
capabilities = get_model_capabilities(target_provider, new_model)
|
|
|
|
|
|
|
|
|
|
# --- Get full model info from models.dev ---
|
|
|
|
|
model_info = get_model_info(target_provider, new_model)
|
|
|
|
|
|
2026-04-05 18:41:03 -07:00
|
|
|
# --- Collect warnings ---
|
|
|
|
|
warnings: list[str] = []
|
|
|
|
|
if validation.get("message"):
|
|
|
|
|
warnings.append(validation["message"])
|
|
|
|
|
hermes_warn = _check_hermes_model_warning(new_model)
|
|
|
|
|
if hermes_warn:
|
|
|
|
|
warnings.append(hermes_warn)
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# --- Build result ---
|
2026-03-24 07:08:07 -07:00
|
|
|
return ModelSwitchResult(
|
|
|
|
|
success=True,
|
|
|
|
|
new_model=new_model,
|
|
|
|
|
target_provider=target_provider,
|
|
|
|
|
provider_changed=provider_changed,
|
|
|
|
|
api_key=api_key,
|
|
|
|
|
base_url=base_url,
|
2026-04-02 09:36:24 -07:00
|
|
|
api_mode=api_mode,
|
2026-04-05 18:41:03 -07:00
|
|
|
warning_message=" | ".join(warnings) if warnings else "",
|
2026-03-24 07:08:07 -07:00
|
|
|
provider_label=provider_label,
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
resolved_via_alias=resolved_alias,
|
|
|
|
|
capabilities=capabilities,
|
|
|
|
|
model_info=model_info,
|
|
|
|
|
is_global=is_global,
|
2026-03-24 07:08:07 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Authenticated providers listing (for /model no-args display)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def list_authenticated_providers(
|
|
|
|
|
current_provider: str = "",
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
current_base_url: str = "",
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
user_providers: dict = None,
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
custom_providers: list | None = None,
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
max_models: int = 8,
|
|
|
|
|
) -> List[dict]:
|
|
|
|
|
"""Detect which providers have credentials and list their curated models.
|
|
|
|
|
|
|
|
|
|
Uses the curated model lists from hermes_cli/models.py (OPENROUTER_MODELS,
|
|
|
|
|
_PROVIDER_MODELS) — NOT the full models.dev catalog. These are hand-picked
|
|
|
|
|
agentic models that work well as agent backends.
|
2026-03-24 07:08:07 -07:00
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
Returns a list of dicts, each with:
|
|
|
|
|
- slug: str — the --provider value to use
|
|
|
|
|
- name: str — display name
|
|
|
|
|
- is_current: bool
|
|
|
|
|
- is_user_defined: bool
|
|
|
|
|
- models: list[str] — curated model IDs (up to max_models)
|
|
|
|
|
- total_models: int — total curated count
|
|
|
|
|
- source: str — "built-in", "models.dev", "user-config"
|
|
|
|
|
|
|
|
|
|
Only includes providers that have API keys set or are user-defined endpoints.
|
2026-03-24 07:08:07 -07:00
|
|
|
"""
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
import os
|
|
|
|
|
from agent.models_dev import (
|
|
|
|
|
PROVIDER_TO_MODELS_DEV,
|
|
|
|
|
fetch_models_dev,
|
|
|
|
|
get_provider_info as _mdev_pinfo,
|
|
|
|
|
)
|
2026-04-09 11:13:11 -07:00
|
|
|
from hermes_cli.auth import PROVIDER_REGISTRY
|
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.
2026-04-22 17:33:42 -07:00
|
|
|
from hermes_cli.models import (
|
|
|
|
|
OPENROUTER_MODELS, _PROVIDER_MODELS,
|
2026-04-24 17:41:19 +08:00
|
|
|
_MODELS_DEV_PREFERRED, _merge_with_models_dev, provider_model_ids,
|
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.
2026-04-22 17:33:42 -07:00
|
|
|
)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
|
|
|
|
results: List[dict] = []
|
2026-04-15 17:34:15 -07:00
|
|
|
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)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
|
|
|
|
data = fetch_models_dev()
|
|
|
|
|
|
|
|
|
|
# Build curated model lists keyed by hermes provider ID
|
|
|
|
|
curated: dict[str, list[str]] = dict(_PROVIDER_MODELS)
|
|
|
|
|
curated["openrouter"] = [mid for mid, _ in OPENROUTER_MODELS]
|
|
|
|
|
# "nous" shares OpenRouter's curated list if not separately defined
|
|
|
|
|
if "nous" not in curated:
|
|
|
|
|
curated["nous"] = curated["openrouter"]
|
2026-04-16 20:03:31 +09:30
|
|
|
# Ollama Cloud uses dynamic discovery (no static curated list)
|
|
|
|
|
if "ollama-cloud" not in curated:
|
|
|
|
|
from hermes_cli.models import fetch_ollama_cloud_models
|
|
|
|
|
curated["ollama-cloud"] = fetch_ollama_cloud_models()
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
|
|
|
|
# --- 1. Check Hermes-mapped providers ---
|
|
|
|
|
for hermes_id, mdev_id in PROVIDER_TO_MODELS_DEV.items():
|
2026-04-15 17:34:15 -07:00
|
|
|
# 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:
|
|
|
|
|
continue
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
pdata = data.get(mdev_id)
|
|
|
|
|
if not isinstance(pdata, dict):
|
|
|
|
|
continue
|
|
|
|
|
|
2026-04-09 11:13:11 -07:00
|
|
|
# Prefer auth.py PROVIDER_REGISTRY for env var names — it's our
|
|
|
|
|
# source of truth. models.dev can have wrong mappings (e.g.
|
|
|
|
|
# minimax-cn → MINIMAX_API_KEY instead of MINIMAX_CN_API_KEY).
|
|
|
|
|
pconfig = PROVIDER_REGISTRY.get(hermes_id)
|
2026-04-10 11:14:02 -07:00
|
|
|
# Skip non-API-key auth providers here — they are handled in
|
|
|
|
|
# section 2 (HERMES_OVERLAYS) with proper auth store checking.
|
|
|
|
|
if pconfig and pconfig.auth_type != "api_key":
|
|
|
|
|
continue
|
2026-04-09 11:13:11 -07:00
|
|
|
if pconfig and pconfig.api_key_env_vars:
|
|
|
|
|
env_vars = list(pconfig.api_key_env_vars)
|
|
|
|
|
else:
|
|
|
|
|
env_vars = pdata.get("env", [])
|
|
|
|
|
if not isinstance(env_vars, list):
|
|
|
|
|
continue
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
|
|
|
|
# Check if any env var is set
|
|
|
|
|
has_creds = any(os.environ.get(ev) for ev in env_vars)
|
2026-04-12 21:11:49 +02:00
|
|
|
if not has_creds:
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.auth import _load_auth_store
|
|
|
|
|
store = _load_auth_store()
|
|
|
|
|
if store and hermes_id in store.get("credential_pool", {}):
|
|
|
|
|
has_creds = True
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
if not has_creds:
|
|
|
|
|
continue
|
|
|
|
|
|
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.
2026-04-22 17:33:42 -07:00
|
|
|
# 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.
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
model_ids = curated.get(hermes_id, [])
|
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.
2026-04-22 17:33:42 -07:00
|
|
|
if hermes_id in _MODELS_DEV_PREFERRED:
|
|
|
|
|
model_ids = _merge_with_models_dev(hermes_id, model_ids)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
total = len(model_ids)
|
|
|
|
|
top = model_ids[:max_models]
|
|
|
|
|
|
|
|
|
|
slug = hermes_id
|
|
|
|
|
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_user_defined": False,
|
|
|
|
|
"models": top,
|
|
|
|
|
"total_models": total,
|
|
|
|
|
"source": "built-in",
|
|
|
|
|
})
|
2026-04-15 17:34:15 -07:00
|
|
|
seen_slugs.add(slug.lower())
|
|
|
|
|
seen_mdev_ids.add(mdev_id)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
2026-04-10 00:25:57 +01:00
|
|
|
# --- 2. Check Hermes-only providers (nous, openai-codex, copilot, opencode-go) ---
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
from hermes_cli.providers import HERMES_OVERLAYS
|
2026-04-10 00:25:57 +01:00
|
|
|
from hermes_cli.auth import PROVIDER_REGISTRY as _auth_registry
|
2026-04-10 14:46:57 -07:00
|
|
|
|
|
|
|
|
# Build reverse mapping: models.dev ID → Hermes provider ID.
|
|
|
|
|
# HERMES_OVERLAYS keys may be models.dev IDs (e.g. "github-copilot")
|
|
|
|
|
# while _PROVIDER_MODELS and config.yaml use Hermes IDs ("copilot").
|
|
|
|
|
_mdev_to_hermes = {v: k for k, v in PROVIDER_TO_MODELS_DEV.items()}
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
for pid, overlay in HERMES_OVERLAYS.items():
|
2026-04-15 17:34:15 -07:00
|
|
|
if pid.lower() in seen_slugs:
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
continue
|
2026-04-10 14:46:57 -07:00
|
|
|
|
|
|
|
|
# Resolve Hermes slug — e.g. "github-copilot" → "copilot"
|
|
|
|
|
hermes_slug = _mdev_to_hermes.get(pid, pid)
|
2026-04-15 17:34:15 -07:00
|
|
|
if hermes_slug.lower() in seen_slugs:
|
2026-04-10 14:46:57 -07:00
|
|
|
continue
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# Check if credentials exist
|
|
|
|
|
has_creds = False
|
|
|
|
|
if overlay.extra_env_vars:
|
|
|
|
|
has_creds = any(os.environ.get(ev) for ev in overlay.extra_env_vars)
|
2026-04-10 00:25:57 +01:00
|
|
|
# Also check api_key_env_vars from PROVIDER_REGISTRY for api_key auth_type
|
|
|
|
|
if not has_creds and overlay.auth_type == "api_key":
|
2026-04-10 14:46:57 -07:00
|
|
|
for _key in (pid, hermes_slug):
|
|
|
|
|
pcfg = _auth_registry.get(_key)
|
|
|
|
|
if pcfg and pcfg.api_key_env_vars:
|
|
|
|
|
if any(os.environ.get(ev) for ev in pcfg.api_key_env_vars):
|
|
|
|
|
has_creds = True
|
|
|
|
|
break
|
2026-04-12 00:33:42 -07:00
|
|
|
# Check auth store and credential pool for non-env-var credentials.
|
|
|
|
|
# This applies to OAuth providers AND api_key providers that also
|
|
|
|
|
# support OAuth (e.g. anthropic supports both API key and Claude Code
|
|
|
|
|
# OAuth via external credential files).
|
|
|
|
|
if not has_creds:
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
try:
|
2026-04-07 22:23:28 -07:00
|
|
|
from hermes_cli.auth import _load_auth_store
|
|
|
|
|
store = _load_auth_store()
|
2026-04-10 14:46:57 -07:00
|
|
|
providers_store = store.get("providers", {})
|
|
|
|
|
pool_store = store.get("credential_pool", {})
|
|
|
|
|
if store and (
|
|
|
|
|
pid in providers_store or hermes_slug in providers_store
|
|
|
|
|
or pid in pool_store or hermes_slug in pool_store
|
|
|
|
|
):
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
has_creds = True
|
2026-04-07 22:23:28 -07:00
|
|
|
except Exception as exc:
|
|
|
|
|
logger.debug("Auth store check failed for %s: %s", pid, exc)
|
2026-04-12 00:33:42 -07:00
|
|
|
# Fallback: check the credential pool with full auto-seeding.
|
|
|
|
|
# This catches credentials that exist in external stores (e.g.
|
|
|
|
|
# Codex CLI ~/.codex/auth.json) which _seed_from_singletons()
|
|
|
|
|
# imports on demand but aren't in the raw auth.json yet.
|
|
|
|
|
if not has_creds:
|
|
|
|
|
try:
|
|
|
|
|
from agent.credential_pool import load_pool
|
|
|
|
|
pool = load_pool(hermes_slug)
|
|
|
|
|
if pool.has_credentials():
|
|
|
|
|
has_creds = True
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.debug("Credential pool check failed for %s: %s", hermes_slug, exc)
|
|
|
|
|
# Fallback: check external credential files directly.
|
|
|
|
|
# The credential pool gates anthropic behind
|
|
|
|
|
# is_provider_explicitly_configured() to prevent auxiliary tasks
|
|
|
|
|
# from silently consuming Claude Code tokens (PR #4210).
|
|
|
|
|
# But the /model picker is discovery-oriented — we WANT to show
|
|
|
|
|
# providers the user can switch to, even if they aren't currently
|
|
|
|
|
# configured.
|
|
|
|
|
if not has_creds and hermes_slug == "anthropic":
|
|
|
|
|
try:
|
|
|
|
|
from agent.anthropic_adapter import (
|
|
|
|
|
read_claude_code_credentials,
|
|
|
|
|
read_hermes_oauth_credentials,
|
|
|
|
|
)
|
|
|
|
|
hermes_creds = read_hermes_oauth_credentials()
|
|
|
|
|
cc_creds = read_claude_code_credentials()
|
|
|
|
|
if (hermes_creds and hermes_creds.get("accessToken")) or \
|
|
|
|
|
(cc_creds and cc_creds.get("accessToken")):
|
|
|
|
|
has_creds = True
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.debug("Anthropic external creds check failed: %s", exc)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
if not has_creds:
|
|
|
|
|
continue
|
|
|
|
|
|
2026-04-24 17:41:19 +08:00
|
|
|
if hermes_slug in {"copilot", "copilot-acp"}:
|
|
|
|
|
model_ids = provider_model_ids(hermes_slug)
|
|
|
|
|
else:
|
|
|
|
|
# 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)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
total = len(model_ids)
|
|
|
|
|
top = model_ids[:max_models]
|
|
|
|
|
|
|
|
|
|
results.append({
|
2026-04-10 14:46:57 -07:00
|
|
|
"slug": hermes_slug,
|
|
|
|
|
"name": get_label(hermes_slug),
|
|
|
|
|
"is_current": hermes_slug == current_provider or pid == current_provider,
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
"is_user_defined": False,
|
|
|
|
|
"models": top,
|
|
|
|
|
"total_models": total,
|
|
|
|
|
"source": "hermes",
|
|
|
|
|
})
|
2026-04-15 17:34:15 -07:00
|
|
|
seen_slugs.add(pid.lower())
|
|
|
|
|
seen_slugs.add(hermes_slug.lower())
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
2026-04-13 14:59:50 -07:00
|
|
|
# --- 2b. Cross-check canonical provider list ---
|
|
|
|
|
# Catches providers that are in CANONICAL_PROVIDERS but weren't found
|
|
|
|
|
# in PROVIDER_TO_MODELS_DEV or HERMES_OVERLAYS (keeps /model in sync
|
|
|
|
|
# with `hermes model`).
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.models import CANONICAL_PROVIDERS as _canon_provs
|
|
|
|
|
except ImportError:
|
|
|
|
|
_canon_provs = []
|
|
|
|
|
|
|
|
|
|
for _cp in _canon_provs:
|
2026-04-15 17:34:15 -07:00
|
|
|
if _cp.slug.lower() in seen_slugs:
|
2026-04-13 14:59:50 -07:00
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Check credentials via PROVIDER_REGISTRY (auth.py)
|
|
|
|
|
_cp_config = _auth_registry.get(_cp.slug)
|
|
|
|
|
_cp_has_creds = False
|
|
|
|
|
if _cp_config and _cp_config.api_key_env_vars:
|
|
|
|
|
_cp_has_creds = any(os.environ.get(ev) for ev in _cp_config.api_key_env_vars)
|
|
|
|
|
# Also check auth store and credential pool
|
|
|
|
|
if not _cp_has_creds:
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.auth import _load_auth_store
|
|
|
|
|
_cp_store = _load_auth_store()
|
|
|
|
|
_cp_providers_store = _cp_store.get("providers", {})
|
|
|
|
|
_cp_pool_store = _cp_store.get("credential_pool", {})
|
|
|
|
|
if _cp_store and (
|
|
|
|
|
_cp.slug in _cp_providers_store
|
|
|
|
|
or _cp.slug in _cp_pool_store
|
|
|
|
|
):
|
|
|
|
|
_cp_has_creds = True
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
if not _cp_has_creds:
|
|
|
|
|
try:
|
|
|
|
|
from agent.credential_pool import load_pool
|
|
|
|
|
_cp_pool = load_pool(_cp.slug)
|
|
|
|
|
if _cp_pool.has_credentials():
|
|
|
|
|
_cp_has_creds = True
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
if not _cp_has_creds:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
_cp_model_ids = curated.get(_cp.slug, [])
|
|
|
|
|
_cp_total = len(_cp_model_ids)
|
|
|
|
|
_cp_top = _cp_model_ids[:max_models]
|
|
|
|
|
|
|
|
|
|
results.append({
|
|
|
|
|
"slug": _cp.slug,
|
|
|
|
|
"name": _cp.label,
|
|
|
|
|
"is_current": _cp.slug == current_provider,
|
|
|
|
|
"is_user_defined": False,
|
|
|
|
|
"models": _cp_top,
|
|
|
|
|
"total_models": _cp_total,
|
|
|
|
|
"source": "canonical",
|
|
|
|
|
})
|
2026-04-15 17:34:15 -07:00
|
|
|
seen_slugs.add(_cp.slug.lower())
|
2026-04-13 14:59:50 -07:00
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# --- 3. User-defined endpoints from config ---
|
2026-04-19 22:04:19 -07:00
|
|
|
# Track (name, base_url) of what section 3 emits so section 4 can skip
|
|
|
|
|
# any overlapping ``custom_providers:`` entries. Callers typically pass
|
|
|
|
|
# both (gateway/CLI invoke ``get_compatible_custom_providers()`` which
|
|
|
|
|
# merges ``providers:`` into the list) — without this, the same endpoint
|
|
|
|
|
# produces two picker rows: one bare-slug ("openrouter") from section 3
|
|
|
|
|
# and one "custom:openrouter" from section 4, both labelled identically.
|
|
|
|
|
_section3_emitted_pairs: set = set()
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
if user_providers and isinstance(user_providers, dict):
|
|
|
|
|
for ep_name, ep_cfg in user_providers.items():
|
|
|
|
|
if not isinstance(ep_cfg, dict):
|
|
|
|
|
continue
|
2026-04-19 05:44:44 -07:00
|
|
|
# Skip if this slug was already emitted (e.g. canonical provider
|
|
|
|
|
# with the same name) or will be picked up by section 4.
|
|
|
|
|
if ep_name.lower() in seen_slugs:
|
|
|
|
|
continue
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
display_name = ep_cfg.get("name", "") or ep_name
|
2026-04-19 05:44:44 -07:00
|
|
|
# ``base_url`` is Hermes's canonical write key (matches
|
|
|
|
|
# custom_providers and _save_custom_provider); ``api`` / ``url``
|
|
|
|
|
# remain as fallbacks for hand-edited / legacy configs.
|
|
|
|
|
api_url = (
|
|
|
|
|
ep_cfg.get("base_url", "")
|
|
|
|
|
or ep_cfg.get("api", "")
|
|
|
|
|
or ep_cfg.get("url", "")
|
|
|
|
|
or ""
|
|
|
|
|
)
|
|
|
|
|
# ``default_model`` is the legacy key; ``model`` matches what
|
|
|
|
|
# custom_providers entries use, so accept either.
|
|
|
|
|
default_model = ep_cfg.get("default_model", "") or ep_cfg.get("model", "")
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
2026-04-13 15:53:21 +10:00
|
|
|
# Build models list from both default_model and full models array
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
models_list = []
|
|
|
|
|
if default_model:
|
|
|
|
|
models_list.append(default_model)
|
fix(model_switch): enumerate dict-format models in /model picker
list_authenticated_providers() builds /model picker rows for CLI, TUI and
gateway flows, but fails to enumerate custom provider models stored in
dict form:
- custom_providers[] entries surface only the singular `model:` field,
hiding every other model in the `models:` dict.
- providers: dict entries with dict-format `models:` are silently dropped
and render as `(0 models)`.
Hermes's own writer (main.py::_save_custom_provider) persists configured
models as a dict keyed by model id, and most downstream readers
(agent/models_dev.py, gateway/run.py, run_agent.py, hermes_cli/config.py)
already consume that dict format. The /model picker was the only stale
path.
Add a dict branch in both sections of list_authenticated_providers(),
preferring dict (canonical) and keeping the list branch as fallback for
hand-edited / legacy configs. Dedup against the already-added default
model so nothing duplicates when the default is also a dict key.
Six new regression tests in tests/hermes_cli/ cover: dict models with a
default, dict models without a default, and default dedup against a
matching dict key.
Fixes #11677
Fixes #9148
Related: #11017
2026-04-19 18:03:21 +08:00
|
|
|
# Also include the full models list from config.
|
|
|
|
|
# Hermes writes ``models:`` as a dict keyed by model id
|
|
|
|
|
# (see hermes_cli/main.py::_save_custom_provider); older
|
|
|
|
|
# configs or hand-edited files may still use a list.
|
2026-04-13 15:53:21 +10:00
|
|
|
cfg_models = ep_cfg.get("models", [])
|
fix(model_switch): enumerate dict-format models in /model picker
list_authenticated_providers() builds /model picker rows for CLI, TUI and
gateway flows, but fails to enumerate custom provider models stored in
dict form:
- custom_providers[] entries surface only the singular `model:` field,
hiding every other model in the `models:` dict.
- providers: dict entries with dict-format `models:` are silently dropped
and render as `(0 models)`.
Hermes's own writer (main.py::_save_custom_provider) persists configured
models as a dict keyed by model id, and most downstream readers
(agent/models_dev.py, gateway/run.py, run_agent.py, hermes_cli/config.py)
already consume that dict format. The /model picker was the only stale
path.
Add a dict branch in both sections of list_authenticated_providers(),
preferring dict (canonical) and keeping the list branch as fallback for
hand-edited / legacy configs. Dedup against the already-added default
model so nothing duplicates when the default is also a dict key.
Six new regression tests in tests/hermes_cli/ cover: dict models with a
default, dict models without a default, and default dedup against a
matching dict key.
Fixes #11677
Fixes #9148
Related: #11017
2026-04-19 18:03:21 +08:00
|
|
|
if isinstance(cfg_models, dict):
|
|
|
|
|
for m in cfg_models:
|
|
|
|
|
if m and m not in models_list:
|
|
|
|
|
models_list.append(m)
|
|
|
|
|
elif isinstance(cfg_models, list):
|
2026-04-13 15:53:21 +10:00
|
|
|
for m in cfg_models:
|
|
|
|
|
if m and m not in models_list:
|
|
|
|
|
models_list.append(m)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
2026-04-24 05:41:01 +08:00
|
|
|
# Official OpenAI API rows in providers: often have base_url but no
|
|
|
|
|
# explicit models: dict — avoid a misleading zero count in /model.
|
|
|
|
|
if not models_list:
|
|
|
|
|
url_lower = str(api_url).strip().lower()
|
|
|
|
|
if "api.openai.com" in url_lower:
|
|
|
|
|
fb = curated.get("openai") or []
|
|
|
|
|
if fb:
|
|
|
|
|
models_list = list(fb)
|
|
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# Try to probe /v1/models if URL is set (but don't block on it)
|
|
|
|
|
# For now just show what we know from config
|
|
|
|
|
results.append({
|
|
|
|
|
"slug": ep_name,
|
|
|
|
|
"name": display_name,
|
|
|
|
|
"is_current": ep_name == current_provider,
|
|
|
|
|
"is_user_defined": True,
|
|
|
|
|
"models": models_list,
|
|
|
|
|
"total_models": len(models_list) if models_list else 0,
|
|
|
|
|
"source": "user-config",
|
|
|
|
|
"api_url": api_url,
|
|
|
|
|
})
|
2026-04-19 05:44:44 -07:00
|
|
|
seen_slugs.add(ep_name.lower())
|
2026-04-20 12:06:08 -07:00
|
|
|
seen_slugs.add(custom_provider_slug(display_name).lower())
|
2026-04-19 22:04:19 -07:00
|
|
|
_pair = (
|
|
|
|
|
str(display_name).strip().lower(),
|
|
|
|
|
str(api_url).strip().rstrip("/").lower(),
|
|
|
|
|
)
|
|
|
|
|
if _pair[0] and _pair[1]:
|
|
|
|
|
_section3_emitted_pairs.add(_pair)
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
# --- 4. Saved custom providers from config ---
|
feat(model-picker): group custom_providers by name into a single row per provider
The /model picker currently renders one row per ``custom_providers``
entry. When several entries share the same provider name (e.g. four
``ollama-cloud`` entries for ``qwen3-coder``, ``glm-5.1``, ``kimi-k2``,
``minimax-m2.7``), users see four separate "Ollama Cloud" rows in the
picker, which is confusing UX — there is only one Ollama Cloud
provider, so there should be one row containing four models.
This PR groups ``custom_providers`` entries that share the same provider
name into a single picker row while keeping entries with distinct names
as separate rows. So:
* Four entries named ``Ollama Cloud`` → one "Ollama Cloud" row with
four models inside.
* One entry named ``Ollama Cloud`` and one named ``Moonshot`` → two
separate rows, one model each.
Implementation
--------------
Replaces the single-pass loop in ``list_authenticated_providers()`` with
a two-pass approach:
1. First pass: build an ``OrderedDict`` keyed by ``custom_provider_slug(name)``,
accumulating ``models`` per group while preserving discovery order.
2. Second pass: iterate the groups and append one result row per group,
skipping any slug that already appeared in an earlier provider source
(the existing ``seen_slugs`` guard).
Insertion order is preserved via ``OrderedDict``, so providers and
their models still appear in the order the user listed them in
``custom_providers``. No new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:19:32 +00:00
|
|
|
# Each ``custom_providers`` entry represents one model under a named
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
# provider. Entries sharing the same endpoint (``base_url`` + ``api_key``)
|
|
|
|
|
# are grouped into a single picker row, so e.g. four Ollama entries
|
|
|
|
|
# pointing at ``http://localhost:11434/v1`` with per-model display names
|
|
|
|
|
# ("Ollama — GLM 5.1", "Ollama — Qwen3-coder", ...) appear as one
|
|
|
|
|
# "Ollama" row with four models inside instead of four near-duplicates
|
|
|
|
|
# that differ only by suffix. Entries with distinct endpoints still
|
|
|
|
|
# produce separate rows.
|
|
|
|
|
#
|
|
|
|
|
# When the grouped endpoint matches ``current_base_url`` the group's
|
|
|
|
|
# slug becomes ``current_provider`` so that selecting a model from the
|
|
|
|
|
# picker flows back through the runtime provider that already holds
|
|
|
|
|
# valid credentials — no re-resolution needed.
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
if custom_providers and isinstance(custom_providers, list):
|
feat(model-picker): group custom_providers by name into a single row per provider
The /model picker currently renders one row per ``custom_providers``
entry. When several entries share the same provider name (e.g. four
``ollama-cloud`` entries for ``qwen3-coder``, ``glm-5.1``, ``kimi-k2``,
``minimax-m2.7``), users see four separate "Ollama Cloud" rows in the
picker, which is confusing UX — there is only one Ollama Cloud
provider, so there should be one row containing four models.
This PR groups ``custom_providers`` entries that share the same provider
name into a single picker row while keeping entries with distinct names
as separate rows. So:
* Four entries named ``Ollama Cloud`` → one "Ollama Cloud" row with
four models inside.
* One entry named ``Ollama Cloud`` and one named ``Moonshot`` → two
separate rows, one model each.
Implementation
--------------
Replaces the single-pass loop in ``list_authenticated_providers()`` with
a two-pass approach:
1. First pass: build an ``OrderedDict`` keyed by ``custom_provider_slug(name)``,
accumulating ``models`` per group while preserving discovery order.
2. Second pass: iterate the groups and append one result row per group,
skipping any slug that already appeared in an earlier provider source
(the existing ``seen_slugs`` guard).
Insertion order is preserved via ``OrderedDict``, so providers and
their models still appear in the order the user listed them in
``custom_providers``. No new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:19:32 +00:00
|
|
|
from collections import OrderedDict
|
|
|
|
|
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
# Key by (base_url, api_key) instead of slug: names frequently
|
|
|
|
|
# differ per model ("Ollama — X") while the endpoint stays the
|
|
|
|
|
# same. Slug-based grouping left them as separate rows.
|
|
|
|
|
groups: "OrderedDict[tuple, dict]" = OrderedDict()
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
for entry in custom_providers:
|
|
|
|
|
if not isinstance(entry, dict):
|
|
|
|
|
continue
|
|
|
|
|
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
raw_name = (entry.get("name") or "").strip()
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
api_url = (
|
|
|
|
|
entry.get("base_url", "")
|
|
|
|
|
or entry.get("url", "")
|
|
|
|
|
or entry.get("api", "")
|
|
|
|
|
or ""
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
).strip().rstrip("/")
|
|
|
|
|
if not raw_name or not api_url:
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
continue
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
api_key = (entry.get("api_key") or "").strip()
|
|
|
|
|
|
|
|
|
|
group_key = (api_url, api_key)
|
|
|
|
|
if group_key not in groups:
|
|
|
|
|
# Strip per-model suffix so "Ollama — GLM 5.1" becomes
|
|
|
|
|
# "Ollama" for the grouped row. Em dash is the convention
|
|
|
|
|
# Hermes's own writer uses; a hyphen variant is accepted
|
|
|
|
|
# for hand-edited configs.
|
|
|
|
|
display_name = raw_name
|
|
|
|
|
for sep in ("—", " - "):
|
|
|
|
|
if sep in display_name:
|
|
|
|
|
display_name = display_name.split(sep)[0].strip()
|
|
|
|
|
break
|
|
|
|
|
if not display_name:
|
|
|
|
|
display_name = raw_name
|
|
|
|
|
# If this endpoint matches the currently active one, use
|
|
|
|
|
# ``current_provider`` as the slug so picker-driven switches
|
|
|
|
|
# route through the live credential pipeline.
|
|
|
|
|
if (
|
|
|
|
|
current_base_url
|
|
|
|
|
and api_url == current_base_url.strip().rstrip("/")
|
|
|
|
|
):
|
|
|
|
|
slug = current_provider or custom_provider_slug(display_name)
|
|
|
|
|
else:
|
|
|
|
|
slug = custom_provider_slug(display_name)
|
|
|
|
|
groups[group_key] = {
|
|
|
|
|
"slug": slug,
|
feat(model-picker): group custom_providers by name into a single row per provider
The /model picker currently renders one row per ``custom_providers``
entry. When several entries share the same provider name (e.g. four
``ollama-cloud`` entries for ``qwen3-coder``, ``glm-5.1``, ``kimi-k2``,
``minimax-m2.7``), users see four separate "Ollama Cloud" rows in the
picker, which is confusing UX — there is only one Ollama Cloud
provider, so there should be one row containing four models.
This PR groups ``custom_providers`` entries that share the same provider
name into a single picker row while keeping entries with distinct names
as separate rows. So:
* Four entries named ``Ollama Cloud`` → one "Ollama Cloud" row with
four models inside.
* One entry named ``Ollama Cloud`` and one named ``Moonshot`` → two
separate rows, one model each.
Implementation
--------------
Replaces the single-pass loop in ``list_authenticated_providers()`` with
a two-pass approach:
1. First pass: build an ``OrderedDict`` keyed by ``custom_provider_slug(name)``,
accumulating ``models`` per group while preserving discovery order.
2. Second pass: iterate the groups and append one result row per group,
skipping any slug that already appeared in an earlier provider source
(the existing ``seen_slugs`` guard).
Insertion order is preserved via ``OrderedDict``, so providers and
their models still appear in the order the user listed them in
``custom_providers``. No new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:19:32 +00:00
|
|
|
"name": display_name,
|
|
|
|
|
"api_url": api_url,
|
|
|
|
|
"models": [],
|
|
|
|
|
}
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
|
fix(model_switch): enumerate dict-format models in /model picker
list_authenticated_providers() builds /model picker rows for CLI, TUI and
gateway flows, but fails to enumerate custom provider models stored in
dict form:
- custom_providers[] entries surface only the singular `model:` field,
hiding every other model in the `models:` dict.
- providers: dict entries with dict-format `models:` are silently dropped
and render as `(0 models)`.
Hermes's own writer (main.py::_save_custom_provider) persists configured
models as a dict keyed by model id, and most downstream readers
(agent/models_dev.py, gateway/run.py, run_agent.py, hermes_cli/config.py)
already consume that dict format. The /model picker was the only stale
path.
Add a dict branch in both sections of list_authenticated_providers(),
preferring dict (canonical) and keeping the list branch as fallback for
hand-edited / legacy configs. Dedup against the already-added default
model so nothing duplicates when the default is also a dict key.
Six new regression tests in tests/hermes_cli/ cover: dict models with a
default, dict models without a default, and default dedup against a
matching dict key.
Fixes #11677
Fixes #9148
Related: #11017
2026-04-19 18:03:21 +08:00
|
|
|
# The singular ``model:`` field only holds the currently
|
|
|
|
|
# active model. Hermes's own writer (main.py::_save_custom_provider)
|
|
|
|
|
# stores every configured model as a dict under ``models:``;
|
|
|
|
|
# downstream readers (agent/models_dev.py, gateway/run.py,
|
|
|
|
|
# run_agent.py, hermes_cli/config.py) already consume that dict.
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
default_model = (entry.get("model") or "").strip()
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
if default_model and default_model not in groups[group_key]["models"]:
|
|
|
|
|
groups[group_key]["models"].append(default_model)
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
|
fix(model_switch): enumerate dict-format models in /model picker
list_authenticated_providers() builds /model picker rows for CLI, TUI and
gateway flows, but fails to enumerate custom provider models stored in
dict form:
- custom_providers[] entries surface only the singular `model:` field,
hiding every other model in the `models:` dict.
- providers: dict entries with dict-format `models:` are silently dropped
and render as `(0 models)`.
Hermes's own writer (main.py::_save_custom_provider) persists configured
models as a dict keyed by model id, and most downstream readers
(agent/models_dev.py, gateway/run.py, run_agent.py, hermes_cli/config.py)
already consume that dict format. The /model picker was the only stale
path.
Add a dict branch in both sections of list_authenticated_providers(),
preferring dict (canonical) and keeping the list branch as fallback for
hand-edited / legacy configs. Dedup against the already-added default
model so nothing duplicates when the default is also a dict key.
Six new regression tests in tests/hermes_cli/ cover: dict models with a
default, dict models without a default, and default dedup against a
matching dict key.
Fixes #11677
Fixes #9148
Related: #11017
2026-04-19 18:03:21 +08:00
|
|
|
cfg_models = entry.get("models", {})
|
|
|
|
|
if isinstance(cfg_models, dict):
|
|
|
|
|
for m in cfg_models:
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
if m and m not in groups[group_key]["models"]:
|
|
|
|
|
groups[group_key]["models"].append(m)
|
fix(model_switch): enumerate dict-format models in /model picker
list_authenticated_providers() builds /model picker rows for CLI, TUI and
gateway flows, but fails to enumerate custom provider models stored in
dict form:
- custom_providers[] entries surface only the singular `model:` field,
hiding every other model in the `models:` dict.
- providers: dict entries with dict-format `models:` are silently dropped
and render as `(0 models)`.
Hermes's own writer (main.py::_save_custom_provider) persists configured
models as a dict keyed by model id, and most downstream readers
(agent/models_dev.py, gateway/run.py, run_agent.py, hermes_cli/config.py)
already consume that dict format. The /model picker was the only stale
path.
Add a dict branch in both sections of list_authenticated_providers(),
preferring dict (canonical) and keeping the list branch as fallback for
hand-edited / legacy configs. Dedup against the already-added default
model so nothing duplicates when the default is also a dict key.
Six new regression tests in tests/hermes_cli/ cover: dict models with a
default, dict models without a default, and default dedup against a
matching dict key.
Fixes #11677
Fixes #9148
Related: #11017
2026-04-19 18:03:21 +08:00
|
|
|
elif isinstance(cfg_models, list):
|
|
|
|
|
for m in cfg_models:
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
if m and m not in groups[group_key]["models"]:
|
|
|
|
|
groups[group_key]["models"].append(m)
|
|
|
|
|
|
|
|
|
|
_section4_emitted_slugs: set = set()
|
|
|
|
|
for grp in groups.values():
|
|
|
|
|
slug = grp["slug"]
|
|
|
|
|
# If the slug is already claimed by a built-in / overlay /
|
|
|
|
|
# user-provider row (sections 1-3), skip this custom group
|
|
|
|
|
# to avoid shadowing a real provider.
|
|
|
|
|
if slug.lower() in seen_slugs and slug.lower() not in _section4_emitted_slugs:
|
feat(model-picker): group custom_providers by name into a single row per provider
The /model picker currently renders one row per ``custom_providers``
entry. When several entries share the same provider name (e.g. four
``ollama-cloud`` entries for ``qwen3-coder``, ``glm-5.1``, ``kimi-k2``,
``minimax-m2.7``), users see four separate "Ollama Cloud" rows in the
picker, which is confusing UX — there is only one Ollama Cloud
provider, so there should be one row containing four models.
This PR groups ``custom_providers`` entries that share the same provider
name into a single picker row while keeping entries with distinct names
as separate rows. So:
* Four entries named ``Ollama Cloud`` → one "Ollama Cloud" row with
four models inside.
* One entry named ``Ollama Cloud`` and one named ``Moonshot`` → two
separate rows, one model each.
Implementation
--------------
Replaces the single-pass loop in ``list_authenticated_providers()`` with
a two-pass approach:
1. First pass: build an ``OrderedDict`` keyed by ``custom_provider_slug(name)``,
accumulating ``models`` per group while preserving discovery order.
2. Second pass: iterate the groups and append one result row per group,
skipping any slug that already appeared in an earlier provider source
(the existing ``seen_slugs`` guard).
Insertion order is preserved via ``OrderedDict``, so providers and
their models still appear in the order the user listed them in
``custom_providers``. No new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:19:32 +00:00
|
|
|
continue
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
# If a prior section-4 group already used this slug (two custom
|
|
|
|
|
# endpoints with the same cleaned name — e.g. two OpenAI-
|
|
|
|
|
# compatible gateways named identically with different keys),
|
|
|
|
|
# append a counter so both rows stay visible in the picker.
|
|
|
|
|
if slug.lower() in _section4_emitted_slugs:
|
|
|
|
|
base_slug = slug
|
|
|
|
|
n = 2
|
|
|
|
|
while f"{base_slug}-{n}".lower() in seen_slugs:
|
|
|
|
|
n += 1
|
|
|
|
|
slug = f"{base_slug}-{n}"
|
|
|
|
|
grp["slug"] = slug
|
2026-04-19 22:04:19 -07:00
|
|
|
# Skip if section 3 already emitted this endpoint under its
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
# ``providers:`` dict key — matches on (display_name, base_url).
|
|
|
|
|
# Prevents two picker rows labelled identically when callers
|
|
|
|
|
# pass both ``user_providers`` and a compatibility-merged
|
|
|
|
|
# ``custom_providers`` list.
|
2026-04-19 22:04:19 -07:00
|
|
|
_pair_key = (
|
|
|
|
|
str(grp["name"]).strip().lower(),
|
|
|
|
|
str(grp["api_url"]).strip().rstrip("/").lower(),
|
|
|
|
|
)
|
|
|
|
|
if _pair_key[0] and _pair_key[1] and _pair_key in _section3_emitted_pairs:
|
|
|
|
|
continue
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
results.append({
|
|
|
|
|
"slug": slug,
|
feat(model-picker): group custom_providers by name into a single row per provider
The /model picker currently renders one row per ``custom_providers``
entry. When several entries share the same provider name (e.g. four
``ollama-cloud`` entries for ``qwen3-coder``, ``glm-5.1``, ``kimi-k2``,
``minimax-m2.7``), users see four separate "Ollama Cloud" rows in the
picker, which is confusing UX — there is only one Ollama Cloud
provider, so there should be one row containing four models.
This PR groups ``custom_providers`` entries that share the same provider
name into a single picker row while keeping entries with distinct names
as separate rows. So:
* Four entries named ``Ollama Cloud`` → one "Ollama Cloud" row with
four models inside.
* One entry named ``Ollama Cloud`` and one named ``Moonshot`` → two
separate rows, one model each.
Implementation
--------------
Replaces the single-pass loop in ``list_authenticated_providers()`` with
a two-pass approach:
1. First pass: build an ``OrderedDict`` keyed by ``custom_provider_slug(name)``,
accumulating ``models`` per group while preserving discovery order.
2. Second pass: iterate the groups and append one result row per group,
skipping any slug that already appeared in an earlier provider source
(the existing ``seen_slugs`` guard).
Insertion order is preserved via ``OrderedDict``, so providers and
their models still appear in the order the user listed them in
``custom_providers``. No new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:19:32 +00:00
|
|
|
"name": grp["name"],
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
"is_current": slug == current_provider,
|
|
|
|
|
"is_user_defined": True,
|
feat(model-picker): group custom_providers by name into a single row per provider
The /model picker currently renders one row per ``custom_providers``
entry. When several entries share the same provider name (e.g. four
``ollama-cloud`` entries for ``qwen3-coder``, ``glm-5.1``, ``kimi-k2``,
``minimax-m2.7``), users see four separate "Ollama Cloud" rows in the
picker, which is confusing UX — there is only one Ollama Cloud
provider, so there should be one row containing four models.
This PR groups ``custom_providers`` entries that share the same provider
name into a single picker row while keeping entries with distinct names
as separate rows. So:
* Four entries named ``Ollama Cloud`` → one "Ollama Cloud" row with
four models inside.
* One entry named ``Ollama Cloud`` and one named ``Moonshot`` → two
separate rows, one model each.
Implementation
--------------
Replaces the single-pass loop in ``list_authenticated_providers()`` with
a two-pass approach:
1. First pass: build an ``OrderedDict`` keyed by ``custom_provider_slug(name)``,
accumulating ``models`` per group while preserving discovery order.
2. Second pass: iterate the groups and append one result row per group,
skipping any slug that already appeared in an earlier provider source
(the existing ``seen_slugs`` guard).
Insertion order is preserved via ``OrderedDict``, so providers and
their models still appear in the order the user listed them in
``custom_providers``. No new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:19:32 +00:00
|
|
|
"models": grp["models"],
|
|
|
|
|
"total_models": len(grp["models"]),
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
"source": "user-config",
|
feat(model-picker): group custom_providers by name into a single row per provider
The /model picker currently renders one row per ``custom_providers``
entry. When several entries share the same provider name (e.g. four
``ollama-cloud`` entries for ``qwen3-coder``, ``glm-5.1``, ``kimi-k2``,
``minimax-m2.7``), users see four separate "Ollama Cloud" rows in the
picker, which is confusing UX — there is only one Ollama Cloud
provider, so there should be one row containing four models.
This PR groups ``custom_providers`` entries that share the same provider
name into a single picker row while keeping entries with distinct names
as separate rows. So:
* Four entries named ``Ollama Cloud`` → one "Ollama Cloud" row with
four models inside.
* One entry named ``Ollama Cloud`` and one named ``Moonshot`` → two
separate rows, one model each.
Implementation
--------------
Replaces the single-pass loop in ``list_authenticated_providers()`` with
a two-pass approach:
1. First pass: build an ``OrderedDict`` keyed by ``custom_provider_slug(name)``,
accumulating ``models`` per group while preserving discovery order.
2. Second pass: iterate the groups and append one result row per group,
skipping any slug that already appeared in an earlier provider source
(the existing ``seen_slugs`` guard).
Insertion order is preserved via ``OrderedDict``, so providers and
their models still appear in the order the user listed them in
``custom_providers``. No new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:19:32 +00:00
|
|
|
"api_url": grp["api_url"],
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
})
|
2026-04-15 17:34:15 -07:00
|
|
|
seen_slugs.add(slug.lower())
|
fix(model_switch): group custom_providers by endpoint in /model picker (#9210)
Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.
Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
set to current_provider so picker-driven switches route through the
live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
"Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
colliding on cleaned name) are disambiguated with a numeric suffix
(custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.
Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).
Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.
Closes #9210
2026-04-23 03:05:12 -07:00
|
|
|
_section4_emitted_slugs.add(slug.lower())
|
fix: include custom_providers in /model command listings and resolution
Custom providers defined in config.yaml under were
completely invisible to the /model command in both gateway (Telegram,
Discord, etc.) and CLI. The provider listing skipped them and explicit
switching via --provider failed with "Unknown provider".
Root cause: gateway/run.py, cli.py, and model_switch.py only read the
dict from config, ignoring entirely.
Changes:
- providers.py: add resolve_custom_provider() and extend
resolve_provider_full() to check custom_providers after user_providers
- model_switch.py: propagate custom_providers through switch_model(),
list_authenticated_providers(), and get_authenticated_provider_slugs();
add custom provider section to provider listings
- gateway/run.py: read custom_providers from config, pass to all
model-switch calls
- cli.py: hoist config loading, pass custom_providers to listing and
switch calls
Tests: 4 new regression tests covering listing, resolution, and gateway
command handler. All 71 tests pass.
2026-04-09 22:33:34 +02:00
|
|
|
|
feat: /model command — models.dev primary database + --provider flag (#5181)
Full overhaul of the model/provider system.
## What changed
- models.dev (109 providers, 4000+ models) as primary database for provider identity AND model metadata
- --provider flag replaces colon syntax for explicit provider switching
- Full ModelInfo/ProviderInfo dataclasses with context, cost, capabilities, modalities
- HermesOverlay system merges models.dev + Hermes-specific transport/auth/aggregator flags
- User-defined endpoints via config.yaml providers: section
- /model (no args) lists authenticated providers with curated model catalog
- Rich metadata display: context window, max output, cost/M tokens, capabilities
- Config migration: custom_providers list → providers dict (v11→v12)
- AIAgent.switch_model() for in-place model swap preserving conversation
## Files
agent/models_dev.py, hermes_cli/providers.py, hermes_cli/model_switch.py,
hermes_cli/model_normalize.py, cli.py, gateway/run.py, run_agent.py,
hermes_cli/config.py, hermes_cli/commands.py
2026-04-05 01:04:44 -07:00
|
|
|
# Sort: current provider first, then by model count descending
|
|
|
|
|
results.sort(key=lambda r: (not r["is_current"], -r["total_models"]))
|
|
|
|
|
|
|
|
|
|
return results
|