Skip to content

fix(model-switch): resolve bare model names via custom_providers catalog#13760

Closed
rzbdz wants to merge 2 commits into
NousResearch:mainfrom
rzbdz:fix/custom-provider-model-switch
Closed

fix(model-switch): resolve bare model names via custom_providers catalog#13760
rzbdz wants to merge 2 commits into
NousResearch:mainfrom
rzbdz:fix/custom-provider-model-switch

Conversation

@rzbdz

@rzbdz rzbdz commented Apr 22, 2026

Copy link
Copy Markdown

Problem

When a user types /model claude-sonnet-4.6 while on a custom provider (e.g. smt-claude), the switch fails with:

✗ Note: `claude-sonnet-4.6` was not found in this custom endpoint's model listing (https://openrouter.ai/api/v1/models).
  Similar models: `anthropic/claude-sonnet-4.6`, ...

The session then silently falls back to the custom provider group's default model instead of switching. Three root causes:

  1. cli.py: _handle_model_switch only loaded custom_providers in the picker path (no-args). When a model name was given, switch_model was called with custom_providers=None.

  2. model_switch.py: No step searched the user's custom_providers models catalog before falling through to a live API probe. The probe could hit the wrong URL (e.g. OpenRouter) if base_url state was stale.

  3. model_switch.py: The is_custom guard only matched "custom" and "local" exactly — not custom:smt-claude slugs — so detect_provider_for_model() could incorrectly re-route the model to OpenRouter.

  4. models.py: validate_requested_model didn't treat custom:* slugs as "custom", so named custom providers fell through to the generic error path.

Fix

  • _find_model_in_custom_providers() — new helper that scans every custom_providers entry's models dict/list for a matching model name. Matching is case-insensitive and normalises dots↔hyphens (claude-sonnet-4.6 matches claude-sonnet-4-6).
  • Inserted as step c2 in PATH B of switch_model() — runs before the live API probe, so configured models always win.
  • is_custom guard extended to cover custom:* slugs.
  • cli.py loads custom_provs unconditionally before calling switch_model().
  • validate_requested_model normalises custom:*"custom".

Testing

5 new tests in tests/hermes_cli/test_model_switch_custom_providers.py:

  • test_find_model_in_custom_providers_dict_format
  • test_find_model_in_custom_providers_singular_model
  • test_find_model_in_custom_providers_no_match
  • test_switch_model_bare_name_resolves_via_custom_providers
  • test_switch_model_bare_name_dot_hyphen_normalisation

All 2440 existing tests pass (2 pre-existing failures in test_gateway_service.py are unrelated).

TroyMitchell911 and others added 2 commits April 21, 2026 17:38
Three related bugs caused custom_providers per-model context_length
overrides to be ignored, particularly when switching models at runtime:

1. __init__: self._config_context_length was assigned *before* the
   custom_providers lookup, so per-model overrides were never stored
   on the agent instance.  Moved the assignment after the lookup.

2. switch_model: re-resolve config_context_length from config.yaml
   for the new (model, base_url) pair instead of reusing the stale
   value from __init__.  Checks both top-level model.context_length
   and custom_providers per-model overrides.

3. get_model_context_length: when a custom endpoint's /models response
   omits context_length, fall through to models.dev / OpenRouter /
   hardcoded defaults instead of returning 128K immediately.  Proxy
   endpoints (LiteLLM, NewAPI, etc.) often omit context_length but
   serve well-known models whose metadata is available downstream.

Signed-off-by: Troy Mitchell <i@troy-y.org>
When a user types /model <name> while on a custom provider, the pipeline
previously fell through to a live API probe against the endpoint's /models
listing. If the endpoint returned model IDs in a different format (or the
probe hit OpenRouter's URL due to stale state), validation failed and the
switch was rejected — leaving the user stuck on the default model.

Three fixes:

1. model_switch.py: Add _find_model_in_custom_providers() helper that
   scans every custom_providers entry's models dict/list for a matching
   model name (case-insensitive, dots-vs-hyphens normalised). Insert it
   as step c2 in PATH B so bare names like 'claude-sonnet-4.6' resolve
   to the correct custom:smt-claude provider before hitting live probing.

2. model_switch.py: Extend the is_custom guard (step e) to cover
   custom:* slugs, preventing detect_provider_for_model() from
   incorrectly routing custom provider models to OpenRouter/Anthropic.

3. cli.py: Load custom_provs before calling switch_model() in
   _handle_model_switch(), not just in the picker path. Previously the
   switch call always received custom_providers=None, so step c2 never
   had data to search.

4. models.py: Treat custom:* slugs identically to 'custom' in
   validate_requested_model() so named custom providers probe their own
   endpoint rather than falling through to the generic error path.
@rzbdz rzbdz closed this Apr 22, 2026
@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard comp/agent Core agent loop, run_agent.py, prompt builder area/config Config system, migrations, profiles labels Apr 22, 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/agent Core agent loop, run_agent.py, prompt builder comp/cli CLI entry point, hermes_cli/, setup wizard type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants