fix: do not build phantom "Custom" group when active provider is set - #191
fix: do not build phantom "Custom" group when active provider is set#191mbac wants to merge 1 commit into
Conversation
When model.provider is a real provider (e.g. openai-codex) and model.base_url
is configured, hermes_cli reports 'custom' as an authenticated provider. The
WebUI model picker was building a separate "Custom" group for it and parking
the configured default_model there instead of under the active provider's
group — diverging from the TUI which correctly shows the model under its
configured provider.
Two fixes in api/config.py get_available_models():
1. Discard 'custom' from detected_providers when active_provider is set and
isn't 'custom' itself. The base_url belongs to the active provider.
2. Replace the substring-based default-model injection check with an exact
match against _PROVIDER_DISPLAY. The old check `active_provider.lower() in
g.get('provider', '').lower()` silently failed for hyphenated IDs like
'openai-codex' vs display name 'OpenAI Codex' (hyphen vs. space),
falling through to groups[0] and landing the model in the alphabetical
first group instead.
Adds two regression tests in tests/test_model_resolver.py covering both
conditions.
|
Thanks for the detailed writeup — the bug analysis is accurate and the fix is correct. What the bug isTwo independent problems both lurk in 1. Phantom "Custom" group
2. Hyphen/space mismatch in the "ensure default_model appears" pass The original injection check was: if active_provider and active_provider.lower() in g.get('provider', '').lower()For Fix assessmentFix 1 — discard phantom if active_provider and active_provider != 'custom':
detected_providers.discard('custom')This is the right guard. The One edge case worth noting but not a blocker: a user who sets Fix 2 — exact display-name match: target_display = _PROVIDER_DISPLAY.get(active_provider, active_provider or '').lower()
...
if target_display and g.get('provider', '').lower() == target_display:Using Tests:
No blockersThe fix is minimal, well-targeted, and both test cases are solid regression guards. The |
nesquena
left a comment
There was a problem hiding this comment.
Full Review: PR #191 — phantom Custom provider group fix
Thanks @mbac — excellent bug analysis and honest self-assessment. The AI-written code is actually quite solid.
Security Audit
Clean. Changes are limited to model dropdown group construction logic. No external resources, no injection vectors, no XSS.
Code Review
Fix 1: Suppress phantom custom provider — Correct. When active_provider is set (e.g. openai-codex) and isn't custom itself, detected_providers.discard('custom') removes the phantom bucket. The active_provider != 'custom' guard correctly preserves standalone custom providers (Ollama, LM Studio).
Fix 2: Display name matching — Correct. The old substring check active_provider.lower() in g.get('provider', '').lower() broke for openai-codex vs OpenAI Codex (hyphen vs space). The new code uses _PROVIDER_DISPLAY.get(active_provider) for exact display-name matching. This is the right fix — uses the existing mapping table rather than inventing new string manipulation.
To answer your questions:
- The
active_provider != 'custom'guard looks correct — users withmodel.provider: customwill still get their Custom group. - The
_PROVIDER_DISPLAY-based match should cover all providers since_PROVIDER_DISPLAYis the authoritative mapping. If a provider isn't in the dict,active_provideritself is used as fallback, which is reasonable. - The
monkeypatch.setitem(sys.modules, ...)approach is fine for this codebase — it's a clean way to mock hermes_cli without it being installed.
Tests
508 passed, 0 failed, 41 skipped. No regressions. Two well-crafted regression tests — the second one cleverly adds anthropic to catch the groups[0] alphabetical fallback bug.
Merge Order Note
This PR and #189 both touch api/config.py and test_model_resolver.py but different functions. Recommend merging this one first since it's earlier in the pipeline (model group construction), then #189 (model routing) with a trivial rebase.
Verdict
Approved. Solid fix, well-tested, honest contribution. Ready to merge.
Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses nesquena#191 (HTTPS support issue).
Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses #191 (HTTPS support issue).
Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses #191 (HTTPS support issue).
Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses #191 (HTTPS support issue).
… branch (#201) * feat: optional HTTPS/TLS support via cert and key env vars Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses #191 (HTTPS support issue). * fix: use current branch upstream for update checks, not repo default branch The update checker in api/updates.py always compared HEAD against origin/master (or origin/main), which produced false 'N updates available' alerts when the user is on a feature branch and master has moved forward with unrelated commits. Now uses git rev-parse --abbrev-ref @{upstream} to get the current branch's tracking branch for both the behind-count check and the apply-update pull command. Falls back to the default branch if no upstream is set (brand-new local branch with no tracking config). Fixes #200.
* feat: optional HTTPS/TLS support via cert and key env vars Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses #191 (HTTPS support issue). * fix: use current branch upstream for update checks, not repo default branch The update checker in api/updates.py always compared HEAD against origin/master (or origin/main), which produced false 'N updates available' alerts when the user is on a feature branch and master has moved forward with unrelated commits. Now uses git rev-parse --abbrev-ref @{upstream} to get the current branch's tracking branch for both the behind-count check and the apply-update pull command. Falls back to the default branch if no upstream is set (brand-new local branch with no tracking config). Fixes #200. * fix: support CLI sessions in /api/list file browser _handle_list_dir() only checked WebUI in-memory sessions, returning 'Session not found' for CLI sessions imported from the agent's state.db. Now falls back to get_cli_sessions() to find the workspace path for CLI sessions that aren't loaded in WebUI memory. Fixes: workspace pane showing empty for CLI sessions.
|
Rebased onto current master (post v0.41.0, #189 merged). Both fixes are correct and the tests pass. One additional fix in the rebase: 564 tests passing. Ready to merge. |
) Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses nesquena#191 (HTTPS support issue).
… branch (nesquena#201) * feat: optional HTTPS/TLS support via cert and key env vars Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses nesquena#191 (HTTPS support issue). * fix: use current branch upstream for update checks, not repo default branch The update checker in api/updates.py always compared HEAD against origin/master (or origin/main), which produced false 'N updates available' alerts when the user is on a feature branch and master has moved forward with unrelated commits. Now uses git rev-parse --abbrev-ref @{upstream} to get the current branch's tracking branch for both the behind-count check and the apply-update pull command. Falls back to the default branch if no upstream is set (brand-new local branch with no tracking config). Fixes nesquena#200.
* feat: optional HTTPS/TLS support via cert and key env vars Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses nesquena#191 (HTTPS support issue). * fix: use current branch upstream for update checks, not repo default branch The update checker in api/updates.py always compared HEAD against origin/master (or origin/main), which produced false 'N updates available' alerts when the user is on a feature branch and master has moved forward with unrelated commits. Now uses git rev-parse --abbrev-ref @{upstream} to get the current branch's tracking branch for both the behind-count check and the apply-update pull command. Falls back to the default branch if no upstream is set (brand-new local branch with no tracking config). Fixes nesquena#200. * fix: support CLI sessions in /api/list file browser _handle_list_dir() only checked WebUI in-memory sessions, returning 'Session not found' for CLI sessions imported from the agent's state.db. Now falls back to get_cli_sessions() to find the workspace path for CLI sessions that aren't loaded in WebUI memory. Fixes: workspace pane showing empty for CLI sessions.
) Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses nesquena#191 (HTTPS support issue).
… branch (nesquena#201) * feat: optional HTTPS/TLS support via cert and key env vars Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses nesquena#191 (HTTPS support issue). * fix: use current branch upstream for update checks, not repo default branch The update checker in api/updates.py always compared HEAD against origin/master (or origin/main), which produced false 'N updates available' alerts when the user is on a feature branch and master has moved forward with unrelated commits. Now uses git rev-parse --abbrev-ref @{upstream} to get the current branch's tracking branch for both the behind-count check and the apply-update pull command. Falls back to the default branch if no upstream is set (brand-new local branch with no tracking config). Fixes nesquena#200.
* feat: optional HTTPS/TLS support via cert and key env vars Add optional HTTPS support controlled by two env vars: HERMES_WEBUI_TLS_CERT=/path/to/cert.pem HERMES_WEBUI_TLS_KEY=/path/to/key.pem - Wraps server socket with ssl.SSLContext (min TLSv1.2) - Dynamic scheme detection for startup messages (http:// vs https://) - Graceful fallback to HTTP if cert loading fails — server never crashes due to bad TLS config, just prints a warning and continues - Auth cookie Secure flag already set when HTTPS is detected via getpeercert - 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works, fallback on bad paths Addresses nesquena#191 (HTTPS support issue). * fix: use current branch upstream for update checks, not repo default branch The update checker in api/updates.py always compared HEAD against origin/master (or origin/main), which produced false 'N updates available' alerts when the user is on a feature branch and master has moved forward with unrelated commits. Now uses git rev-parse --abbrev-ref @{upstream} to get the current branch's tracking branch for both the behind-count check and the apply-update pull command. Falls back to the default branch if no upstream is set (brand-new local branch with no tracking config). Fixes nesquena#200. * fix: support CLI sessions in /api/list file browser _handle_list_dir() only checked WebUI in-memory sessions, returning 'Session not found' for CLI sessions imported from the agent's state.db. Now falls back to get_cli_sessions() to find the workspace path for CLI sessions that aren't loaded in WebUI memory. Fixes: workspace pane showing empty for CLI sessions.
Summary
When
model.provideris a real provider (e.g.openai-codex) andmodel.base_urlis configured,hermes_cli.models.list_available_providers()reports'custom'as an authenticated provider because thebase_urlmakes the "custom endpoint" path available. The WebUI model picker was then building a separate "Custom" group for it and parking the configureddefault_modelthere, while the configured provider's group (e.g. "OpenAI Codex") listed only its hardcoded_PROVIDER_MODELSentries. The TUI does not have this problem because it treatsmodel.provider+model.defaultas a single source of truth.Repro (real config)
Before this patch,
/api/modelsreturns:After:
What the patch does
Two small edits in
api/config.pyinsideget_available_models():customdetected provider whenactive_provideris set and isn'tcustomitself — thebase_urlbelongs to the active provider, not a separate bucket._PROVIDER_DISPLAY. The previous checkactive_provider.lower() in g.get('provider', '').lower()silently returnedFalsefor hyphenated provider IDs vs. space-separated display names ('openai-codex' in 'openai codex'isFalse), falling through togroups[0]and landing the model in the alphabetical first group.Two regression tests added in
tests/test_model_resolver.py. The second one mocks three authenticated providers (anthropic,openai-codex,custom) specifically so thatanthropicsorts beforeopenai-codex— this catches the fallback-to-groups[0] bug that a two-provider mock would miss.This patch was written by Claude Opus 4.6 (Anthropic) during an investigation session. I (the GitHub user opening this PR) am not a confident-enough Python developer to vouch for the correctness of the change myself. The AI agent traced the bug, reproduced it via
get_available_models()directly, and proposed this fix — but I can't independently verify edge cases.I'd appreciate if maintainers could review the code carefully before merging, especially:
'custom'fromdetected_providerscould affect users whose config legitimately wants a standalone Custom group (e.g.model.provider: customwith a local Ollama endpoint — theactive_provider != 'custom'guard is meant to preserve this, but please double-check)._PROVIDER_DISPLAY-based injection match covers every provider ID in_PROVIDER_DISPLAYwithout breaking the existingtest_no_duplicate_when_default_model_is_prefixedand similar tests.monkeypatch.setitem(sys.modules, …)approach is idiomatic for this codebase, or if you'd prefer a different stubbing style.Happy to iterate on the approach — just let me know what changes you'd like.
Test plan
pytest tests/test_model_resolver.py -vpasses (16/16, including the two new tests)pytest tests/ -qpasses (508 passed, 41 skipped — the skipped tests all require hermes-agent which isn't in the test env)gpt-5.4now appears under "OpenAI Codex" in the composer model dropdown, with no phantom "Custom" group.🤖 Patch generated with Claude Code (Opus 4.6)