Summary
Two related gaps in the model picker that block users from selecting OpenRouter's free-tier models (and other variants that don't appear in the default catalog):
- OpenRouter
:free and other tool-supportless variants are silently filtered out. hermes_cli/models.py::fetch_openrouter_models() (line 940) intersects three sources — curated remote manifest, live /v1/models, and a tool-support filter — and the user has no UI hint that filtering happened or how to override it.
- The "Custom model ID" input has no provider field. Typing a custom ID like
minimax/minimax-m2.5:free routes via namespace inference, which sends it to MiniMax direct API (the minimax/ namespace owns its own provider) instead of the user's intended OpenRouter routing. The chat then fails silently because MiniMax direct doesn't recognize the :free variant or has no API key configured.
This is a sub-issue of #1240 (Provider/Model source-of-truth umbrella).
Reported by
@AvidFuturist (Discord, May 1 2026):
- "some models do not show up, (e.g. openrouter free tier like
minimax/minimax-m2.5:free unless it is intentional). I can provide a custom model ID, but that does not let me pick provider thus it fails silently"
Root cause walkthrough
Why :free variants are invisible
hermes_cli/models.py:940-1005 (fetch_openrouter_models):
# 1. Curated manifest fetch
remote = get_curated_openrouter_models() # hosted catalog
fallback = list(remote) if remote else list(OPENROUTER_MODELS)
preferred_ids = [mid for mid, _ in fallback]
# 2. Live /v1/models fetch
with urllib.request.urlopen("https://openrouter.ai/api/v1/models", ...) as resp:
payload = json.loads(resp.read().decode())
live_by_id = {item["id"]: item for item in payload["data"]}
# 3. Intersection: only IDs in BOTH curated manifest AND live response
for preferred_id in preferred_ids:
live_item = live_by_id.get(preferred_id)
if live_item is None:
continue
# 4. Tool-support filter (Kilo-Org/kilocode#9068 port)
if not _openrouter_model_supports_tools(live_item):
continue
...
So a model is shown only if it's:
- In the curated manifest (
OPENROUTER_MODELS snapshot or hosted catalog), AND
- Returned by live
/v1/models, AND
- Has
tools in supported_parameters per OpenRouter's metadata
minimax/minimax-m2.5:free may fail step 1 (not in the curated manifest) OR step 3 (OpenRouter's metadata doesn't list tools). Either way the user sees nothing in the picker — no entry, no warning, no hint that filtering occurred.
The tool-support filter is correct in intent — surfacing a non-tool model leads to immediate runtime failures when hermes-agent issues a tool call. But the implementation has no escape hatch and no UI feedback.
Why custom model ID fails silently
static/ui.js:625-742 builds the "Custom model ID" input. On submit (_applyCustom at :725):
const _applyCustom=()=>{const v=_ci.value.trim();if(!v)return;selectModelFromDropdown(v);_ci.value='';};
selectModelFromDropdown(v) (line 740) creates a temp <option> with the bare value and calls sel.onchange(). There's no provider field anywhere in this flow.
The value then hits the resolution chain in api/config.py::_apply_provider_prefix (line 863) and api/streaming.py calling resolve_model_provider. The resolution rules (hermes_cli/runtime_provider.py and hermes_cli/providers.py:340):
- Namespace
minimax/ → MiniMax direct provider
- Namespace
openrouter/ → OpenRouter
- Namespace
ollama-cloud/ → Ollama Cloud
- No namespace → falls back to active provider in
config.yaml
So minimax/minimax-m2.5:free is parsed as "MiniMax direct, model minimax-m2.5:free" — wrong provider. The user wanted OpenRouter's proxy of MiniMax's free model. The correct ID would be @openrouter:minimax/minimax-m2.5:free (the explicit @provider: prefix syntax discussed in #1253), but the picker doesn't expose that syntax and most users won't know it exists.
The chat then fails because MiniMax direct either has no API key configured or returns 404/400 for the unknown :free suffix. The error gets swallowed by the chat error path or surfaces as a generic "Provider returned error."
Proposed fixes (two parts, can ship independently)
Fix 1 — Custom model ID input gets a provider selector
Modify static/ui.js:629 (the _custRow HTML):
_custRow.innerHTML = `
<select class="model-custom-provider">
<option value="">Detect from ID</option>
<option value="openrouter">OpenRouter</option>
<option value="anthropic">Anthropic</option>
<option value="openai">OpenAI</option>
<option value="ollama-cloud">Ollama Cloud</option>
... (configured providers only)
</select>
<input class="model-custom-input" type="text"
placeholder="e.g. minimax/minimax-m2.5:free" ...>
<button class="model-custom-btn">Use</button>
`;
_applyCustom then prefixes @<provider>: before the value when a provider is selected:
const _applyCustom = () => {
const v = _ci.value.trim();
if (!v) return;
const prov = _custProvSelect.value;
const final = prov ? `@${prov}:${v}` : v;
selectModelFromDropdown(final);
_ci.value = '';
};
This routes the custom ID through the existing @provider:model syntax (#1253 work, landed in v0.50.255 via PR #1390). Picker doesn't need to know about routing details — just produces the canonical form.
Provider list should be only the configured providers (sourced from the existing <optgroup> labels in <select id="modelSelect">).
Scope: ~25 LOC in static/ui.js + a few i18n keys.
Fix 2 — Surface filtered OpenRouter models with a "Show all" escape hatch
Two sub-changes:
A. Pass the filtered-out OpenRouter IDs from hermes_cli/models.py up to the WebUI as metadata, so the picker can show them in a collapsed "Filtered out (no tool support reported)" section:
// At bottom of OpenRouter group in dropdown:
[+ 12 more models without tool-call support — show anyway]
Clicking expands the section. Selecting one of those models shows a yellow warning banner: "This model may not support tool calls — chats may fail."
B. Add an environment / config override to skip the tool-support filter entirely:
# config.yaml
providers:
openrouter:
skip_tool_support_filter: true # show all models
Or OPENROUTER_SKIP_TOOL_FILTER=1 env var. For power users who know what they're doing.
C. As a smaller alternative to A: when :free or other curated-but-unfiltered variants exist on OpenRouter's live catalog but get filtered by tool-support, log them at agent startup so users can see what was hidden. Less invasive than UI changes but requires log inspection.
Scope:
- A: ~50 LOC (back-end metadata + front-end render)
- B: ~10 LOC config plumbing
- C: ~5 LOC log statement
Recommend B + C first; A is a follow-up if reports continue.
Test shape
For Fix 1:
- Unit: in
tests/test_byok_model_dropdown.py or a new file, simulate _applyCustom with provider="openrouter" and value="some/model" → assert selectModelFromDropdown("@openrouter:some/model") is called
- Manual: type
minimax/minimax-m2.5:free with provider=OpenRouter → confirm chat routes through OpenRouter, not MiniMax direct
For Fix 2:
tests/test_credential_pool_providers.py already covers OpenRouter resolution — add a test for the skip-tool-filter override
- Manual: with
skip_tool_support_filter: true, confirm :free and other previously-hidden models appear in the picker
Scope
- Fix 1 (custom ID provider selector): ~25 LOC, front-end only, low risk
- Fix 2B+2C (OpenRouter filter override + log): ~15 LOC across
hermes_cli/models.py + WebUI
- Fix 2A (filtered-out collapsed section): ~50 LOC, larger surface
Cross-links
Summary
Two related gaps in the model picker that block users from selecting OpenRouter's free-tier models (and other variants that don't appear in the default catalog):
:freeand other tool-supportless variants are silently filtered out.hermes_cli/models.py::fetch_openrouter_models()(line 940) intersects three sources — curated remote manifest, live/v1/models, and a tool-support filter — and the user has no UI hint that filtering happened or how to override it.minimax/minimax-m2.5:freeroutes via namespace inference, which sends it to MiniMax direct API (theminimax/namespace owns its own provider) instead of the user's intended OpenRouter routing. The chat then fails silently because MiniMax direct doesn't recognize the:freevariant or has no API key configured.This is a sub-issue of #1240 (Provider/Model source-of-truth umbrella).
Reported by
@AvidFuturist (Discord, May 1 2026):
minimax/minimax-m2.5:freeunless it is intentional). I can provide a custom model ID, but that does not let me pick provider thus it fails silently"Root cause walkthrough
Why
:freevariants are invisiblehermes_cli/models.py:940-1005(fetch_openrouter_models):So a model is shown only if it's:
OPENROUTER_MODELSsnapshot or hosted catalog), AND/v1/models, ANDtoolsinsupported_parametersper OpenRouter's metadataminimax/minimax-m2.5:freemay fail step 1 (not in the curated manifest) OR step 3 (OpenRouter's metadata doesn't listtools). Either way the user sees nothing in the picker — no entry, no warning, no hint that filtering occurred.The tool-support filter is correct in intent — surfacing a non-tool model leads to immediate runtime failures when hermes-agent issues a tool call. But the implementation has no escape hatch and no UI feedback.
Why custom model ID fails silently
static/ui.js:625-742builds the "Custom model ID" input. On submit (_applyCustomat:725):selectModelFromDropdown(v)(line 740) creates a temp<option>with the bare value and callssel.onchange(). There's no provider field anywhere in this flow.The value then hits the resolution chain in
api/config.py::_apply_provider_prefix(line 863) andapi/streaming.pycallingresolve_model_provider. The resolution rules (hermes_cli/runtime_provider.pyandhermes_cli/providers.py:340):minimax/→ MiniMax direct provideropenrouter/→ OpenRouterollama-cloud/→ Ollama Cloudconfig.yamlSo
minimax/minimax-m2.5:freeis parsed as "MiniMax direct, modelminimax-m2.5:free" — wrong provider. The user wanted OpenRouter's proxy of MiniMax's free model. The correct ID would be@openrouter:minimax/minimax-m2.5:free(the explicit@provider:prefix syntax discussed in #1253), but the picker doesn't expose that syntax and most users won't know it exists.The chat then fails because MiniMax direct either has no API key configured or returns 404/400 for the unknown
:freesuffix. The error gets swallowed by the chat error path or surfaces as a generic "Provider returned error."Proposed fixes (two parts, can ship independently)
Fix 1 — Custom model ID input gets a provider selector
Modify
static/ui.js:629(the_custRowHTML):_applyCustomthen prefixes@<provider>:before the value when a provider is selected:This routes the custom ID through the existing
@provider:modelsyntax (#1253 work, landed in v0.50.255 via PR #1390). Picker doesn't need to know about routing details — just produces the canonical form.Provider list should be only the configured providers (sourced from the existing
<optgroup>labels in<select id="modelSelect">).Scope: ~25 LOC in
static/ui.js+ a few i18n keys.Fix 2 — Surface filtered OpenRouter models with a "Show all" escape hatch
Two sub-changes:
A. Pass the filtered-out OpenRouter IDs from
hermes_cli/models.pyup to the WebUI as metadata, so the picker can show them in a collapsed "Filtered out (no tool support reported)" section:Clicking expands the section. Selecting one of those models shows a yellow warning banner: "This model may not support tool calls — chats may fail."
B. Add an environment / config override to skip the tool-support filter entirely:
Or
OPENROUTER_SKIP_TOOL_FILTER=1env var. For power users who know what they're doing.C. As a smaller alternative to A: when
:freeor other curated-but-unfiltered variants exist on OpenRouter's live catalog but get filtered by tool-support, log them at agent startup so users can see what was hidden. Less invasive than UI changes but requires log inspection.Scope:
Recommend B + C first; A is a follow-up if reports continue.
Test shape
For Fix 1:
tests/test_byok_model_dropdown.pyor a new file, simulate_applyCustomwith provider="openrouter" and value="some/model" → assertselectModelFromDropdown("@openrouter:some/model")is calledminimax/minimax-m2.5:freewith provider=OpenRouter → confirm chat routes through OpenRouter, not MiniMax directFor Fix 2:
tests/test_credential_pool_providers.pyalready covers OpenRouter resolution — add a test for the skip-tool-filter overrideskip_tool_support_filter: true, confirm:freeand other previously-hidden models appear in the pickerScope
hermes_cli/models.py+ WebUICross-links