fix: /api/models/live skips probe for custom providers with model config (#3718) - #3719
DanielMaly wants to merge 4 commits into
Conversation
The /api/models/live handler skipped the live /v1/models probe for custom providers when a model: entry existed in custom_providers config. Config IDs were added to the main ids list, so the "if not ids" guard prevented the upstream probe from ever running. Fix: collect config-specified IDs in a separate _config_ids list so the live fetch always runs. Config entries are merged as fallback after the fetch, and used as the full list if the fetch fails.
|
| Filename | Overview |
|---|---|
| api/routes.py | Core fix: separates config-specified model IDs into _config_ids, removes the if not ids guard to always probe upstream, and merges/falls back correctly; timeout replaced with the named constant from config |
| tests/test_issue3718_live_models_custom_probe.py | New regression test file with 6 tests covering merge, fallback, dedup, structural guard, mock-based integration, and timeout-constant usage; tests 1-3 verify locally-replicated logic rather than calling the production handler, but cover the right contract cases |
| CHANGELOG.md | Adds a clear, accurate changelog entry for the #3718 fix describing the previous mis-behavior and the new merge/fallback approach |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["/api/models/live?provider=custom:*"] --> B["provider_model_ids() → []"]
B --> C["_config_ids = []<br/>collect model IDs from custom_providers config"]
C --> D{"provider == 'custom'<br/>or starts with 'custom:'?"}
D -- No --> E["Other provider flow"]
D -- Yes --> F{"_base_url AND<br/>_api_key available?"}
F -- No --> G["ids stays []"]
F -- Yes --> H["HTTP GET /v1/models\ntimeout=CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS"]
H -- Success --> I["ids = parsed live model list"]
H -- Exception --> G
I --> J{"ids non-empty?"}
G --> J
J -- Yes (live succeeded) --> K["Append _config_ids entries<br/>NOT already in live set"]
J -- No (live failed / no creds) --> L["ids = list(_config_ids)"]
K --> M["Return merged model list"]
L --> M
Reviews (4): Last reviewed commit: "fix: address maintainer review feedback ..." | Re-trigger Greptile
…ing checks Replace source-code string assertions with behavioral tests that exercise the merge logic directly (config+live, fallback on failure, dedup). Keep one structural guard to verify the 'if not ids' removal. Addresses Greptile feedback on nesquena#3719.
|
Reading VerificationThe whole change lives inside the if ids:
_live_set = set(ids)
for _cid in _config_ids:
if _cid not in _live_set:
ids.append(_cid)
else:
ids = list(_config_ids)I confirmed the PR's claim that the static Two divergences from the hardened static pathNow that the live fetch runs for every custom provider (not just ones without config entries), it's worth noting the live path doesn't reuse the static path's protections:
Neither blocks the fix — both are pre-existing — but a follow-up that routes the live handler through On the testsThe three behavioral tests reconstruct the merge/fallback algorithm inline rather than invoking |
- Use CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS instead of hardcoded timeout=8 for the custom provider live fetch, matching the static path's timeout cap. - Add mocked integration test that exercises the real fetch/parse/merge path with a fake /v1/models response. - Add timeout constant test to verify the constant is used.
|
Thanks for the review! Pushed a follow-up commit addressing the two actionable points: Timeout mismatch — Replaced the hardcoded Test coverage — Added a mock-based integration test ( SSRF guard — Agree this is a pre-existing gap worth closing. I'll file a follow-up issue for routing the live handler through |
… with model config #3719) (#3747) * fix(#3718): /api/models/live probes upstream for custom providers with model config (#3719) @DanielMaly. Config model IDs were added to the ids list before the 'if not ids:' guard, so a custom provider with a model: field skipped the live /v1/models probe and Settings' refresh returned only the config entry. Now collects config IDs separately, always probes for custom providers, merges live (priority) + config (fallback). Includes the maintainer review follow-ups (CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS constant + behavioral tests). Captured all 3 logical PR commits' net effect; routes.py + test verified byte-identical to the PR head. + CHANGELOG v0.51.298. * test(#3718): remove unused BytesIO import (ruff F401) * test(#3719): update timeout assertion to CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS The #3719 maintainer-review commit replaced the hardcoded urlopen timeout=8 with the CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS constant (5.0). test_named_custom_live_fetch_uses_matching_entry_endpoint asserted the old literal 8. Reference the constant directly now so the assertion can't drift again. Not a behavior change — only the live-probe timeout value (8s -> 5s) moved, URL + auth unchanged. --------- Co-authored-by: nesquena-hermes <[email protected]>
|
Thanks @(author) — this fix shipped in v0.51.298. Your change was cherry-picked onto the release stage during the 2026-06-06 sweep (live model probe for custom providers with model config — stage-3719), so this PR is now redundant against master (it shows as conflicting because the fix is already present). Closing as indirectly merged with full attribution. Appreciate the contribution! 🙏 |
|
(Correcting attribution above: thanks @DanielMaly for this — credited in the release CHANGELOG.) |
… with model config nesquena#3719) (nesquena#3747) * fix(nesquena#3718): /api/models/live probes upstream for custom providers with model config (nesquena#3719) @DanielMaly. Config model IDs were added to the ids list before the 'if not ids:' guard, so a custom provider with a model: field skipped the live /v1/models probe and Settings' refresh returned only the config entry. Now collects config IDs separately, always probes for custom providers, merges live (priority) + config (fallback). Includes the maintainer review follow-ups (CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS constant + behavioral tests). Captured all 3 logical PR commits' net effect; routes.py + test verified byte-identical to the PR head. + CHANGELOG v0.51.298. * test(nesquena#3718): remove unused BytesIO import (ruff F401) * test(nesquena#3719): update timeout assertion to CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS The nesquena#3719 maintainer-review commit replaced the hardcoded urlopen timeout=8 with the CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS constant (5.0). test_named_custom_live_fetch_uses_matching_entry_endpoint asserted the old literal 8. Reference the constant directly now so the assertion can't drift again. Not a behavior change — only the live-probe timeout value (8s -> 5s) moved, URL + auth unchanged. --------- Co-authored-by: nesquena-hermes <[email protected]>
Problem
/api/models/liveskips the live/v1/modelsprobe forcustomandcustom:*providers when the provider has amodel:field incustom_providersconfig.Root cause: config-specified model IDs were added directly to the
idslist before theif not ids:guard that controls the live fetch. So a provider withmodel: assistantwould setids = ["assistant"], the guard would evaluate to False, and the upstream probe never ran.The Settings refresh button returned only the config entry instead of the full upstream catalog.
Fix
_config_idslist instead of the mainidslistif not idsguard)The static
/api/modelsendpoint inconfig.pyalready handled this correctly — only the live handler had this bug.Testing
tests/test_issue3718_live_models_custom_probe.py(3 assertions)/api/models/live?provider=custom:litellmnow returns full model list (59 models) instead of just the config entryCloses #3718