Skip to content

fix: custom provider with slash model name no longer rerouted to OpenRouter - #255

Closed
nesquena-hermes wants to merge 1 commit into
masterfrom
fix/custom-provider-slash-routing
Closed

fix: custom provider with slash model name no longer rerouted to OpenRouter#255
nesquena-hermes wants to merge 1 commit into
masterfrom
fix/custom-provider-slash-routing

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Summary

Fixes a routing bug where custom provider configurations with slash-containing model names (e.g. google/gemma-4-26b-a4b) were silently rerouted to OpenRouter instead of the user's custom endpoint.

Problem

resolve_model_provider() in api/config.py checks custom_providers list entries first, but if a user configures only model.default: google/gemma-4-26b-a4b with provider: custom and base_url: http://... (without a custom_providers list), the code falls through to:

if prefix in _PROVIDER_MODELS and prefix != config_provider:
    return model_id, 'openrouter', None  # BUG: 'google' is in _PROVIDER_MODELS

This routes to OpenRouter, which returns a 401. The CLI doesn't have this problem because it uses a different routing path.

Fix

One guard added before the _PROVIDER_MODELS heuristic in resolve_model_provider():

# If a custom endpoint base_url is configured, trust it entirely.
# Don't apply the OpenRouter slash heuristic — a slash in the model
# name is just part of the model identifier, not a provider routing hint.
if config_base_url:
    return model_id, config_provider, config_base_url

When base_url is explicitly configured, the function returns immediately with the configured provider and URL. The OpenRouter slash heuristic only fires when no base_url is set.

Tests

Added test_custom_endpoint_slash_model_routes_to_custom_not_openrouter to tests/test_model_resolver.py:

  • Verifies provider=custom + base_url + google/gemma-4-26b-a4b → routes to custom, not openrouter
  • Verifies provider=openrouter + same model still routes to openrouter (no regression)

625 passed, 0 failed, 0 skipped (up from 624 — +1 new test)

Fixes #230

…Router

When base_url is configured, resolve_model_provider() now trusts the
configured provider/base_url and skips the slash-based OpenRouter heuristic.
Fixes the case where model: google/gemma-4-26b-a4b with provider: custom
would be silently routed to OpenRouter, resulting in 401 errors.

Fixes #230
@nesquena

Copy link
Copy Markdown
Owner

Full Review: PR #255 — custom provider slash model routing fix

Security Audit

Clean. One logic branch added to resolve_model_provider() — when config_base_url is set and the model has a slash, return as-is instead of rerouting to OpenRouter. No new inputs, no new endpoints.

Code Review

The fix is correct and minimal:

if config_base_url:
    return model_id, config_provider, config_base_url

This runs after the OpenRouter early-return (which preserves slash models for OpenRouter) and after the prefix-match check (which strips provider prefixes for direct API). The logic is: "if the user configured a base_url, trust their routing — don't interpret a slash in the model name as OpenRouter syntax."

The regression test is thorough — tests both the custom provider case (should NOT go to OpenRouter) and the OpenRouter case (should still work as before). Good boundary coverage.

This fixes the same issue as external PR #189 (@smurmann) via a different mechanism. #189 checks custom_providers config entries; this one checks config_base_url. This approach is simpler (5 lines vs 14) and covers the broader case — any provider with a base_url, not just explicitly declared custom providers.

Tests

574 passed, 0 failed. (1 pre-existing failure from unmerged #243.)

Stacked Branch Note

PRs #256 and #257 are stacked on top of this branch. Merge order: #255 first, then #256, then #257. After #255 merges, #256 and #257 will need rebasing to drop the already-merged commits.

Verdict

Approved. Clean, minimal, correct. Ready to merge.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator Author

Agent review — APPROVED ✅ (merged to stage)

Reviewed full diff and verified the fix in resolve_model_provider(). The guard if config_base_url: return model_id, config_provider, config_base_url is correctly positioned after the openrouter early-return and before the slash-based OpenRouter heuristic — meaning it only fires when a custom endpoint is configured, and openrouter configurations are never affected.

The regression test covers both paths: custom+base_url with slash model → stays custom, openrouter with slash model → still routes to openrouter.

Tests: 625 passed on review branch. Stage total: 645 passed, 0 failed.
Merged to stage branch.

nesquena-hermes pushed a commit that referenced this pull request Apr 11, 2026
…Router (#255)

When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.

Fixes #230
nesquena-hermes added a commit that referenced this pull request Apr 11, 2026
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)

When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.

Fixes #230

* test: mobile layout regression suite — 14 tests for every QA run (#254)

Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.

* feat: /skills slash command lists and filters available Hermes skills (#257)

Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.

Fixes #248

* feat: shared app dialogs replace native confirm()/prompt() calls (#251)

Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.

Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.

Extracted from PR #242.

* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)

Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.

Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.

Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.

Fixes #247 Fixes #246

* feat: session ⋯ action dropdown replaces per-row buttons (#252)

Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.

5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.

Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.

Extracted from PR #242.

* docs: v0.47.0 release notes, bump version, update test counts (645)

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
@nesquena-hermes

Copy link
Copy Markdown
Collaborator Author

Merged to master via stage branch in PR #258 (v0.47.0).

@nesquena-hermes
nesquena-hermes deleted the fix/custom-provider-slash-routing branch April 12, 2026 03:14
JKJameson pushed a commit to JKJameson/hermes-webui that referenced this pull request Apr 25, 2026
* fix: custom provider with slash model name no longer rerouted to OpenRouter (nesquena#255)

When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.

Fixes nesquena#230

* test: mobile layout regression suite — 14 tests for every QA run (nesquena#254)

Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.

* feat: /skills slash command lists and filters available Hermes skills (nesquena#257)

Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.

Fixes nesquena#248

* feat: shared app dialogs replace native confirm()/prompt() calls (nesquena#251)

Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.

Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.

Extracted from PR nesquena#242.

* fix: Android Chrome mobile — workspace panel close + profile dropdown (nesquena#256)

Fix nesquena#247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.

Fix nesquena#246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.

Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.

Fixes nesquena#247 Fixes nesquena#246

* feat: session ⋯ action dropdown replaces per-row buttons (nesquena#252)

Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.

5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.

Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.

Extracted from PR nesquena#242.

* docs: v0.47.0 release notes, bump version, update test counts (645)

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
* fix: custom provider with slash model name no longer rerouted to OpenRouter (nesquena#255)

When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.

Fixes nesquena#230

* test: mobile layout regression suite — 14 tests for every QA run (nesquena#254)

Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.

* feat: /skills slash command lists and filters available Hermes skills (nesquena#257)

Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.

Fixes nesquena#248

* feat: shared app dialogs replace native confirm()/prompt() calls (nesquena#251)

Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.

Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.

Extracted from PR nesquena#242.

* fix: Android Chrome mobile — workspace panel close + profile dropdown (nesquena#256)

Fix nesquena#247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.

Fix nesquena#246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.

Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.

Fixes nesquena#247 Fixes nesquena#246

* feat: session ⋯ action dropdown replaces per-row buttons (nesquena#252)

Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.

5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.

Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.

Extracted from PR nesquena#242.

* docs: v0.47.0 release notes, bump version, update test counts (645)

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chat is not responding.

2 participants