Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions hermes_cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1700,18 +1700,47 @@ def parse_model_input(raw: str, current_provider: str) -> tuple[str, str]:
model_part = stripped[colon + 1:].strip()
if provider_part and model_part and provider_part in _KNOWN_PROVIDER_NAMES:
# Support custom:name:model triple syntax for named custom
# providers. ``custom:local:qwen`` → ("custom:local", "qwen").
# Single colon ``custom:qwen`` → ("custom", "qwen") as before.
# providers. `custom:local:qwen` → ("custom:local", "qwen").
# Single colon `custom:qwen` → ("custom", "qwen") as before.
#
# BUT: Ollama models use colons in their tags (e.g.
# `glm-5.2:cloud`, `nomic-embed-text:latest`). When the
# middle part is NOT a registered custom provider name, we must
# treat everything after `custom:` as the model id to avoid
# truncating `glm-5.2:cloud` into just `cloud`.
if provider_part == "custom" and ":" in model_part:
second_colon = model_part.find(":")
custom_name = model_part[:second_colon].strip()
actual_model = model_part[second_colon + 1:].strip()
if custom_name and actual_model:
if custom_name and actual_model and _is_registered_custom_provider(custom_name):
return (f"custom:{custom_name}", actual_model)
return (normalize_provider(provider_part), model_part)
return (current_provider, stripped)


def _is_registered_custom_provider(name: str) -> bool:
"""Check whether *name* matches a custom provider in config.yaml.

Used by :func:`parse_model_input` to distinguish ``custom:ollama:qwen``
(provider ``custom:ollama``, model ``qwen``) from ``custom:glm-5.2:cloud``
(provider ``custom``, model ``glm-5.2:cloud`` — an Ollama tag).
"""
try:
from hermes_cli.config import get_compatible_custom_providers

providers = get_compatible_custom_providers()
normalized = name.strip().lower()
for entry in providers:
entry_name = str(entry.get("name", "") or "").strip().lower()
entry_key = str(entry.get("provider_key", "") or "").strip().lower()
if normalized and (normalized == entry_name or normalized == entry_key):
return True
except Exception:
import logging
logging.getLogger(__name__).debug("Failed to check custom provider registry", exc_info=True)
return False


def _get_custom_base_url() -> str:
"""Get the custom endpoint base_url from config.yaml."""
model_cfg = _get_model_config_dict()
Expand Down
42 changes: 39 additions & 3 deletions tests/hermes_cli/test_model_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,50 @@ def test_custom_colon_model_single(self):
assert model == "qwen-2.5"

def test_custom_triple_syntax(self):
"""custom:name:model → named custom provider."""
provider, model = parse_model_input("custom:local-server:qwen-2.5", "openrouter")
"""custom:name:model → named custom provider (when name is registered)."""
with patch(
"hermes_cli.models._is_registered_custom_provider",
return_value=True,
):
provider, model = parse_model_input("custom:local-server:qwen-2.5", "openrouter")
assert provider == "custom:local-server"
assert model == "qwen-2.5"

def test_custom_triple_unregistered_falls_back(self):
"""custom:name:model → when name is NOT a registered custom provider,
treat everything after custom: as the model id.

Ollama models use colons in tags (e.g. glm-5.2:cloud).
Without this guard, parse_model_input truncates the model name
to just the part after the last colon (e.g. 'cloud' instead of
'glm-5.2:cloud'), causing HTTP 404 errors.
"""
with patch(
"hermes_cli.models._is_registered_custom_provider",
return_value=False,
):
provider, model = parse_model_input("custom:glm-5.2:cloud", "custom")
assert provider == "custom"
assert model == "glm-5.2:cloud"

def test_custom_ollama_tag_not_truncated(self):
"""custom:glm-5.2:cloud must not be truncated to custom:glm-5.2 / cloud.

Regression test: Ollama cloud models have colons in their tags.
The triple-syntax parser must only split when the middle part is a
registered custom provider name.
"""
provider, model = parse_model_input("custom:glm-5.2:cloud", "custom")
assert provider == "custom"
assert model == "glm-5.2:cloud"

def test_custom_triple_spaces(self):
"""Triple syntax should handle whitespace."""
provider, model = parse_model_input("custom: my-server : my-model ", "openrouter")
with patch(
"hermes_cli.models._is_registered_custom_provider",
return_value=True,
):
provider, model = parse_model_input("custom: my-server : my-model ", "openrouter")
assert provider == "custom:my-server"
assert model == "my-model"

Expand Down
Loading