mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-30 06:41:51 +00:00
Remove unused imports (F401) and duplicate/shadowed import redefinitions (F811) across the codebase using ruff's safe autofixes. No behavioral changes -- imports only. - ~1400 safe autofixes applied across 644 files (net -1072 lines) - __init__.py re-exports preserved (excluded from F401 removal so public re-export surfaces stay intact) - Re-exports that are imported or monkeypatched by tests but look unused in their defining module are kept with explicit # noqa: F401 (gateway/run.py load_dotenv; run_agent re-exports from agent.message_sanitization, agent.context_compressor, agent.retry_utils, agent.prompt_builder, agent.process_bootstrap, agent.codex_responses_adapter) - Unsafe F841 (unused-variable) fixes deliberately skipped -- those can change behavior when the RHS has side effects - ruff lints remain disabled in pyproject.toml (only PLW1514 is selected); this is a one-time cleanup, not a config change Verification: - python -m compileall: clean - pytest --collect-only: all 27161 tests collect (zero import errors) - core entry points import clean (run_agent, model_tools, cli, toolsets, hermes_state, batch_runner, gateway) - static scan: every name any test imports directly from an edited module still resolves
262 lines
11 KiB
Python
262 lines
11 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,
|
|
_DOT_TO_HYPHEN_PROVIDERS,
|
|
_normalize_for_deepseek,
|
|
detect_vendor,
|
|
)
|
|
|
|
|
|
# ── 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}"
|
|
|
|
def test_opencode_go_not_in_dot_to_hyphen_set(self):
|
|
"""opencode-go must NOT be in the dot-to-hyphen provider set."""
|
|
assert "opencode-go" not in _DOT_TO_HYPHEN_PROVIDERS
|
|
|
|
|
|
# ── Anthropic dot-to-hyphen conversion (regression) ────────────────────
|
|
|
|
class TestAnthropicDotToHyphen:
|
|
"""Anthropic API still needs dots→hyphens."""
|
|
|
|
@pytest.mark.parametrize("model,expected", [
|
|
("claude-sonnet-4.6", "claude-sonnet-4-6"),
|
|
("claude-opus-4.5", "claude-opus-4-5"),
|
|
])
|
|
def test_anthropic_converts_dots(self, model, expected):
|
|
result = normalize_model_for_provider(model, "anthropic")
|
|
assert result == expected
|
|
|
|
def test_anthropic_strips_vendor_prefix(self):
|
|
result = normalize_model_for_provider("anthropic/claude-sonnet-4.6", "anthropic")
|
|
assert result == "claude-sonnet-4-6"
|
|
|
|
|
|
# ── OpenCode Zen regression ────────────────────────────────────────────
|
|
|
|
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"),
|
|
("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_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) ──────────────────────────────
|
|
|
|
class TestCopilotDotPreservation:
|
|
"""Copilot preserves dots in model names."""
|
|
|
|
@pytest.mark.parametrize("model,expected", [
|
|
("claude-sonnet-4.6", "claude-sonnet-4.6"),
|
|
("gpt-5.4", "gpt-5.4"),
|
|
])
|
|
def test_copilot_preserves_dots(self, model, expected):
|
|
result = normalize_model_for_provider(model, "copilot")
|
|
assert result == expected
|
|
|
|
|
|
# ── 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".
|
|
"""
|
|
|
|
@pytest.mark.parametrize("model,expected", [
|
|
# Vendor-prefixed Anthropic IDs — prefix must be stripped.
|
|
("anthropic/claude-opus-4.6", "claude-opus-4.6"),
|
|
("anthropic/claude-sonnet-4.6", "claude-sonnet-4.6"),
|
|
("anthropic/claude-sonnet-4.5", "claude-sonnet-4.5"),
|
|
("anthropic/claude-haiku-4.5", "claude-haiku-4.5"),
|
|
# Vendor-prefixed OpenAI IDs — prefix must be stripped.
|
|
("openai/gpt-5.4", "gpt-5.4"),
|
|
("openai/gpt-4o", "gpt-4o"),
|
|
("openai/gpt-4o-mini", "gpt-4o-mini"),
|
|
# Dash-notation Claude IDs — must be converted to dot-notation.
|
|
("claude-opus-4-6", "claude-opus-4.6"),
|
|
("claude-sonnet-4-6", "claude-sonnet-4.6"),
|
|
("claude-sonnet-4-5", "claude-sonnet-4.5"),
|
|
("claude-haiku-4-5", "claude-haiku-4.5"),
|
|
# Combined: vendor-prefixed + dash-notation.
|
|
("anthropic/claude-opus-4-6", "claude-opus-4.6"),
|
|
("anthropic/claude-sonnet-4-6", "claude-sonnet-4.6"),
|
|
# Already-canonical inputs pass through unchanged.
|
|
("claude-sonnet-4.6", "claude-sonnet-4.6"),
|
|
("gpt-5.4", "gpt-5.4"),
|
|
("gpt-5-mini", "gpt-5-mini"),
|
|
])
|
|
def test_copilot_normalization(self, model, expected):
|
|
assert normalize_model_for_provider(model, "copilot") == expected
|
|
|
|
@pytest.mark.parametrize("model,expected", [
|
|
("anthropic/claude-sonnet-4.6", "claude-sonnet-4.6"),
|
|
("claude-sonnet-4-6", "claude-sonnet-4.6"),
|
|
("claude-opus-4-6", "claude-opus-4.6"),
|
|
("openai/gpt-5.4", "gpt-5.4"),
|
|
])
|
|
def test_copilot_acp_normalization(self, model, expected):
|
|
"""Copilot ACP shares the same API expectations as HTTP Copilot."""
|
|
assert normalize_model_for_provider(model, "copilot-acp") == expected
|
|
|
|
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) ──────────────────────────────────
|
|
|
|
class TestAggregatorProviders:
|
|
"""Aggregators need vendor/model slugs."""
|
|
|
|
def test_openrouter_prepends_vendor(self):
|
|
result = normalize_model_for_provider("claude-sonnet-4.6", "openrouter")
|
|
assert result == "anthropic/claude-sonnet-4.6"
|
|
|
|
def test_nous_prepends_vendor(self):
|
|
result = normalize_model_for_provider("gpt-5.4", "nous")
|
|
assert result == "openai/gpt-5.4"
|
|
|
|
def test_vendor_already_present(self):
|
|
result = normalize_model_for_provider("anthropic/claude-sonnet-4.6", "openrouter")
|
|
assert result == "anthropic/claude-sonnet-4.6"
|
|
|
|
|
|
class TestIssue6211NativeProviderPrefixNormalization:
|
|
@pytest.mark.parametrize("model,target_provider,expected", [
|
|
("zai/glm-5.1", "zai", "glm-5.1"),
|
|
("google/gemini-2.5-pro", "gemini", "google/gemini-2.5-pro"),
|
|
("moonshot/kimi-k2.5", "kimi-coding", "kimi-k2.5"),
|
|
("anthropic/claude-sonnet-4.6", "openrouter", "anthropic/claude-sonnet-4.6"),
|
|
("Qwen/Qwen3.5-397B-A17B", "huggingface", "Qwen/Qwen3.5-397B-A17B"),
|
|
("modal/zai-org/GLM-5-FP8", "custom", "modal/zai-org/GLM-5-FP8"),
|
|
])
|
|
def test_native_provider_prefixes_are_only_stripped_on_matching_provider(
|
|
self, model, target_provider, expected
|
|
):
|
|
assert normalize_model_for_provider(model, target_provider) == expected
|
|
|
|
|
|
# ── detect_vendor ──────────────────────────────────────────────────────
|
|
|
|
class TestDetectVendor:
|
|
@pytest.mark.parametrize("model,expected", [
|
|
("claude-sonnet-4.6", "anthropic"),
|
|
("gpt-5.4-mini", "openai"),
|
|
("minimax-m2.7", "minimax"),
|
|
("glm-4.5", "z-ai"),
|
|
("kimi-k2.5", "moonshotai"),
|
|
])
|
|
def test_detects_known_vendors(self, model, expected):
|
|
assert detect_vendor(model) == expected
|
|
|
|
|
|
# ── 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.
|
|
"""
|
|
|
|
@pytest.mark.parametrize("model", [
|
|
"deepseek-v4-pro",
|
|
"deepseek-v4-flash",
|
|
"deepseek/deepseek-v4-pro", # vendor-prefixed
|
|
"deepseek/deepseek-v4-flash",
|
|
"DeepSeek-V4-Pro", # case-insensitive
|
|
"deepseek-v4-flash-20260423", # dated variant
|
|
"deepseek-v5-pro", # future V-series
|
|
"deepseek-v10-ultra", # double-digit future
|
|
])
|
|
def test_v_series_passes_through(self, model):
|
|
expected = model.split("/", 1)[-1].lower()
|
|
assert _normalize_for_deepseek(model) == expected
|
|
|
|
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_v4_flash(self):
|
|
result = normalize_model_for_provider("deepseek-v4-flash", "deepseek")
|
|
assert result == "deepseek-v4-flash"
|
|
|
|
|
|
# ── DeepSeek regressions (existing behaviour still holds) ──────────────
|
|
|
|
class TestDeepseekCanonicalAndReasonerMapping:
|
|
"""Canonical pass-through and reasoner-keyword folding stay intact."""
|
|
|
|
@pytest.mark.parametrize("model,expected", [
|
|
("deepseek-chat", "deepseek-chat"),
|
|
("deepseek-reasoner", "deepseek-reasoner"),
|
|
("DEEPSEEK-CHAT", "deepseek-chat"),
|
|
])
|
|
def test_canonical_models_pass_through(self, model, expected):
|
|
assert _normalize_for_deepseek(model) == expected
|
|
|
|
@pytest.mark.parametrize("model", [
|
|
"deepseek-r1",
|
|
"deepseek-r1-0528",
|
|
"deepseek-think-v3",
|
|
"deepseek-reasoning-preview",
|
|
"deepseek-cot-experimental",
|
|
])
|
|
def test_reasoner_keywords_map_to_reasoner(self, model):
|
|
assert _normalize_for_deepseek(model) == "deepseek-reasoner"
|
|
|
|
@pytest.mark.parametrize("model", [
|
|
"deepseek-chat-v3.1", # 'chat' prefix, not V-series pattern
|
|
"unknown-model",
|
|
"something-random",
|
|
"gpt-5", # non-DeepSeek names still fall through
|
|
])
|
|
def test_unknown_names_fall_back_to_chat(self, model):
|
|
assert _normalize_for_deepseek(model) == "deepseek-chat"
|