mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: preserve dots in model names for OpenCode Zen and ZAI providers (#8794)
OpenCode Zen was in _DOT_TO_HYPHEN_PROVIDERS, causing all dotted model names (minimax-m2.5-free, gpt-5.4, glm-5.1) to be mangled. The fix: Layer 1 (model_normalize.py): Remove opencode-zen from the blanket dot-to-hyphen set. Add an explicit block that preserves dots for non-Claude models while keeping Claude hyphenated (Zen's Claude endpoint uses anthropic_messages mode which expects hyphens). Layer 2 (run_agent.py _anthropic_preserve_dots): Add opencode-zen and zai to the provider allowlist. Broaden URL check from opencode.ai/zen/go to opencode.ai/zen/ to cover both Go and Zen endpoints. Add bigmodel.cn for ZAI URL detection. Also adds glm-5.1 to ZAI model lists in models.py and setup.py. Closes #7710 Salvaged from contributions by: - konsisumer (PR #7739, #7719) - DomGrieco (PR #8708) - Esashiero (PR #7296) - sharziki (PR #7497) - XiaoYingGee (PR #8750) - APTX4869-maker (PR #8752) - kagura-agent (PR #7157)
This commit is contained in:
parent
a0cd2c5338
commit
83ca0844f7
6 changed files with 63 additions and 12 deletions
|
|
@ -8,8 +8,9 @@ Different LLM providers expect model identifiers in different formats:
|
|||
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``.
|
||||
|
|
@ -67,7 +68,6 @@ _AGGREGATOR_PROVIDERS: frozenset[str] = frozenset({
|
|||
# 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.
|
||||
|
|
@ -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'
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
|||
"gemma-4-26b-it",
|
||||
],
|
||||
"zai": [
|
||||
"glm-5.1",
|
||||
"glm-5",
|
||||
"glm-5-turbo",
|
||||
"glm-4.7",
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ _DEFAULT_PROVIDER_MODELS = {
|
|||
"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"],
|
||||
|
|
|
|||
|
|
@ -5856,11 +5856,12 @@ class AIAgent:
|
|||
"""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."""
|
||||
|
|
|
|||
|
|
@ -308,6 +308,34 @@ class TestMinimaxPreserveDots:
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -54,14 +54,19 @@ class TestAnthropicDotToHyphen:
|
|||
|
||||
# ── 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
|
||||
|
||||
|
|
@ -69,6 +74,10 @@ class TestOpenCodeZenDotToHyphen:
|
|||
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) ──────────────────────────────
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue