hermes-agent/tests/hermes_cli/test_models.py
Teknium b34e565957 feat(models): catalog-labeled silent default — GLM-5.2 marked "default": true in the model catalog
The remote model catalog (website/static/api/model-catalog.json) now labels
exactly one entry per provider block with "default": true — z-ai/glm-5.2 for
both OpenRouter and Nous Portal. That labeled entry is the model Hermes
silently lands on when the user never picked one, and it can be rotated by
editing the manifest alone: no release needed.

- model_catalog.py: get_default_model_from_cache() reads the label from the
  in-process/disk cache only — never triggers a network fetch, so hot
  resolution paths (agent build, gateway session setup) stay network-free.
- models.py: get_preferred_silent_default_model() resolves catalog label
  first, PREFERRED_SILENT_DEFAULT_MODEL constant second (offline/fresh
  install). _PROVIDER_SILENT_DEFAULT_OVERRIDES dict replaced by
  _SILENT_DEFAULT_PROVIDERS routing through the shared resolver.
  fetch_openrouter_models() preserves the "default" badge through live
  /v1/models refreshes so the picker shows it.
- scripts/build_model_catalog.py: generator emits the default label so
  regeneration can't drop it.
- website/docs/reference/model-catalog.md: schema documents the new field.
- Salvaged from PR #61141 (@HumphreySun98): bare-provider /model switches
  (/model nous) route through the cost-safe default instead of curated
  entry [0].
- tests: catalog-label precedence, constant fallback, stale-label fallback,
  cache-only (no network) guarantee, and a shipped-manifest contract test
  pinning the labeled entry to PREFERRED_SILENT_DEFAULT_MODEL.

E2E (temp HERMES_HOME): fresh-install constant fallback, shipped-manifest
label read, release-free rotation (relabeled cache -> new default across
models.py, tui_gateway, and gateway empty-model paths) all verified.
2026-07-15 00:10:31 -07:00

978 lines
44 KiB
Python

