Skip to content
Merged
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
20 changes: 16 additions & 4 deletions hermes_cli/model_normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
hyphens: ``claude-sonnet-4-6``.
- **Copilot** expects bare names *with* dots preserved:
``claude-sonnet-4.6``.
- **OpenCode Zen** follows the same dot-to-hyphen convention as
Anthropic: ``claude-sonnet-4-6``.
- **OpenCode Zen** preserves dots for GPT/GLM/Gemini/Kimi/MiniMax-style
model IDs, but Claude still uses hyphenated native names like
``claude-sonnet-4-6``.
- **OpenCode Go** preserves dots in model names: ``minimax-m2.7``.
- **DeepSeek** only accepts two model identifiers:
``deepseek-chat`` and ``deepseek-reasoner``.
Expand Down Expand Up @@ -67,7 +68,6 @@
# Providers that want bare names with dots replaced by hyphens.
_DOT_TO_HYPHEN_PROVIDERS: frozenset[str] = frozenset({
"anthropic",
"opencode-zen",
})

# Providers that want bare names with dots preserved.
Expand Down Expand Up @@ -329,6 +329,9 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str:
>>> normalize_model_for_provider("claude-sonnet-4.6", "opencode-zen")
'claude-sonnet-4-6'

>>> normalize_model_for_provider("minimax-m2.5-free", "opencode-zen")
'minimax-m2.5-free'

>>> normalize_model_for_provider("deepseek-v3", "deepseek")
'deepseek-chat'

Expand All @@ -351,7 +354,16 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str:
if provider in _AGGREGATOR_PROVIDERS:
return _prepend_vendor(name)

# --- Anthropic / OpenCode: strip matching provider prefix, dots -> hyphens ---
# --- OpenCode Zen: Claude stays hyphenated; other models keep dots ---
if provider == "opencode-zen":
bare = _strip_matching_provider_prefix(name, provider)
if "/" in bare:
return bare
if bare.lower().startswith("claude-"):
return _dots_to_hyphens(bare)
return bare

