Files
hermes-agent/tests/hermes_cli/test_model_normalize.py
teknium1 96c9cc9ac6 test: purge low-value tests, lane py12 (287 removed)
Change-detectors, tautologies, source-reading tests, redundant duplicates,
mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane
ledger (category + reason for every removal).
2026-09-23 03:15:26 -07:00

204 lines
9.0 KiB
Python

"""Tests for hermes_cli.model_normalize — provider-aware model name normalization.
Covers issue #5211: opencode-go model names with dots (e.g. minimax-m2.7)
must NOT be mangled to hyphens (minimax-m2-7).
"""
import pytest
from hermes_cli.model_normalize import (
normalize_model_for_provider,
_normalize_for_deepseek,
)
# ── Regression: issue #5211 ────────────────────────────────────────────
class TestIssue5211OpenCodeGoDotPreservation:
"""OpenCode Go model names with dots must pass through unchanged."""
@pytest.mark.parametrize("model,expected", [
("minimax-m2.7", "minimax-m2.7"),
("minimax-m2.5", "minimax-m2.5"),
("glm-4.5", "glm-4.5"),
("kimi-k2.5", "kimi-k2.5"),
("some-model-1.0.3", "some-model-1.0.3"),
])
def test_opencode_go_preserves_dots(self, model, expected):
result = normalize_model_for_provider(model, "opencode-go")
assert result == expected, f"Expected {expected!r}, got {result!r}"
# ── Anthropic dot-to-hyphen conversion (regression) ────────────────────
# ── OpenCode Zen regression ────────────────────────────────────────────
# ── Copilot dot preservation (regression) ──────────────────────────────
# ── Copilot model-name normalization (issue #6879 regression) ──────────
class TestCopilotModelNormalization:
"""Copilot requires bare dot-notation model IDs.
Regression coverage for issue #6879 and the broken Copilot branch
that previously left vendor-prefixed Anthropic IDs (e.g.
``anthropic/claude-sonnet-4.6``) and dash-notation Claude IDs (e.g.
``claude-sonnet-4-6``) unchanged, causing the Copilot API to reject
the request with HTTP 400 "model_not_supported".
"""
def test_openai_codex_still_strips_openai_prefix(self):
"""Regression: openai-codex must still strip the openai/ prefix."""
assert normalize_model_for_provider("openai/gpt-5.4", "openai-codex") == "gpt-5.4"
# ── Aggregator providers (regression) ──────────────────────────────────
# ── detect_vendor ──────────────────────────────────────────────────────
# ── DeepSeek V-series pass-through (bug: V4 models silently folded to V3) ──
class TestDeepseekVSeriesPassThrough:
"""DeepSeek's V-series IDs (``deepseek-v4-pro``, ``deepseek-v4-flash``,
and future ``deepseek-v<N>-*`` variants) are first-class model IDs
accepted directly by DeepSeek's Chat Completions API. Earlier code
folded every non-reasoner name into ``deepseek-chat``, which on
aggregators (Nous portal, OpenRouter via DeepInfra) routes to V3 —
silently downgrading users who picked V4.
"""
def test_deepseek_provider_preserves_v4_pro(self):
"""End-to-end via normalize_model_for_provider — user selecting
V4 Pro must reach DeepSeek's API as V4 Pro, not V3 alias."""
result = normalize_model_for_provider("deepseek-v4-pro", "deepseek")
assert result == "deepseek-v4-pro"
def test_deepseek_provider_preserves_versionless_flash_id(self):
"""``deepseek-flash`` must reach DeepSeek's API unchanged.
DeepSeek's 2026-09 Flash refresh dropped the ``v<N>`` marker from the
public id: ``GET /v1/models`` reports ``deepseek-flash`` and the API
accepts it directly (verified live — it answers 200, and the older
``deepseek-v4-flash`` is aliased onto it). Folding it onto
``deepseek-v4-flash`` meant the id users picked never reached the wire
and the config stored a different model than the picker advertised.
"""
assert (
normalize_model_for_provider("deepseek-flash", "deepseek")
== "deepseek-flash"
)
# ── DeepSeek post-2026-07-24 alias remapping ───────────────────────────
class TestDeepseekRetiredAliasesAndCustomSlugs:
"""Only the two retired aliases are rewritten; every other id is the user's and reaches the
wire as typed (a shape allow-list swallowed the vendor's own ``deepseek-flash``, #107206)."""
def test_provider_path_rewrites_reasoner(self):
assert (
normalize_model_for_provider("deepseek-reasoner", "deepseek")
== "deepseek-flash"
)
@pytest.mark.parametrize("model", [
"deepseek-v4.1-flash",
"deepseek-v4-flash-0731",
"deepseek-r1",
"deepseek-next-preview",
"my-fine-tune",
])
def test_unknown_ids_pass_through_untouched(self, model):
assert _normalize_for_deepseek(model) == model
# ── Regression: issue #78796 ───────────────────────────────────────────
class TestIssue78796NvidiaPrefixRepair:
"""A bare NVIDIA model id must regain its ``vendor/`` prefix.
build.nvidia.com serves ``nvidia/nemotron-…``; a bare
``nemotron-3-ultra-550b-a55b`` returns a naked ``404 page not found``
that never names the model, so the failure reads like an outage.
"""
@pytest.mark.parametrize("model,expected", [
("nemotron-3-ultra-550b-a55b", "nvidia/nemotron-3-ultra-550b-a55b"),
("nemotron-3-super-120b-a12b", "nvidia/nemotron-3-super-120b-a12b"),
(
"nemotron-3-nano-omni-30b-a3b-reasoning",
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
),
])
def test_bare_nemotron_regains_prefix(self, model, expected):
assert normalize_model_for_provider(model, "nvidia") == expected
def test_third_party_model_gets_its_own_vendor(self):
"""NIM also hosts third-party models — the prefix is the catalogue's,
not a hardcoded ``nvidia/``."""
assert normalize_model_for_provider("glm-5.2", "nvidia") == "z-ai/glm-5.2"
@pytest.mark.parametrize("model", [
"nvidia/nemotron-3-ultra-550b-a55b",
"z-ai/glm-5.2",
])
def test_already_prefixed_is_untouched(self, model):
assert normalize_model_for_provider(model, "nvidia") == model
@pytest.mark.parametrize("model", [
"my-local-nim-container",
"some-finetune-v2",
])
def test_unknown_names_pass_through(self, model):
"""The same provider id fronts local NIM containers. An id absent from
the catalogue is a lookup miss, not a guess — leave it alone."""
assert normalize_model_for_provider(model, "nvidia") == model
def test_other_providers_unaffected(self):
assert normalize_model_for_provider("my-model", "custom") == "my-model"
assert (
normalize_model_for_provider("claude-sonnet-4.6", "openrouter")
== "anthropic/claude-sonnet-4.6"
)
class TestColonProviderPrefixIsStrippedLikeSlash:
"""Issue #64787: ``-m openai-codex:gpt-5.6-sol`` (Hermes's own ``provider:model`` switch syntax)
reached the Codex wire with the prefix attached and got HTTP 400. A matching ``provider:`` prefix
must normalize exactly like ``provider/``; a later colon (Ollama tags) is never a separator."""
def test_agent_init_strips_colon_prefix_before_the_wire(self, tmp_path, monkeypatch):
"""Production path: ``AIAgent(model="openai-codex:gpt-5.6-sol", provider="openai-codex")`` —
the form that survives ``-m provider:model --provider X``, programmatic construction and
gateway config — must leave ``agent.model`` without the prefix (agent/agent_init.py)."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
(tmp_path / ".env").write_text("", encoding="utf-8")
(tmp_path / "config.yaml").write_text("{}\n", encoding="utf-8")
from run_agent import AIAgent
agent = AIAgent(
model="openai-codex:gpt-5.6-sol", provider="openai-codex", api_key="sk-dummy",
base_url="https://chatgpt.com/backend-api/codex", quiet_mode=True,
skip_context_files=True, skip_memory=True, platform="cli",
)
assert agent.model == "gpt-5.6-sol"
@pytest.mark.parametrize("model,provider,expected", [
("openai:gpt-5.4", "openai-codex", "gpt-5.4"),
("zai:glm-5.1", "zai", "glm-5.1"),
("custom:qwen3:8b", "custom", "qwen3:8b"),
])
def test_matching_colon_prefix_stripped(self, model, provider, expected):
assert normalize_model_for_provider(model, provider) == expected
@pytest.mark.parametrize("model,provider", [
("qwen3:8b", "custom"), # bare Ollama tag: first colon is not a provider prefix
("anthropic:claude-x", "openai-codex"), # non-matching prefix passes through untouched
])
def test_non_matching_colon_untouched(self, model, provider):
assert normalize_model_for_provider(model, provider) == model