"""Tests for the hermes_cli models module."""
from unittest.mock import patch, MagicMock
from hermes_cli.nous_account import NousPortalAccountInfo
from hermes_cli.models import (
OPENROUTER_MODELS, fetch_openrouter_models, model_ids, detect_provider_for_model,
is_nous_free_tier, partition_nous_models_by_tier,
check_nous_free_tier, _FREE_TIER_CACHE_TTL,
union_with_portal_free_recommendations,
union_with_portal_paid_recommendations,
)
import hermes_cli.models as _models_mod
LIVE_OPENROUTER_MODELS = [
("anthropic/claude-opus-4.6", "recommended"),
("qwen/qwen3.7-max", ""),
("nvidia/nemotron-3-super-120b-a12b:free", "free"),
]
class TestModelIds:
def test_returns_non_empty_list(self):
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
ids = model_ids()
assert isinstance(ids, list)
assert len(ids) > 0
def test_ids_match_fetched_catalog(self):
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
ids = model_ids()
expected = [mid for mid, _ in LIVE_OPENROUTER_MODELS]
assert ids == expected
def test_all_ids_contain_provider_slash(self):
"""Model IDs should follow the provider/model format."""
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
for mid in model_ids():
assert "/" in mid, f"Model ID '{mid}' missing provider/ prefix"
def test_no_duplicate_ids(self):
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
ids = model_ids()
assert len(ids) == len(set(ids)), "Duplicate model IDs found"
class TestOpenRouterModels:
def test_structure_is_list_of_tuples(self):
for entry in OPENROUTER_MODELS:
assert isinstance(entry, tuple) and len(entry) == 2
mid, desc = entry
assert isinstance(mid, str) and len(mid) > 0
assert isinstance(desc, str)
class TestFetchOpenRouterModels:
def test_live_fetch_recomputes_free_tags(self, monkeypatch):
class _Resp:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def read(self):
return b'{"data":[{"id":"anthropic/claude-opus-4.8","pricing":{"prompt":"0.000015","completion":"0.000075"}},{"id":"qwen/qwen3.7-max","pricing":{"prompt":"0.000000325","completion":"0.00000195"}},{"id":"nvidia/nemotron-3-super-120b-a12b:free","pricing":{"prompt":"0","completion":"0"}}]}'
monkeypatch.setattr(_models_mod, "_openrouter_catalog_cache", None)
with patch("hermes_cli.models._urlopen_model_catalog_request", return_value=_Resp()):
models = fetch_openrouter_models(force_refresh=True)
assert models == [
("anthropic/claude-opus-4.8", "recommended"),
("qwen/qwen3.7-max", ""),
("nvidia/nemotron-3-super-120b-a12b:free", "free"),
]
def test_falls_back_to_static_snapshot_on_fetch_failure(self, monkeypatch):
monkeypatch.setattr(_models_mod, "_openrouter_catalog_cache", None)
# Pin the remote manifest out too — otherwise the fallback silently
# depends on whatever the deployed catalog currently contains.
with patch("hermes_cli.model_catalog.get_curated_openrouter_models", return_value=None), \
patch("hermes_cli.models._urlopen_model_catalog_request", side_effect=OSError("boom")):
models = fetch_openrouter_models(force_refresh=True)
assert models == OPENROUTER_MODELS
def test_filters_out_models_without_tool_support(self, monkeypatch):
"""Models whose supported_parameters omits 'tools' must not appear in the picker.
hermes-agent is tool-calling-first — surfacing a non-tool model leads to
immediate runtime failures when the user selects it. Ported from
Kilo-Org/kilocode#9068.
"""
class _Resp:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def read(self):
# opus-4.6 advertises tools → kept
# nano-image has explicit supported_parameters that OMITS tools → dropped
# qwen3.7-max advertises tools → kept
return (
b'{"data":['
b'{"id":"anthropic/claude-opus-4.6","pricing":{"prompt":"0.000015","completion":"0.000075"},'
b'"supported_parameters":["temperature","tools","tool_choice"]},'
b'{"id":"google/gemini-3-pro-image-preview","pricing":{"prompt":"0.00001","completion":"0.00003"},'
b'"supported_parameters":["temperature","response_format"]},'
b'{"id":"qwen/qwen3.7-max","pricing":{"prompt":"0.000000325","completion":"0.00000195"},'
b'"supported_parameters":["tools","temperature"]}'
b']}'
)
# Include the image-only id in the curated list so it has a chance to be surfaced.
monkeypatch.setattr(
_models_mod,
"OPENROUTER_MODELS",
[
("anthropic/claude-opus-4.6", ""),
("google/gemini-3-pro-image-preview", ""),
("qwen/qwen3.7-max", ""),
],
)
monkeypatch.setattr(_models_mod, "_openrouter_catalog_cache", None)
with (
patch("hermes_cli.model_catalog.get_curated_openrouter_models", return_value=[]),
patch("hermes_cli.models._urlopen_model_catalog_request", return_value=_Resp()),
):
models = fetch_openrouter_models(force_refresh=True)
ids = [mid for mid, _ in models]
assert "anthropic/claude-opus-4.6" in ids
assert "qwen/qwen3.7-max" in ids
# Image-only model advertised supported_parameters WITHOUT tools → must be dropped.
assert "google/gemini-3-pro-image-preview" not in ids
def test_permissive_when_supported_parameters_missing(self, monkeypatch):
"""Models missing the supported_parameters field keep appearing in the picker.
Some OpenRouter-compatible gateways (Nous Portal, private mirrors, older
catalog snapshots) don't populate supported_parameters. Treating missing
as 'unknown → allow' prevents the picker from silently emptying on
those gateways.
"""
class _Resp:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def read(self):
# No supported_parameters field at all on either entry.
return (
b'{"data":['
b'{"id":"anthropic/claude-opus-4.8","pricing":{"prompt":"0.000015","completion":"0.000075"}},'
b'{"id":"qwen/qwen3.7-max","pricing":{"prompt":"0.000000325","completion":"0.00000195"}}'
b']}'
)
monkeypatch.setattr(_models_mod, "_openrouter_catalog_cache", None)
with patch("hermes_cli.models._urlopen_model_catalog_request", return_value=_Resp()):
models = fetch_openrouter_models(force_refresh=True)
ids = [mid for mid, _ in models]
assert "anthropic/claude-opus-4.8" in ids
assert "qwen/qwen3.7-max" in ids
class TestOpenRouterToolSupportHelper:
"""Unit tests for _openrouter_model_supports_tools (Kilo port #9068)."""
def test_tools_in_supported_parameters(self):
from hermes_cli.models import _openrouter_model_supports_tools
assert _openrouter_model_supports_tools(
{"id": "x", "supported_parameters": ["temperature", "tools"]}
) is True
def test_tools_missing_from_supported_parameters(self):
from hermes_cli.models import _openrouter_model_supports_tools
assert _openrouter_model_supports_tools(
{"id": "x", "supported_parameters": ["temperature", "response_format"]}
) is False
def test_supported_parameters_absent_is_permissive(self):
"""Missing field → allow (so older / non-OR gateways still work)."""
from hermes_cli.models import _openrouter_model_supports_tools
assert _openrouter_model_supports_tools({"id": "x"}) is True
def test_supported_parameters_none_is_permissive(self):
from hermes_cli.models import _openrouter_model_supports_tools
assert _openrouter_model_supports_tools({"id": "x", "supported_parameters": None}) is True
def test_supported_parameters_malformed_is_permissive(self):
"""Malformed (non-list) value → allow rather than silently drop."""
from hermes_cli.models import _openrouter_model_supports_tools
assert _openrouter_model_supports_tools(
{"id": "x", "supported_parameters": "tools,temperature"}
) is True
def test_non_dict_item_is_permissive(self):
from hermes_cli.models import _openrouter_model_supports_tools
assert _openrouter_model_supports_tools(None) is True
assert _openrouter_model_supports_tools("anthropic/claude-opus-4.6") is True
def test_empty_supported_parameters_list_drops_model(self):
"""Explicit empty list → no tools → drop."""
from hermes_cli.models import _openrouter_model_supports_tools
assert _openrouter_model_supports_tools(
{"id": "x", "supported_parameters": []}
) is False
class TestFindOpenrouterSlug:
def test_exact_match(self):
from hermes_cli.models import _find_openrouter_slug
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
assert _find_openrouter_slug("anthropic/claude-opus-4.6") == "anthropic/claude-opus-4.6"
def test_bare_name_match(self):
from hermes_cli.models import _find_openrouter_slug
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
result = _find_openrouter_slug("claude-opus-4.6")
assert result == "anthropic/claude-opus-4.6"
def test_case_insensitive(self):
from hermes_cli.models import _find_openrouter_slug
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
result = _find_openrouter_slug("Anthropic/Claude-Opus-4.6")
assert result is not None
def test_unknown_returns_none(self):
from hermes_cli.models import _find_openrouter_slug
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
assert _find_openrouter_slug("totally-fake-model-xyz") is None
class TestDetectProviderForModel:
def test_anthropic_model_detected(self):
"""claude-opus-4-6 should resolve to anthropic provider."""
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
result = detect_provider_for_model("claude-opus-4-6", "openai-codex")
assert result is not None
assert result[0] == "anthropic"
def test_deepseek_model_detected(self):
"""deepseek-chat should resolve to deepseek provider."""
result = detect_provider_for_model("deepseek-chat", "openai-codex")
assert result is not None
# Provider is deepseek (direct) or openrouter (fallback) depending on creds
assert result[0] in {"deepseek", "openrouter"}
def test_current_provider_model_returns_none(self):
"""Models belonging to the current provider should not trigger a switch."""
assert detect_provider_for_model("gpt-5.3-codex", "openai-codex") is None
def test_short_alias_resolves_to_static_model(self):
"""Short aliases (e.g. sonnet) should resolve without network lookups."""
with patch(
"hermes_cli.models.fetch_openrouter_models",
side_effect=AssertionError("network lookup should not run"),
):
result = detect_provider_for_model("sonnet", "auto")
assert result is not None
assert result[0] == "anthropic"
assert result[1].startswith("claude-sonnet")
def test_openrouter_slug_match(self):
"""Models in the OpenRouter catalog should be found."""
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
result = detect_provider_for_model("anthropic/claude-opus-4.6", "openai-codex")
assert result is not None
assert result[0] == "openrouter"
assert result[1] == "anthropic/claude-opus-4.6"
def test_bare_name_gets_openrouter_slug(self, monkeypatch):
for env_var in (
"ANTHROPIC_API_KEY",
"ANTHROPIC_TOKEN",
"CLAUDE_CODE_TOKEN",
"CLAUDE_CODE_OAUTH_TOKEN",
):
monkeypatch.delenv(env_var, raising=False)
"""Bare model names should get mapped to full OpenRouter slugs."""
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
result = detect_provider_for_model("claude-opus-4.6", "openai-codex")
assert result is not None
# Should find it on OpenRouter with full slug
assert result[1] == "anthropic/claude-opus-4.6"
def test_unknown_model_returns_none(self):
"""Completely unknown model names should return None."""
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
assert detect_provider_for_model("nonexistent-model-xyz", "openai-codex") is None
def test_aggregator_not_suggested(self):
"""nous/openrouter should never be auto-suggested as target provider."""
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
result = detect_provider_for_model("claude-opus-4-6", "openai-codex")
assert result is not None
assert result[0] not in {"nous",} # nous has claude models but shouldn't be suggested
def test_custom_provider_not_overridden_by_static_catalog(self):
"""When current provider is custom:*, a static-catalog match must NOT
override it — otherwise a model served by the user's own endpoint gets
misattributed to a native provider, rewriting model.provider (#48305).
`gpt-5.4` is in the static openai catalog; with current=custom:foo,
detection must return None instead of switching to openai.
"""
assert detect_provider_for_model("gpt-5.4", "custom:foo") is None
def test_bare_custom_provider_not_overridden_by_static_catalog(self):
"""Same protection for the bare 'custom' provider."""
assert detect_provider_for_model("gpt-5.4", "custom") is None
def test_non_custom_provider_detection_unaffected(self):
"""The custom-provider guard must NOT change detection for non-custom
current providers — a static-catalog model still routes normally."""
result = detect_provider_for_model("gpt-5.4", "openrouter")
assert result is not None and result[0] == "openai"
class TestIsNousFreeTier:
"""Tests for is_nous_free_tier — account tier detection."""
def test_paid_service_access_allowed_true_is_not_free(self):
assert is_nous_free_tier({"paid_service_access": {"allowed": True}}) is False
def test_paid_service_access_allowed_false_is_free(self):
assert is_nous_free_tier({"paid_service_access": {"allowed": False}}) is True
def test_paid_service_access_paid_access_fallback(self):
assert is_nous_free_tier({"paid_service_access": {"paid_access": False}}) is True
def test_paid_plus_tier(self):
assert is_nous_free_tier({"subscription": {"plan": "Plus", "tier": 2, "monthly_charge": 20}}) is False
def test_free_tier_by_charge(self):
assert is_nous_free_tier({"subscription": {"plan": "Free", "tier": 0, "monthly_charge": 0}}) is True
def test_no_charge_field_not_free(self):
"""Missing monthly_charge defaults to not-free (don't block users)."""
assert is_nous_free_tier({"subscription": {"plan": "Free", "tier": 0}}) is False
def test_plan_name_alone_not_free(self):
"""Plan name alone is not enough — monthly_charge is required."""
assert is_nous_free_tier({"subscription": {"plan": "free"}}) is False
def test_empty_subscription_not_free(self):
"""Empty subscription dict defaults to not-free (don't block users)."""
assert is_nous_free_tier({"subscription": {}}) is False
def test_no_subscription_not_free(self):
"""Missing subscription key returns False."""
assert is_nous_free_tier({}) is False
def test_empty_response_not_free(self):
"""Completely empty response defaults to not-free."""
assert is_nous_free_tier({}) is False
class TestPartitionNousModelsByTier:
"""Tests for partition_nous_models_by_tier — free vs paid tier model split."""
_PAID = {"prompt": "0.000003", "completion": "0.000015"}
_FREE = {"prompt": "0", "completion": "0"}
def test_paid_tier_all_selectable(self):
"""Paid users get all models as selectable, none unavailable."""
models = ["anthropic/claude-opus-4.6", "xiaomi/mimo-v2-pro"]
pricing = {"anthropic/claude-opus-4.6": self._PAID, "xiaomi/mimo-v2-pro": self._FREE}
sel, unav = partition_nous_models_by_tier(models, pricing, free_tier=False)
assert sel == models
assert unav == []
def test_free_tier_splits_correctly(self):
"""Free users see only free models; paid ones are unavailable."""
models = ["anthropic/claude-opus-4.6", "xiaomi/mimo-v2-pro", "openai/gpt-5.4"]
pricing = {
"anthropic/claude-opus-4.6": self._PAID,
"xiaomi/mimo-v2-pro": self._FREE,
"openai/gpt-5.4": self._PAID,
}
sel, unav = partition_nous_models_by_tier(models, pricing, free_tier=True)
assert sel == ["xiaomi/mimo-v2-pro"]
assert unav == ["anthropic/claude-opus-4.6", "openai/gpt-5.4"]
def test_no_pricing_returns_all(self):
"""Without pricing data, all models are selectable."""
models = ["anthropic/claude-opus-4.6", "openai/gpt-5.4"]
sel, unav = partition_nous_models_by_tier(models, {}, free_tier=True)
assert sel == models
assert unav == []
def test_all_free_models(self):
"""When all models are free, free-tier users can select all."""
models = ["xiaomi/mimo-v2-pro", "xiaomi/mimo-v2-omni"]
pricing = {m: self._FREE for m in models}
sel, unav = partition_nous_models_by_tier(models, pricing, free_tier=True)
assert sel == models
assert unav == []
def test_all_paid_models(self):
"""When all models are paid, free-tier users have none selectable."""
models = ["anthropic/claude-opus-4.6", "openai/gpt-5.4"]
pricing = {m: self._PAID for m in models}
sel, unav = partition_nous_models_by_tier(models, pricing, free_tier=True)
assert sel == []
assert unav == models
class TestUnionWithPortalFreeRecommendations:
"""Tests for union_with_portal_free_recommendations.
The Portal's freeRecommendedModels endpoint is the source of truth for
what's free *right now* — the in-repo curated list and docs-hosted
manifest can lag. This helper guarantees the picker still surfaces
Portal-flagged free models even when the rest of the catalog is stale.
"""
_PAID = {"prompt": "0.000003", "completion": "0.000015"}
_FREE = {"prompt": "0", "completion": "0"}
def _payload(self, free_models: list[str]) -> dict:
return {
"freeRecommendedModels": [
{"modelName": mid, "displayName": mid} for mid in free_models
],
}
def test_adds_portal_free_model_missing_from_curated(self):
"""A Portal-advertised free model not in curated is appended + priced free."""
curated = ["anthropic/claude-opus-4.6"]
pricing = {"anthropic/claude-opus-4.6": self._PAID}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=self._payload(["qwen/qwen3.6-plus"]),
):
ids, p = union_with_portal_free_recommendations(curated, pricing, "")
# Curated ("HA") models stay first; Portal-only picks follow.
assert ids[0] == "anthropic/claude-opus-4.6"
assert ids[-1] == "qwen/qwen3.6-plus" # appended
# Synthetic free pricing entry created
assert p["qwen/qwen3.6-plus"] == self._FREE
# Existing pricing untouched
assert p["anthropic/claude-opus-4.6"] == self._PAID
def test_does_not_duplicate_curated_entries(self):
"""A Portal free model already in curated is not duplicated."""
curated = ["qwen/qwen3.6-plus", "anthropic/claude-opus-4.6"]
pricing = {
"qwen/qwen3.6-plus": self._FREE,
"anthropic/claude-opus-4.6": self._PAID,
}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=self._payload(["qwen/qwen3.6-plus"]),
):
ids, p = union_with_portal_free_recommendations(curated, pricing, "")
assert ids == curated
assert p == pricing
def test_then_partition_keeps_portal_free_model(self):
"""End-to-end: Portal-flagged free model survives partition."""
# Simulate the broken-state-before-this-fix: in-repo curated list
# contains qwen/qwen3.6-plus (because new builds shipped it) but
# live pricing endpoint hasn't published its zero-cost entry yet.
# The Portal's freeRecommendedModels still flags it as free.
curated = ["qwen/qwen3.6-plus", "anthropic/claude-opus-4.6"]
pricing = {"anthropic/claude-opus-4.6": self._PAID} # qwen missing!
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=self._payload(["qwen/qwen3.6-plus"]),
):
ids, p = union_with_portal_free_recommendations(curated, pricing, "")
sel, unav = partition_nous_models_by_tier(ids, p, free_tier=True)
assert "qwen/qwen3.6-plus" in sel
assert "anthropic/claude-opus-4.6" in unav
def test_empty_payload_returns_inputs_unchanged(self):
"""Empty Portal response leaves curated + pricing untouched."""
curated = ["a", "b"]
pricing = {"a": self._PAID}
with patch("hermes_cli.models.fetch_nous_recommended_models", return_value={}):
ids, p = union_with_portal_free_recommendations(curated, pricing, "")
assert ids == curated
assert p == pricing
def test_missing_freeRecommendedModels_key(self):
"""Portal payload without freeRecommendedModels degrades gracefully."""
curated = ["a"]
pricing = {"a": self._PAID}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value={"paidRecommendedModels": [{"modelName": "x"}]},
):
ids, p = union_with_portal_free_recommendations(curated, pricing, "")
assert ids == curated
assert p == pricing
def test_fetch_failure_returns_inputs(self):
"""Network failures don't blow up the picker."""
curated = ["a"]
pricing = {"a": self._PAID}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
side_effect=RuntimeError("network down"),
):
ids, p = union_with_portal_free_recommendations(curated, pricing, "")
assert ids == curated
assert p == pricing
def test_invalid_entries_skipped(self):
"""Non-dict / missing-modelName entries are filtered out."""
curated = ["a"]
pricing = {"a": self._PAID}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value={
"freeRecommendedModels": [
"not-a-dict",
{"displayName": "no-modelName"},
{"modelName": ""},
{"modelName": "qwen/qwen3.6-plus"},
]
},
):
ids, p = union_with_portal_free_recommendations(curated, pricing, "")
assert ids == ["a", "qwen/qwen3.6-plus"]
assert p["qwen/qwen3.6-plus"] == self._FREE
class TestUnionWithPortalPaidRecommendations:
"""Tests for union_with_portal_paid_recommendations.
Mirror of TestUnionWithPortalFreeRecommendations: the Portal's
paidRecommendedModels endpoint is the source of truth for what's a
blessed paid model *right now*. The in-repo curated list and
docs-hosted manifest can lag — this helper guarantees newly-launched
paid models surface in the picker for paid-tier users without a CLI
release.
"""
_PAID = {"prompt": "0.000003", "completion": "0.000015"}
_FREE = {"prompt": "0", "completion": "0"}
def _payload(self, paid_models: list[str]) -> dict:
return {
"paidRecommendedModels": [
{"modelName": mid, "displayName": mid} for mid in paid_models
],
}
def test_adds_portal_paid_model_missing_from_curated(self):
"""A Portal-advertised paid model not in curated is appended."""
curated = ["anthropic/claude-opus-4.6"]
pricing = {"anthropic/claude-opus-4.6": self._PAID}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=self._payload(["openai/gpt-5.4"]),
):
ids, p = union_with_portal_paid_recommendations(curated, pricing, "")
# Curated ("HA") models stay first; Portal-only picks follow.
assert ids[0] == "anthropic/claude-opus-4.6"
assert ids[-1] == "openai/gpt-5.4" # appended
# Existing pricing untouched
assert p["anthropic/claude-opus-4.6"] == self._PAID
def test_does_not_synthesize_pricing_for_paid_models(self):
"""Paid recommendations missing from live pricing get no synthetic entry.
Synthesizing zero pricing (like the free helper does) would mislead
:func:`partition_nous_models_by_tier` into treating them as free;
synthesizing a non-zero placeholder would lie to the user. The
right thing is to leave pricing absent so the picker shows a blank
column until the live pricing endpoint catches up.
"""
curated = ["anthropic/claude-opus-4.6"]
pricing = {"anthropic/claude-opus-4.6": self._PAID}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=self._payload(["openai/gpt-5.4"]),
):
_, p = union_with_portal_paid_recommendations(curated, pricing, "")
assert "openai/gpt-5.4" not in p
assert p["anthropic/claude-opus-4.6"] == self._PAID
def test_does_not_duplicate_curated_entries(self):
"""A Portal paid model already in curated is not duplicated."""
curated = ["openai/gpt-5.4", "anthropic/claude-opus-4.6"]
pricing = {
"openai/gpt-5.4": self._PAID,
"anthropic/claude-opus-4.6": self._PAID,
}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=self._payload(["openai/gpt-5.4"]),
):
ids, p = union_with_portal_paid_recommendations(curated, pricing, "")
assert ids == curated
assert p == pricing
def test_empty_payload_returns_inputs_unchanged(self):
"""Empty Portal response leaves curated + pricing untouched."""
curated = ["a", "b"]
pricing = {"a": self._PAID}
with patch("hermes_cli.models.fetch_nous_recommended_models", return_value={}):
ids, p = union_with_portal_paid_recommendations(curated, pricing, "")
assert ids == curated
assert p == pricing
def test_missing_paidRecommendedModels_key(self):
"""Portal payload without paidRecommendedModels degrades gracefully."""
curated = ["a"]
pricing = {"a": self._PAID}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value={"freeRecommendedModels": [{"modelName": "x"}]},
):
ids, p = union_with_portal_paid_recommendations(curated, pricing, "")
assert ids == curated
assert p == pricing
def test_fetch_failure_returns_inputs(self):
"""Network failures don't blow up the picker."""
curated = ["a"]
pricing = {"a": self._PAID}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
side_effect=RuntimeError("network down"),
):
ids, p = union_with_portal_paid_recommendations(curated, pricing, "")
assert ids == curated
assert p == pricing
def test_invalid_entries_skipped(self):
"""Non-dict / missing-modelName entries are filtered out."""
curated = ["a"]
pricing = {"a": self._PAID}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value={
"paidRecommendedModels": [
"not-a-dict",
{"displayName": "no-modelName"},
{"modelName": ""},
{"modelName": "openai/gpt-5.4"},
]
},
):
ids, p = union_with_portal_paid_recommendations(curated, pricing, "")
assert ids == ["a", "openai/gpt-5.4"]
# No synthetic entry — pricing is untouched.
assert "openai/gpt-5.4" not in p
def test_preserves_relative_order_of_new_paid_models(self):
"""Multiple new paid models are appended in payload order, after curated."""
curated = ["anthropic/claude-opus-4.6"]
pricing = {"anthropic/claude-opus-4.6": self._PAID}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=self._payload(["openai/gpt-5.4", "openai/gpt-5.5"]),
):
ids, _ = union_with_portal_paid_recommendations(curated, pricing, "")
assert ids == [
"anthropic/claude-opus-4.6",
"openai/gpt-5.4",
"openai/gpt-5.5",
]
class TestCheckNousFreeTierCache:
"""Tests for the TTL cache on check_nous_free_tier()."""
def setup_method(self):
_models_mod._free_tier_cache = None
def teardown_method(self):
_models_mod._free_tier_cache = None
@patch("hermes_cli.nous_account.get_nous_portal_account_info")
def test_result_is_cached(self, mock_account):
"""Second call within TTL returns cached result without account lookup."""
mock_account.return_value = NousPortalAccountInfo(
logged_in=True,
source="jwt",
fresh=False,
paid_service_access=False,
)
result1 = check_nous_free_tier()
result2 = check_nous_free_tier()
assert result1 is True
assert result2 is True
assert mock_account.call_count == 1
@patch("hermes_cli.nous_account.get_nous_portal_account_info")
def test_cache_expires_after_ttl(self, mock_account):
"""After TTL expires, account info is resolved again."""
mock_account.return_value = NousPortalAccountInfo(
logged_in=True,
source="jwt",
fresh=False,
paid_service_access=True,
)
result1 = check_nous_free_tier()
assert mock_account.call_count == 1
cached_result, cached_at = _models_mod._free_tier_cache
_models_mod._free_tier_cache = (cached_result, cached_at - _FREE_TIER_CACHE_TTL - 1)
result2 = check_nous_free_tier()
assert mock_account.call_count == 2
assert result1 is False
assert result2 is False
@patch("hermes_cli.nous_account.get_nous_portal_account_info")
def test_force_fresh_bypasses_cache(self, mock_account):
mock_account.return_value = NousPortalAccountInfo(
logged_in=True,
source="account_api",
fresh=True,
paid_service_access=True,
)
assert check_nous_free_tier() is False
assert check_nous_free_tier(force_fresh=True) is False
assert mock_account.call_count == 2
mock_account.assert_called_with(force_fresh=True)
def test_cache_ttl_is_short(self):
"""TTL should be short enough to catch upgrades quickly (<=5 min)."""
assert _FREE_TIER_CACHE_TTL <= 300
class TestNousRecommendedModels:
"""Tests for fetch_nous_recommended_models + get_nous_recommended_aux_model."""
_SAMPLE_PAYLOAD = {
"paidRecommendedModels": [],
"freeRecommendedModels": [],
"paidRecommendedCompactionModel": None,
"paidRecommendedVisionModel": None,
"freeRecommendedCompactionModel": {
"modelName": "google/gemini-3-flash-preview",
"displayName": "Google: Gemini 3 Flash Preview",
},
"freeRecommendedVisionModel": {
"modelName": "google/gemini-3-flash-preview",
"displayName": "Google: Gemini 3 Flash Preview",
},
}
def setup_method(self):
_models_mod._nous_recommended_cache.clear()
def teardown_method(self):
_models_mod._nous_recommended_cache.clear()
def _mock_urlopen(self, payload):
"""Return a context-manager mock mimicking urllib.request.urlopen()."""
import json as _json
response = MagicMock()
response.read.return_value = _json.dumps(payload).encode()
cm = MagicMock()
cm.__enter__.return_value = response
cm.__exit__.return_value = False
return cm
def test_fetch_caches_per_portal_url(self):
from hermes_cli.models import fetch_nous_recommended_models
mock_cm = self._mock_urlopen(self._SAMPLE_PAYLOAD)
with patch("hermes_cli.models._urlopen_model_catalog_request", return_value=mock_cm) as mock_urlopen:
a = fetch_nous_recommended_models("https://portal.example.com")
b = fetch_nous_recommended_models("https://portal.example.com")
assert a == self._SAMPLE_PAYLOAD
assert b == self._SAMPLE_PAYLOAD
assert mock_urlopen.call_count == 1 # second call served from cache
def test_fetch_cache_is_keyed_per_portal(self):
from hermes_cli.models import fetch_nous_recommended_models
mock_cm = self._mock_urlopen(self._SAMPLE_PAYLOAD)
with patch("hermes_cli.models._urlopen_model_catalog_request", return_value=mock_cm) as mock_urlopen:
fetch_nous_recommended_models("https://portal.example.com")
fetch_nous_recommended_models("https://portal.staging-nousresearch.com")
assert mock_urlopen.call_count == 2 # different portals → separate fetches
def test_fetch_returns_empty_on_network_failure(self):
from hermes_cli.models import fetch_nous_recommended_models
with patch("hermes_cli.models._urlopen_model_catalog_request", side_effect=OSError("boom")):
result = fetch_nous_recommended_models("https://portal.example.com")
assert result == {}
def test_fetch_force_refresh_bypasses_cache(self):
from hermes_cli.models import fetch_nous_recommended_models
mock_cm = self._mock_urlopen(self._SAMPLE_PAYLOAD)
with patch("hermes_cli.models._urlopen_model_catalog_request", return_value=mock_cm) as mock_urlopen:
fetch_nous_recommended_models("https://portal.example.com")
fetch_nous_recommended_models("https://portal.example.com", force_refresh=True)
assert mock_urlopen.call_count == 2
def test_get_aux_model_returns_vision_recommendation(self):
from hermes_cli.models import get_nous_recommended_aux_model
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=self._SAMPLE_PAYLOAD,
):
# Free tier → free vision recommendation.
model = get_nous_recommended_aux_model(vision=True, free_tier=True)
assert model == "google/gemini-3-flash-preview"
def test_get_aux_model_returns_compaction_recommendation(self):
from hermes_cli.models import get_nous_recommended_aux_model
payload = dict(self._SAMPLE_PAYLOAD)
payload["freeRecommendedCompactionModel"] = {"modelName": "minimax/minimax-m2.7"}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=payload,
):
model = get_nous_recommended_aux_model(vision=False, free_tier=True)
assert model == "minimax/minimax-m2.7"
def test_get_aux_model_returns_none_when_field_null(self):
from hermes_cli.models import get_nous_recommended_aux_model
payload = dict(self._SAMPLE_PAYLOAD)
payload["freeRecommendedCompactionModel"] = None
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=payload,
):
model = get_nous_recommended_aux_model(vision=False, free_tier=True)
assert model is None
def test_get_aux_model_returns_none_on_empty_payload(self):
from hermes_cli.models import get_nous_recommended_aux_model
with patch("hermes_cli.models.fetch_nous_recommended_models", return_value={}):
assert get_nous_recommended_aux_model(vision=False, free_tier=True) is None
assert get_nous_recommended_aux_model(vision=True, free_tier=False) is None
def test_get_aux_model_returns_none_when_modelname_blank(self):
from hermes_cli.models import get_nous_recommended_aux_model
payload = {"freeRecommendedCompactionModel": {"modelName": " "}}
with patch(
"hermes_cli.models.fetch_nous_recommended_models",
return_value=payload,
):
assert get_nous_recommended_aux_model(vision=False, free_tier=True) is None
def test_paid_tier_prefers_paid_recommendation(self):
"""Paid-tier users should get the paid model when it's populated."""
from hermes_cli.models import get_nous_recommended_aux_model
payload = {
"paidRecommendedCompactionModel": {"modelName": "anthropic/claude-opus-4.7"},
"freeRecommendedCompactionModel": {"modelName": "google/gemini-3-flash-preview"},
"paidRecommendedVisionModel": {"modelName": "openai/gpt-5.4"},
"freeRecommendedVisionModel": {"modelName": "google/gemini-3-flash-preview"},
}
with patch("hermes_cli.models.fetch_nous_recommended_models", return_value=payload):
text = get_nous_recommended_aux_model(vision=False, free_tier=False)
vision = get_nous_recommended_aux_model(vision=True, free_tier=False)
assert text == "anthropic/claude-opus-4.7"
assert vision == "openai/gpt-5.4"
def test_paid_tier_falls_back_to_free_when_paid_is_null(self):
"""If the Portal returns null for the paid field, fall back to free."""
from hermes_cli.models import get_nous_recommended_aux_model
payload = {
"paidRecommendedCompactionModel": None,
"freeRecommendedCompactionModel": {"modelName": "google/gemini-3-flash-preview"},
"paidRecommendedVisionModel": None,
"freeRecommendedVisionModel": {"modelName": "google/gemini-3-flash-preview"},
}
with patch("hermes_cli.models.fetch_nous_recommended_models", return_value=payload):
text = get_nous_recommended_aux_model(vision=False, free_tier=False)
vision = get_nous_recommended_aux_model(vision=True, free_tier=False)
assert text == "google/gemini-3-flash-preview"
assert vision == "google/gemini-3-flash-preview"
def test_free_tier_never_uses_paid_recommendation(self):
"""Free-tier users must not get paid-only recommendations."""
from hermes_cli.models import get_nous_recommended_aux_model
payload = {
"paidRecommendedCompactionModel": {"modelName": "anthropic/claude-opus-4.7"},
"freeRecommendedCompactionModel": None, # no free recommendation
}
with patch("hermes_cli.models.fetch_nous_recommended_models", return_value=payload):
model = get_nous_recommended_aux_model(vision=False, free_tier=True)
# Free tier must return None — never leak the paid model.
assert model is None
def test_auto_detects_tier_when_not_supplied(self):
"""Default behaviour: call check_nous_free_tier() to pick the tier."""
from hermes_cli.models import get_nous_recommended_aux_model
payload = {
"paidRecommendedCompactionModel": {"modelName": "paid-model"},
"freeRecommendedCompactionModel": {"modelName": "free-model"},
}
with (
patch("hermes_cli.models.fetch_nous_recommended_models", return_value=payload),
patch("hermes_cli.models.check_nous_free_tier", return_value=True),
):
assert get_nous_recommended_aux_model(vision=False) == "free-model"
with (
patch("hermes_cli.models.fetch_nous_recommended_models", return_value=payload),
patch("hermes_cli.models.check_nous_free_tier", return_value=False),
):
assert get_nous_recommended_aux_model(vision=False) == "paid-model"
def test_tier_detection_error_defaults_to_paid(self):
"""If tier detection raises, assume paid so we don't downgrade silently."""
from hermes_cli.models import get_nous_recommended_aux_model
payload = {
"paidRecommendedCompactionModel": {"modelName": "paid-model"},
"freeRecommendedCompactionModel": {"modelName": "free-model"},
}
with (
patch("hermes_cli.models.fetch_nous_recommended_models", return_value=payload),
patch("hermes_cli.models.check_nous_free_tier", side_effect=RuntimeError("boom")),
):
assert get_nous_recommended_aux_model(vision=False) == "paid-model"
class TestCodexSoftAcceptPlausibilityGate:
"""#45006 kernel (b): the openai-codex / xai-oauth hidden-model soft-accept
(#16172 / #19729) must only accept slugs that plausibly belong to that
provider's family. An undeclared, unrelated typed name (e.g. a local model
name) must be REJECTED with actionable --provider guidance instead of being
fake-accepted as a hidden Codex/Grok model (which would 400 on the next turn
and mislabel the provider as 'OpenAI Codex')."""
def test_unrelated_name_rejected_on_openai_codex(self):
from hermes_cli.models import validate_requested_model
r = validate_requested_model("qwen3.5-4b", "openai-codex")
assert r["accepted"] is False
assert r["persist"] is False
assert "--provider" in (r["message"] or "")
def test_unrelated_name_rejected_on_xai_oauth(self):
from hermes_cli.models import validate_requested_model
r = validate_requested_model("llama-3.1-8b", "xai-oauth")
assert r["accepted"] is False
assert "--provider" in (r["message"] or "")
def test_family_shaped_hidden_slug_still_soft_accepted_codex(self):
"""#16172 intent preserved: a gpt-/codex-shaped unknown slug is still
soft-accepted (entitlement-gated hidden models)."""
from hermes_cli.models import validate_requested_model
r = validate_requested_model("gpt-5.9-codex-hidden", "openai-codex")
assert r["accepted"] is True
assert r["recognized"] is False
def test_family_shaped_hidden_slug_still_soft_accepted_xai(self):
from hermes_cli.models import validate_requested_model
r = validate_requested_model("grok-9-hidden", "xai-oauth")
assert r["accepted"] is True
assert r["recognized"] is False
def test_real_catalog_model_unaffected(self):
from hermes_cli.models import validate_requested_model
r = validate_requested_model("gpt-5.5", "openai-codex")
assert r["accepted"] is True
assert r["recognized"] is True