hermes - 💡(How to fix) Fix Model picker canonical ordering buries the active custom provider [2 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When the desktop/TUI model picker requests build_models_payload(..., canonical_order=True), the active provider can lose its first-place ordering if it is a user-defined/custom provider. Canonical providers such as OpenRouter are moved ahead of it, so searching for a model available in both places can visually nudge the user toward the aggregator row instead of the provider currently serving the session.

This is a display-order / selection-footgun issue, not a backend routing issue: picker selections still pass an explicit provider slug.

Root Cause

When the desktop/TUI model picker requests build_models_payload(..., canonical_order=True), the active provider can lose its first-place ordering if it is a user-defined/custom provider. Canonical providers such as OpenRouter are moved ahead of it, so searching for a model available in both places can visually nudge the user toward the aggregator row instead of the provider currently serving the session.

This is a display-order / selection-footgun issue, not a backend routing issue: picker selections still pass an explicit provider slug.

Fix Action

Fixed

Code Example

model:
  provider: custom-gpt
  default: gpt-5.5

providers:
  custom-gpt:
    name: custom-gpt
    base_url: https://example.invalid/v1
    api_key: "${CUSTOM_GPT_API_KEY}"
    models:
      gpt-5.5: {}

---

"is_current": ep_name == current_provider,
"is_user_defined": True,
"models": models_list,
"source": "user-config",

---

results.sort(key=lambda r: (not r["is_current"], -r["total_models"]))

---

canon = sorted(
    (r for r in rows if r["slug"] in order),
    key=lambda r: order[r["slug"]],
)
extras = [r for r in rows if r["slug"] not in order]
return canon + extras

---

current = [r for r in rows if r.get("is_current")]
current_ids = {id(r) for r in current}
remaining = [r for r in rows if id(r) not in current_ids]
canon = sorted(
    (r for r in remaining if r["slug"] in order),
    key=lambda r: order[r["slug"]],
)
extras = [r for r in remaining if r["slug"] not in order]
return current + canon + extras

---

def test_canonical_order_pins_current_provider_before_canonical_rows():
    rows = [
        {"slug": "openrouter", "name": "OpenRouter",
         "models": ["openai/gpt-5.5"], "total_models": 1,
         "is_current": False, "is_user_defined": False,
         "source": "built-in"},
        {"slug": "custom-gpt", "name": "custom-gpt",
         "models": ["gpt-5.5"], "total_models": 1,
         "is_current": True, "is_user_defined": True,
         "source": "user-config"},
    ]
    ctx = _empty_ctx()
    with _list_auth_returning(rows):
        payload = build_models_payload(
            ctx,
            include_unconfigured=True,
            picker_hints=True,
            canonical_order=True,
        )

    slugs = [r["slug"] for r in payload["providers"]]
    assert slugs[0] == "custom-gpt"
    assert slugs.index("custom-gpt") < slugs.index("openrouter")
RAW_BUFFERClick to expand / collapse

Summary

When the desktop/TUI model picker requests build_models_payload(..., canonical_order=True), the active provider can lose its first-place ordering if it is a user-defined/custom provider. Canonical providers such as OpenRouter are moved ahead of it, so searching for a model available in both places can visually nudge the user toward the aggregator row instead of the provider currently serving the session.

This is a display-order / selection-footgun issue, not a backend routing issue: picker selections still pass an explicit provider slug.

Repro

Use a config with an active user-defined provider exposing a bare model name, and also authenticate OpenRouter:

model:
  provider: custom-gpt
  default: gpt-5.5

providers:
  custom-gpt:
    name: custom-gpt
    base_url: https://example.invalid/v1
    api_key: "${CUSTOM_GPT_API_KEY}"
    models:
      gpt-5.5: {}

Also set OPENROUTER_API_KEY or otherwise authenticate OpenRouter.

Then open the desktop/TUI model picker and search gpt-5.5.

Actual Behavior

The user-defined active provider row can be moved below canonical providers. If OpenRouter exposes openai/gpt-5.5, the picker can show the OpenRouter row before the active custom provider row.

Expected Behavior

The active provider should remain visible first, then canonical providers should follow in canonical declaration order, then remaining custom/user-defined rows.

Why It Happens

list_authenticated_providers() correctly marks user-defined provider rows as current and includes their configured model list:

"is_current": ep_name == current_provider,
"is_user_defined": True,
"models": models_list,
"source": "user-config",

It also sorts current rows first:

results.sort(key=lambda r: (not r["is_current"], -r["total_models"]))

But the TUI picker calls build_models_payload() with canonical_order=True. The current _reorder_canonical() implementation puts canonical rows before every non-canonical row, discarding the current-provider priority:

canon = sorted(
    (r for r in rows if r["slug"] in order),
    key=lambda r: order[r["slug"]],
)
extras = [r for r in rows if r["slug"] not in order]
return canon + extras

Proposed Fix

Preserve current rows before applying canonical ordering:

current = [r for r in rows if r.get("is_current")]
current_ids = {id(r) for r in current}
remaining = [r for r in rows if id(r) not in current_ids]
canon = sorted(
    (r for r in remaining if r["slug"] in order),
    key=lambda r: order[r["slug"]],
)
extras = [r for r in remaining if r["slug"] not in order]
return current + canon + extras

Regression Test

def test_canonical_order_pins_current_provider_before_canonical_rows():
    rows = [
        {"slug": "openrouter", "name": "OpenRouter",
         "models": ["openai/gpt-5.5"], "total_models": 1,
         "is_current": False, "is_user_defined": False,
         "source": "built-in"},
        {"slug": "custom-gpt", "name": "custom-gpt",
         "models": ["gpt-5.5"], "total_models": 1,
         "is_current": True, "is_user_defined": True,
         "source": "user-config"},
    ]
    ctx = _empty_ctx()
    with _list_auth_returning(rows):
        payload = build_models_payload(
            ctx,
            include_unconfigured=True,
            picker_hints=True,
            canonical_order=True,
        )

    slugs = [r["slug"] for r in payload["providers"]]
    assert slugs[0] == "custom-gpt"
    assert slugs.index("custom-gpt") < slugs.index("openrouter")

Notes

The compact shell model menu appears to sort provider groups separately, so this report is scoped to the model picker payload path rather than every model display surface.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING