Skip to content

fix(model-switch): keep bare custom endpoint visible when not the active provider - #59808

Open
TheTom wants to merge 4 commits into
NousResearch:mainfrom
TheTom:fix/bare-custom-endpoint-picker-59702
Open

fix(model-switch): keep bare custom endpoint visible when not the active provider#59808
TheTom wants to merge 4 commits into
NousResearch:mainfrom
TheTom:fix/bare-custom-endpoint-picker-59702

Conversation

@TheTom

@TheTom TheTom commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes the bare model.provider: custom + model.base_url picker row (no named custom_providers:/providers: entry) so it stays visible in the model picker regardless of which provider the current session happens to be on.

Previously, section 3b of list_authenticated_providers only built this row when current_provider == "custom", i.e. only while it was the session's active model. As soon as the session switched to any other provider, the row disappeared and never marked itself in seen_slugs, so the unconfigured-canonical-provider fallback in inventory.py silently inserted a misleading 0-model "run hermes model to configure" placeholder in its place, even though the endpoint was fully configured and reachable.

Named custom_providers: entries never had this problem, since section 4 surfaces them unconditionally.

I verified this against a live Desktop instance by capturing the raw model.options WebSocket frame (via CDP, Network domain) at the exact moment the picker showed the row missing, and confirmed the payload itself carried models: [] for the custom slug. This rules out a rendering/clipping issue: the frontend was correctly reflecting empty data, not hiding present data.

Related Issue

Fixes #59702

