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:
@@ -8,8 +8,9 @@ Different LLM providers expect model identifiers in different formats:
|
|||||||
hyphens: ``claude-sonnet-4-6``.
|
hyphens: ``claude-sonnet-4-6``.
|
||||||
- **Copilot** expects bare names *with* dots preserved:
|
- **Copilot** expects bare names *with* dots preserved:
|
||||||
``claude-sonnet-4.6``.
|
``claude-sonnet-4.6``.
|
||||||
- **OpenCode Zen** follows the same dot-to-hyphen convention as
|
- **OpenCode Zen** preserves dots for GPT/GLM/Gemini/Kimi/MiniMax-style
|
||||||
Anthropic: ``claude-sonnet-4-6``.
|
model IDs, but Claude still uses hyphenated native names like
|
||||||
|
``claude-sonnet-4-6``.
|
||||||
- **OpenCode Go** preserves dots in model names: ``minimax-m2.7``.
|
- **OpenCode Go** preserves dots in model names: ``minimax-m2.7``.
|
||||||
- **DeepSeek** only accepts two model identifiers:
|
- **DeepSeek** only accepts two model identifiers:
|
||||||
``deepseek-chat`` and ``deepseek-reasoner``.
|
``deepseek-chat`` and ``deepseek-reasoner``.
|
||||||
@@ -67,7 +68,6 @@ _AGGREGATOR_PROVIDERS: frozenset[str] = frozenset({
|
|||||||
# Providers that want bare names with dots replaced by hyphens.
|
# Providers that want bare names with dots replaced by hyphens.
|
||||||
_DOT_TO_HYPHEN_PROVIDERS: frozenset[str] = frozenset({
|
_DOT_TO_HYPHEN_PROVIDERS: frozenset[str] = frozenset({
|
||||||
"anthropic",
|
"anthropic",
|
||||||
"opencode-zen",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Providers that want bare names with dots preserved.
|
# 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")
|
>>> normalize_model_for_provider("claude-sonnet-4.6", "opencode-zen")
|
||||||
'claude-sonnet-4-6'
|
'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")
|
>>> normalize_model_for_provider("deepseek-v3", "deepseek")
|
||||||
'deepseek-chat'
|
'deepseek-chat'
|
||||||
|
|
||||||
@@ -351,7 +354,16 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str:
|
|||||||
if provider in _AGGREGATOR_PROVIDERS:
|
if provider in _AGGREGATOR_PROVIDERS:
|
||||||
return _prepend_vendor(name)
|
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:
|
if provider in _DOT_TO_HYPHEN_PROVIDERS:
|
||||||
bare = _strip_matching_provider_prefix(name, provider)
|
bare = _strip_matching_provider_prefix(name, provider)
|
||||||
if "/" in bare:
|
if "/" in bare:
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
|||||||
"gemma-4-26b-it",
|
"gemma-4-26b-it",
|
||||||
],
|
],
|
||||||
"zai": [
|
"zai": [
|
||||||
|
"glm-5.1",
|
||||||
"glm-5",
|
"glm-5",
|
||||||
"glm-5-turbo",
|
"glm-5-turbo",
|
||||||
"glm-4.7",
|
"glm-4.7",
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ _DEFAULT_PROVIDER_MODELS = {
|
|||||||
"gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite",
|
"gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite",
|
||||||
"gemma-4-31b-it", "gemma-4-26b-it",
|
"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"],
|
"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": ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"],
|
||||||
"minimax-cn": ["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.
|
"""True when using an anthropic-compatible endpoint that preserves dots in model names.
|
||||||
Alibaba/DashScope keeps dots (e.g. qwen3.5-plus).
|
Alibaba/DashScope keeps dots (e.g. qwen3.5-plus).
|
||||||
MiniMax keeps dots (e.g. MiniMax-M2.7).
|
MiniMax keeps dots (e.g. MiniMax-M2.7).
|
||||||
OpenCode Go keeps dots (e.g. minimax-m2.7)."""
|
OpenCode Go/Zen keeps dots for non-Claude models (e.g. minimax-m2.5-free).
|
||||||
if (getattr(self, "provider", "") or "").lower() in {"alibaba", "minimax", "minimax-cn", "opencode-go"}:
|
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
|
return True
|
||||||
base = (getattr(self, "base_url", "") or "").lower()
|
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:
|
def _is_qwen_portal(self) -> bool:
|
||||||
"""Return True when the base URL targets Qwen Portal."""
|
"""Return True when the base URL targets Qwen Portal."""
|
||||||
|
|||||||
@@ -308,6 +308,34 @@ class TestMinimaxPreserveDots:
|
|||||||
from run_agent import AIAgent
|
from run_agent import AIAgent
|
||||||
assert AIAgent._anthropic_preserve_dots(agent) is False
|
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):
|
def test_normalize_preserves_m27_dot(self):
|
||||||
from agent.anthropic_adapter import normalize_model_name
|
from agent.anthropic_adapter import normalize_model_name
|
||||||
assert normalize_model_name("MiniMax-M2.7", preserve_dots=True) == "MiniMax-M2.7"
|
assert normalize_model_name("MiniMax-M2.7", preserve_dots=True) == "MiniMax-M2.7"
|
||||||
|
|||||||
@@ -54,14 +54,19 @@ class TestAnthropicDotToHyphen:
|
|||||||
|
|
||||||
# ── OpenCode Zen regression ────────────────────────────────────────────
|
# ── OpenCode Zen regression ────────────────────────────────────────────
|
||||||
|
|
||||||
class TestOpenCodeZenDotToHyphen:
|
class TestOpenCodeZenModelNormalization:
|
||||||
"""OpenCode Zen follows Anthropic convention (dots→hyphens)."""
|
"""OpenCode Zen preserves dots for most models, but Claude stays hyphenated."""
|
||||||
|
|
||||||
@pytest.mark.parametrize("model,expected", [
|
@pytest.mark.parametrize("model,expected", [
|
||||||
("claude-sonnet-4.6", "claude-sonnet-4-6"),
|
("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")
|
result = normalize_model_for_provider(model, "opencode-zen")
|
||||||
assert result == expected
|
assert result == expected
|
||||||
|
|
||||||
@@ -69,6 +74,10 @@ class TestOpenCodeZenDotToHyphen:
|
|||||||
result = normalize_model_for_provider("opencode-zen/claude-sonnet-4.6", "opencode-zen")
|
result = normalize_model_for_provider("opencode-zen/claude-sonnet-4.6", "opencode-zen")
|
||||||
assert result == "claude-sonnet-4-6"
|
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) ──────────────────────────────
|
# ── Copilot dot preservation (regression) ──────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user