fix(models): OpenRouter :nitro/:floor routing variants no longer rejected by /model validation
OpenRouter's :nitro, :floor, :exacto, and :online suffixes are request-time routing modifiers valid on any model id — /models lists only the base model. validate_requested_model() compared the full suffixed id against the listing, so a valid variant was either rejected outright or fuzzy-auto-corrected to the base id, silently stripping the user's routing opt-in. Now, for OpenRouter only, a recognized variant suffix validates the BASE id against the live listing (and the curated-catalog soft-accept and static- catalog fallback paths) while preserving the suffixed id for persistence and API requests — checked BEFORE fuzzy correction. :free/:batch/:thinking remain direct catalog SKUs and keep exact-match semantics; unknown suffixes and unknown bases are still rejected. Reported by JEB (Jakob's Hermes Agent) via Discord.
This commit is contained in:
+59
-1
@@ -3325,6 +3325,30 @@ _AGGREGATOR_PROVIDERS = frozenset(
|
||||
{"nous", "openrouter", "ai-gateway", "copilot", "kilocode"}
|
||||
)
|
||||
|
||||
# OpenRouter request-time routing variants (docs: guides/routing/model-variants).
|
||||
# These suffixes are per-request routing modifiers valid on ANY model id —
|
||||
# ":nitro" sorts the endpoint pool by throughput and admits priority-tier
|
||||
# endpoints, ":floor" sorts by price and admits flex-tier endpoints, ":exacto"
|
||||
# applies quality-first provider sorting, ":online" attaches the web plugin.
|
||||
# They are never separate catalog entries: /models lists only the base id.
|
||||
# NOT in this set: ":free", ":batch", ":thinking", ":extended" — those ARE
|
||||
# distinct catalog SKUs that appear in /models when they exist, so absence
|
||||
# from the listing is authoritative for them and the direct-membership check
|
||||
# above handles the valid ones.
|
||||
_OPENROUTER_VARIANT_SUFFIXES = frozenset({"nitro", "floor", "exacto", "online"})
|
||||
|
||||
|
||||
def _openrouter_variant_base(model_id: str) -> Optional[str]:
|
||||
"""Return the base model id when ``model_id`` carries a recognized
|
||||
OpenRouter routing-variant suffix (e.g. ``x-ai/grok-4:nitro`` →
|
||||
``x-ai/grok-4``), else ``None``."""
|
||||
base, sep, suffix = (model_id or "").rpartition(":")
|
||||
if not sep or not base:
|
||||
return None
|
||||
if suffix.lower() in _OPENROUTER_VARIANT_SUFFIXES:
|
||||
return base
|
||||
return None
|
||||
|
||||
# Subscription/OAuth providers whose catalogs RE-EXPOSE other vendors' models
|
||||
# would be listed here (tried only as a last resort for bare short-alias
|
||||
# resolution, after every native-vendor catalog, so they never hijack an alias
|
||||
@@ -6812,6 +6836,24 @@ def validate_requested_model(
|
||||
"recognized": True,
|
||||
"message": None,
|
||||
}
|
||||
# OpenRouter routing variants (":nitro", ":floor", ...) are request-time
|
||||
# modifiers, not catalog entries — /models lists only the base id.
|
||||
# Validate the BASE against the listing but preserve the suffixed id,
|
||||
# and do this BEFORE fuzzy auto-correction: get_close_matches would
|
||||
# otherwise "correct" `model:nitro` → `model` and silently strip the
|
||||
# user's routing opt-in.
|
||||
_variant_base = (
|
||||
_openrouter_variant_base(requested_for_lookup)
|
||||
if normalized == "openrouter"
|
||||
else None
|
||||
)
|
||||
if _variant_base is not None and _variant_base in set(api_models):
|
||||
return {
|
||||
"accepted": True,
|
||||
"persist": True,
|
||||
"recognized": True,
|
||||
"message": None,
|
||||
}
|
||||
else:
|
||||
# API responded but model is not listed. Accept anyway —
|
||||
# the user may have access to models not shown in the public
|
||||
@@ -6853,7 +6895,8 @@ def validate_requested_model(
|
||||
|
||||
_openai_listing_is_authoritative = is_official_openai_host(base_url)
|
||||
if not _openai_listing_is_authoritative and _model_in_provider_catalog(
|
||||
requested_for_lookup.lower(), _provider_keys(normalized)
|
||||
(_variant_base or requested_for_lookup).lower(),
|
||||
_provider_keys(normalized),
|
||||
):
|
||||
return {
|
||||
"accepted": True,
|
||||
@@ -6936,6 +6979,21 @@ def validate_requested_model(
|
||||
"recognized": True,
|
||||
"message": None,
|
||||
}
|
||||
# OpenRouter routing-variant suffixes: validate the base id against
|
||||
# the catalog, keep the suffixed id (same rule as the live-listing
|
||||
# path above — variants never appear as catalog entries).
|
||||
if normalized == "openrouter":
|
||||
_cat_variant_base = _openrouter_variant_base(requested_for_lookup)
|
||||
if (
|
||||
_cat_variant_base is not None
|
||||
and _cat_variant_base.lower() in catalog_lower
|
||||
):
|
||||
return {
|
||||
"accepted": True,
|
||||
"persist": True,
|
||||
"recognized": True,
|
||||
"message": None,
|
||||
}
|
||||
catalog_lower_list = list(catalog_lower.keys())
|
||||
auto = get_close_matches(
|
||||
requested_for_lookup.lower(), catalog_lower_list, n=1, cutoff=0.9
|
||||
|
||||
@@ -594,3 +594,84 @@ class TestProbeApiModelsUserAgent:
|
||||
assert req.get_header("Authorization") is None
|
||||
|
||||
|
||||
|
||||
|
||||
# -- validate — OpenRouter routing-variant suffixes (:nitro / :floor / ...) ----
|
||||
|
||||
class TestValidateOpenRouterVariantSuffixes:
|
||||
"""OpenRouter's `:nitro`, `:floor`, `:exacto`, `:online` are request-time
|
||||
routing modifiers, not catalog models — /models lists only the base id.
|
||||
Validation must accept `base:variant` when `base` is listed, preserve the
|
||||
suffixed id (no auto-correct stripping the routing opt-in), and still
|
||||
reject variants on unknown bases and unknown suffixes."""
|
||||
|
||||
_LISTING = [
|
||||
"~x-ai/grok-latest",
|
||||
"x-ai/grok-4.6",
|
||||
"deepseek/deepseek-v4-flash",
|
||||
"thinkingmachines/inkling:free",
|
||||
]
|
||||
|
||||
def _validate(self, model):
|
||||
return _validate(model, "openrouter", api_models=self._LISTING)
|
||||
|
||||
@pytest.mark.parametrize("suffix", ["nitro", "floor", "exacto", "online"])
|
||||
def test_variant_on_listed_base_accepted_unmodified(self, suffix):
|
||||
result = self._validate(f"~x-ai/grok-latest:{suffix}")
|
||||
assert result["accepted"] is True
|
||||
assert result["recognized"] is True
|
||||
assert result.get("corrected_model") is None
|
||||
assert result["message"] is None
|
||||
|
||||
def test_variant_not_fuzzy_corrected_to_base(self):
|
||||
"""The old failure mode: get_close_matches would 'fix' model:nitro
|
||||
to the bare base id and silently drop the routing behavior."""
|
||||
result = self._validate("x-ai/grok-4.6:nitro")
|
||||
assert result["accepted"] is True
|
||||
assert result.get("corrected_model") is None
|
||||
|
||||
def test_variant_on_unknown_base_rejected(self):
|
||||
result = self._validate("x-ai/notreal-model:nitro")
|
||||
assert result["accepted"] is False
|
||||
|
||||
def test_unknown_suffix_keeps_old_behavior(self):
|
||||
result = self._validate("x-ai/grok-4.6:bogus")
|
||||
assert result["accepted"] is False
|
||||
|
||||
def test_free_sku_still_direct_matched(self):
|
||||
"""`:free` SKUs ARE catalog entries; direct membership handles them."""
|
||||
result = self._validate("thinkingmachines/inkling:free")
|
||||
assert result["accepted"] is True
|
||||
assert result.get("corrected_model") is None
|
||||
|
||||
def test_variant_uppercase_suffix_accepted(self):
|
||||
result = self._validate("x-ai/grok-4.6:NITRO")
|
||||
assert result["accepted"] is True
|
||||
assert result.get("corrected_model") is None
|
||||
|
||||
def test_non_openrouter_provider_unaffected(self):
|
||||
"""The variant carve-out is OpenRouter-only; other providers keep
|
||||
their existing behavior for colon-suffixed names."""
|
||||
result = _validate(
|
||||
"x-ai/grok-4.6:nitro",
|
||||
"groq",
|
||||
api_models=["x-ai/grok-4.6"],
|
||||
)
|
||||
assert result.get("corrected_model") != "x-ai/grok-4.6:nitro"
|
||||
|
||||
def test_static_catalog_fallback_accepts_variant(self):
|
||||
"""Gateway path: /models unreachable → static catalog validates the
|
||||
base id and preserves the suffix."""
|
||||
with patch("hermes_cli.models.fetch_api_models", return_value=None), \
|
||||
patch(
|
||||
"hermes_cli.models.provider_model_ids",
|
||||
return_value=["x-ai/grok-4.6", "anthropic/claude-opus-4.6"],
|
||||
):
|
||||
result = validate_requested_model(
|
||||
"x-ai/grok-4.6:floor",
|
||||
"openrouter",
|
||||
base_url="https://openrouter.ai/api/v1",
|
||||
)
|
||||
assert result["accepted"] is True
|
||||
assert result["recognized"] is True
|
||||
assert result.get("corrected_model") is None
|
||||
|
||||
Reference in New Issue
Block a user