Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions hermes_cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
("nvidia/nemotron-3-super-120b-a12b", ""),
# OpenRouter routers
("openrouter/pareto-code", "auto-routes to cheapest coder meeting openrouter.min_coding_score"),
("openrouter/fusion", "multi-model panel + judge deliberation (priced as sum of all calls)"),
# Free tier
("openrouter/elephant-alpha", "free"),
("openrouter/owl-alpha", "free"),
Expand Down Expand Up @@ -1287,6 +1288,21 @@ def _openrouter_model_is_free(pricing: Any) -> bool:
return False


# OpenRouter "router" model aliases (openrouter/<router>). These are not models
# themselves - they dispatch each request to an underlying tool-capable model -
# so the router alias advertises ``supported_parameters: []`` in /api/v1/models
# even though the request that reaches the chosen model fully supports tools.
# The plain tool-support filter below would therefore drop every router from the
# picker (openrouter/pareto-code is already shipped and hits exactly this),
# leaving the curated entry inert. Treat known routers as tool-capable so they
# survive the filter. Keep this list explicit (not a prefix glob) so a future
# non-router ``openrouter/*`` model that genuinely lacks tools is still filtered.
_OPENROUTER_TOOL_CAPABLE_ROUTERS = frozenset({
"openrouter/pareto-code",
"openrouter/fusion",
})


def _openrouter_model_supports_tools(item: Any) -> bool:
"""Return True when the model's ``supported_parameters`` advertise tool calling.

Expand All @@ -1301,10 +1317,19 @@ def _openrouter_model_supports_tools(item: Any) -> bool:
so the picker doesn't silently empty for those users. Only hide models
whose ``supported_parameters`` is an explicit list that omits ``tools``.

**Router aliases are tool-capable.** OpenRouter router models
(``openrouter/pareto-code``, ``openrouter/fusion``) report an empty
``supported_parameters`` because the alias itself isn't the executor - it
routes to an underlying tool-capable model. Without this allowance the
filter silently drops them from the picker. See
``_OPENROUTER_TOOL_CAPABLE_ROUTERS`` above.

Ported from Kilo-Org/kilocode#9068.
"""
if not isinstance(item, dict):
return True
if str(item.get("id") or "").strip() in _OPENROUTER_TOOL_CAPABLE_ROUTERS:
return True
params = item.get("supported_parameters")
if not isinstance(params, list):
# Field absent / malformed / None — be permissive.
Expand Down
73 changes: 73 additions & 0 deletions tests/hermes_cli/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,79 @@ def test_empty_supported_parameters_list_drops_model(self):
{"id": "x", "supported_parameters": []}
) is False

def test_openrouter_router_aliases_are_tool_capable(self):
"""OpenRouter router aliases advertise supported_parameters:[] but are
tool-capable (they route to a tool-capable model). They must survive the
filter even with an empty list - otherwise the curated picker entry is
inert. openrouter/pareto-code ships today and hits exactly this."""
from hermes_cli.models import _openrouter_model_supports_tools
for router_id in ("openrouter/pareto-code", "openrouter/fusion"):
assert _openrouter_model_supports_tools(
{"id": router_id, "supported_parameters": []}
) is True, router_id

def test_non_router_openrouter_model_without_tools_still_dropped(self):
"""The router allowance is an explicit allowlist, not an openrouter/* glob:
a non-router openrouter/* model that genuinely omits tools is still dropped."""
from hermes_cli.models import _openrouter_model_supports_tools
assert _openrouter_model_supports_tools(
{"id": "openrouter/some-image-model", "supported_parameters": []}
) is False


class TestOpenRouterFusionCurated:
"""openrouter/fusion is a curated, selectable OpenRouter model."""

def test_fusion_in_openrouter_models_snapshot(self):
from hermes_cli.models import OPENROUTER_MODELS
ids = [mid for mid, _ in OPENROUTER_MODELS]
assert "openrouter/fusion" in ids

def test_fusion_survives_live_catalog_filter(self, monkeypatch):
"""End-to-end picker path: even when the live /api/v1/models entry for
openrouter/fusion advertises supported_parameters:[] (its real shape
today), it must still appear in the curated picker output."""
import hermes_cli.models as _models_mod

class _Resp:
def __enter__(self):
return self

def __exit__(self, exc_type, exc, tb):
return False

def read(self):
return (
b'{"data":['
b'{"id":"openrouter/fusion","pricing":{"prompt":"0","completion":"0"},'
b'"supported_parameters":[]},'
b'{"id":"anthropic/claude-opus-4.6","pricing":{"prompt":"0.000015","completion":"0.000075"},'
b'"supported_parameters":["tools"]}'
b']}'
)

monkeypatch.setattr(
_models_mod,
"OPENROUTER_MODELS",
[
("anthropic/claude-opus-4.6", ""),
("openrouter/fusion", "multi-model panel + judge deliberation"),
],
)
monkeypatch.setattr(_models_mod, "_openrouter_catalog_cache", None)
# Force the in-repo fallback list (skip the remote curated manifest,
# which is imported lazily from hermes_cli.model_catalog inside the fn).
monkeypatch.setattr(
"hermes_cli.model_catalog.get_curated_openrouter_models",
lambda *a, **k: None,
raising=False,
)
with patch("hermes_cli.models.urllib.request.urlopen", return_value=_Resp()):
models = _models_mod.fetch_openrouter_models(force_refresh=True)

ids = [mid for mid, _ in models]
assert "openrouter/fusion" in ids


class TestFindOpenrouterSlug:
def test_exact_match(self):
Expand Down
4 changes: 4 additions & 0 deletions website/static/api/model-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@
"id": "openrouter/pareto-code",
"description": "auto-routes to cheapest coder meeting openrouter.min_coding_score"
},
{
"id": "openrouter/fusion",
"description": "multi-model panel + judge deliberation (priced as sum of all calls)"
},
{
"id": "openrouter/elephant-alpha",
"description": "free"
Expand Down
Loading