Changes Made

  • hermes_cli/model_switch.py: section 3b now falls back to reading the static model: config block directly when the caller-supplied current_provider/current_base_url do not identify a bare custom endpoint (i.e. the session is on a different provider). Also adds a live /models probe in that fallback path (mirroring section 4's policy) since there is no caller-supplied current_model to rely on there, and computes is_current by comparison instead of hardcoding it to True.
  • tests/hermes_cli/test_model_switch_custom_providers.py: two backend regressions for the fix. test_list_authenticated_providers_bare_custom_endpoint_survives_provider_switch fails on current main and passes with this change (verified both ways); the second pins that the config-sourced fallback defers to a named custom_providers entry covering the same base_url instead of duplicating it.
  • apps/desktop/src/app/shell/model-menu-panel.custom-providers.test.tsx (new): isolated ModelMenuPanel test using the exact live payload shape from Desktop model picker shows only some named custom_providers entries even though backend data is correct #59702 (one bare custom endpoint plus two named custom_providers entries), exercised through both the REST fallback and the real gateway.request path. These pass against current main as well, pinning down that the panel renders a complete payload correctly and the bug was the payload itself.

How to Test

  1. Configure model.provider: custom + model.base_url in config.yaml without a matching custom_providers:/providers: entry, then switch the active session to a different provider (e.g. an OAuth provider).
  2. Open the Desktop model picker and confirm the "Custom endpoint" row still appears with its configured model, instead of a 0-model "run hermes model to configure" placeholder.
  3. pytest tests/hermes_cli/test_model_switch_custom_providers.py -q (35 passed; the new survives-provider-switch regression fails when run against main's model_switch.py) and the broader pytest tests/hermes_cli/ -q -k "model_switch or model_options or custom_provider" (226 passed).
  4. npm run test:ui -- src/app/shell/model-menu-panel.custom-providers.test.tsx (3 passed), plus eslint and prettier on the new test file.

Checklist

  • My commit messages follow Conventional Commits (fix(scope):)
  • I searched for existing PRs to make sure this isn't a duplicate (fix(desktop): keep model menu rows visible #59803 addresses a different, renderer-side theory for the same issue; this PR fixes the underlying data bug I confirmed via a live WebSocket frame capture)
  • My PR contains only changes related to this fix
  • I've run pytest tests/hermes_cli/test_model_switch_custom_providers.py -q and all tests pass
  • I've added tests for my changes
  • Tested on macOS

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) area/config Config system, migrations, profiles P2 Medium — degraded but workaround exists labels Jul 6, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: competing fix for #59702. This PR fixes the bare-custom picker row at the backend data source (hermes_cli/model_switch.py section 3b) and cites live CDP evidence that the WebSocket payload itself carried models: [] (ruling out frontend clipping). #59803 fixes the same issue from the desktop frontend (Radix scroll clipping in model-menu-panel.tsx). Flagging both so a maintainer can decide whether the root cause is backend data (this PR) or frontend rendering (#59803), or whether both are needed.

@TheTom
TheTom force-pushed the fix/bare-custom-endpoint-picker-59702 branch from 1cee780 to a797f3f Compare July 6, 2026 21:13
TheTom added a commit to TheTom/hermes-go that referenced this pull request Jul 10, 2026
Context-window override leak (NousResearch#62153, fixed here),
session model switches persisting globally (NousResearch#61192),
and bare custom endpoints hidden in the picker (NousResearch#59808,
cherry-pick branch on this fork).

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracing this to the shared backend inventory path. The reported premise is still present on current main: hermes_cli/model_switch.py:2131-2146 emits a bare custom row only when the live provider is custom, while tui_gateway/server.py:12962-12968 overlays picker context with the live session provider.

Problems

  • Blocking: PR hermes_cli/model_switch.py:2115 leaves a truthy legacy scalar model: value as a string, then PR line 2119 calls _model_cfg.get(...). Current hermes_cli/inventory.py:94-97 explicitly supports scalar config.model values, so opening a picker with a non-custom live provider can raise AttributeError.

Suggested changes

  • Normalize the loaded model value to {} unless it is a dict, and add a scalar-config regression test alongside the new provider-switch test.

The patch otherwise applies cleanly to current main (git apply --check). This is an automated hermes-sweeper review.

Comment thread hermes_cli/model_switch.py Outdated
else:
try:
from hermes_cli.config import load_config as _load_config_3b
_model_cfg = _load_config_3b().get("model") or {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load_config() can return a legacy scalar model: value; current hermes_cli/inventory.py:94-97 explicitly supports that shape. Guard this value with isinstance(..., dict) before the .get() calls below, or a non-custom live provider will make picker listing raise AttributeError.

@TheTom

TheTom commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 2 fixups addressing hermes-sweeper's blocking finding:

  • hermes_cli/model_switch.py 3b fallback normalizes config['model'] to {} unless it's a dict before calling .get() — a legacy bare-string model: config (see inventory.py's ConfigContext loader) was crashing with AttributeError whenever the live session was on a non-custom provider.
  • Dropped the needless load_config as _load_config_3b alias, matches the plain load_config() local-import pattern used elsewhere in this file.
  • Added test_list_authenticated_providers_tolerates_legacy_scalar_model_config regression test.

pytest tests/hermes_cli/test_model_switch_custom_providers.py -q — 38 passed.

@TheTom
TheTom force-pushed the fix/bare-custom-endpoint-picker-59702 branch from c706ef0 to c9a810b Compare July 15, 2026 20:21
TheTom added 4 commits July 15, 2026 15:24
…ive provider

The bare model.provider: custom + model.base_url form only surfaced its
picker row when it matched the current session's active provider. Once a
session switched to any other model, the row vanished and never marked
itself seen, so the unconfigured-canonical-provider fallback silently
inserted a misleading 0-model placeholder in its place instead.

Named custom_providers entries never had this problem since they are
surfaced unconditionally. This adds the same static config.yaml lookup
for the bare endpoint case, falls back to a live /models probe (mirroring
section 4's policy) when the caller has no current_model to rely on, and
computes is_current from an actual comparison instead of hardcoding it.

Verified end to end against a live Desktop instance: captured the raw
model.options WebSocket frame at the exact moment the row was missing and
confirmed the payload itself carried models: [] for the custom slug, not a
rendering issue. Also adds an isolated ModelMenuPanel test proving the
frontend grouping and render logic was not at fault.

Fixes NousResearch#59702
…lback

Adds the backend regression the fix actually changes behavior for: the
bare custom row must survive the session being on a different provider
(fails on main, passes with the fix), and the config-sourced fallback
must defer to a named custom_providers entry covering the same base_url.

Also tightens the ModelMenuPanel test: asserts the exact humanized model
display names instead of hedged either-form checks, and corrects the
header comment to say what these tests pin down (the panel renders a
complete payload correctly; the NousResearch#59702 bug was the payload itself).
hermes-sweeper flagged that the config-sourced bare-custom-endpoint
fallback called .get() on config['model'] unconditionally. Older
configs can carry a bare string there (see inventory.py's
ConfigContext loader), which raised AttributeError and crashed the
picker whenever the live session was on a non-custom provider.
@TheTom
TheTom force-pushed the fix/bare-custom-endpoint-picker-59702 branch from c9a810b to adb66ba Compare July 15, 2026 20:24
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop model picker shows only some named custom_providers entries even though backend data is correct

3 participants