# --- Anthropic: strip matching provider prefix, dots -> hyphens ---
if provider in _DOT_TO_HYPHEN_PROVIDERS:
bare = _strip_matching_provider_prefix(name, provider)
if "/" in bare:
Expand Down
1 change: 1 addition & 0 deletions hermes_cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def _codex_curated_models() -> list[str]:
"gemma-4-26b-it",
],
"zai": [
"glm-5.1",
"glm-5",
"glm-5-turbo",
"glm-4.7",
Expand Down
2 changes: 1 addition & 1 deletion hermes_cli/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def _supports_same_provider_pool_setup(provider: str) -> bool:
"gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite",
"gemma-4-31b-it", "gemma-4-26b-it",
],
"zai": ["glm-5", "glm-4.7", "glm-4.5", "glm-4.5-flash"],
"zai": ["glm-5.1", "glm-5", "glm-4.7", "glm-4.5", "glm-4.5-flash"],
"kimi-coding": ["kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"],
"minimax": ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"],
"minimax-cn": ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"],
Expand Down
7 changes: 4 additions & 3 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5856,11 +5856,12 @@ def _anthropic_preserve_dots(self) -> bool:
"""True when using an anthropic-compatible endpoint that preserves dots in model names.
Alibaba/DashScope keeps dots (e.g. qwen3.5-plus).
MiniMax keeps dots (e.g. MiniMax-M2.7).
OpenCode Go keeps dots (e.g. minimax-m2.7)."""
if (getattr(self, "provider", "") or "").lower() in {"alibaba", "minimax", "minimax-cn", "opencode-go"}:
OpenCode Go/Zen keeps dots for non-Claude models (e.g. minimax-m2.5-free).
ZAI/Zhipu keeps dots (e.g. glm-4.7, glm-5.1)."""
if (getattr(self, "provider", "") or "").lower() in {"alibaba", "minimax", "minimax-cn", "opencode-go", "opencode-zen", "zai"}:
return True
base = (getattr(self, "base_url", "") or "").lower()
return "dashscope" in base or "aliyuncs" in base or "minimax" in base or "opencode.ai/zen/go" in base
return "dashscope" in base or "aliyuncs" in base or "minimax" in base or "opencode.ai/zen/" in base or "bigmodel.cn" in base

def _is_qwen_portal(self) -> bool:
"""Return True when the base URL targets Qwen Portal."""
Expand Down
28 changes: 28 additions & 0 deletions tests/agent/test_minimax_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,34 @@ def test_anthropic_does_not_preserve_dots(self):
from run_agent import AIAgent
assert AIAgent._anthropic_preserve_dots(agent) is False

def test_opencode_zen_provider_preserves_dots(self):
from types import SimpleNamespace
agent = SimpleNamespace(provider="opencode-zen", base_url="")
from run_agent import AIAgent
assert AIAgent._anthropic_preserve_dots(agent) is True

def test_opencode_zen_url_preserves_dots(self):
from types import SimpleNamespace
agent = SimpleNamespace(provider="custom", base_url="https://opencode.ai/zen/v1")
from run_agent import AIAgent
assert AIAgent._anthropic_preserve_dots(agent) is True

def test_zai_provider_preserves_dots(self):
from types import SimpleNamespace
agent = SimpleNamespace(provider="zai", base_url="")
from run_agent import AIAgent
assert AIAgent._anthropic_preserve_dots(agent) is True

def test_bigmodel_cn_url_preserves_dots(self):
from types import SimpleNamespace
agent = SimpleNamespace(provider="custom", base_url="https://open.bigmodel.cn/api/paas/v4")
from run_agent import AIAgent
assert AIAgent._anthropic_preserve_dots(agent) is True

def test_normalize_preserves_m25_free_dot(self):
from agent.anthropic_adapter import normalize_model_name
assert normalize_model_name("minimax-m2.5-free", preserve_dots=True) == "minimax-m2.5-free"

def test_normalize_preserves_m27_dot(self):
from agent.anthropic_adapter import normalize_model_name
assert normalize_model_name("MiniMax-M2.7", preserve_dots=True) == "MiniMax-M2.7"
Expand Down
17 changes: 13 additions & 4 deletions tests/hermes_cli/test_model_normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,30 @@ def test_anthropic_strips_vendor_prefix(self):

# ── OpenCode Zen regression ────────────────────────────────────────────

class TestOpenCodeZenDotToHyphen:
"""OpenCode Zen follows Anthropic convention (dots→hyphens)."""
class TestOpenCodeZenModelNormalization:
"""OpenCode Zen preserves dots for most models, but Claude stays hyphenated."""

@pytest.mark.parametrize("model,expected", [
("claude-sonnet-4.6", "claude-sonnet-4-6"),
("glm-4.5", "glm-4-5"),
("opencode-zen/claude-opus-4.5", "claude-opus-4-5"),
("glm-4.5", "glm-4.5"),
("glm-5.1", "glm-5.1"),
("gpt-5.4", "gpt-5.4"),
("minimax-m2.5-free", "minimax-m2.5-free"),
("kimi-k2.5", "kimi-k2.5"),
])
def test_zen_converts_dots(self, model, expected):
def test_zen_normalizes_models(self, model, expected):
result = normalize_model_for_provider(model, "opencode-zen")
assert result == expected

def test_zen_strips_vendor_prefix(self):
result = normalize_model_for_provider("opencode-zen/claude-sonnet-4.6", "opencode-zen")
assert result == "claude-sonnet-4-6"

def test_zen_strips_vendor_prefix_for_non_claude(self):
result = normalize_model_for_provider("opencode-zen/glm-5.1", "opencode-zen")
assert result == "glm-5.1"


# ── Copilot dot preservation (regression) ──────────────────────────────

Expand Down
Loading