Skip to content

bug+feat(models): OpenRouter free-tier variants invisible (tool-support filter) + Custom Model ID input has no provider selector — silent failure #1426

@nesquena-hermes

Description

@nesquena-hermes

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):

  1. 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.
  2. 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:

  1. In the curated manifest (OPENROUTER_MODELS snapshot or hosted catalog), AND
  2. Returned by live /v1/models, AND
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or requesthelp wantedExtra attention is neededsprint-candidateStrong candidate for next sprint

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions