From e7e856cd794ecc84dc9e79d6dba79f7f9e365e75 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sun, 14 Jun 2026 19:56:12 -0700 Subject: [PATCH] fix(providers): keep custom provider when model id has known-provider prefix (#4210) (#4220) * fix: keep custom provider when model id has known-provider prefix (#4210) Siblings of #3872 (v0.51.349, custom provider with a configured base_url preserves intrinsic routing prefixes). The base_url unset variant of the same bug was missed: with provider=custom or custom: and no base_url set, a model id like 'x-ai/grok-2' or 'google/gemma-2' was hijacked to provider='openrouter', base_url=None. The backend then failed to initialize with 'RuntimeError: No LLM provider configured' because the user had no OpenRouter credentials configured. resolve_model_provider() now applies the same _is_custom exemption the config_base_url branch already carries to the cross-provider redirect at the end of the slash branch. The custom provider keeps the request and the full model id; real first-party providers pointed at an OpenAI-compatible proxy (e.g. provider=openai + base_url=litellm) are unaffected because their strip path is in the base_url branch and the 'prefix == config_provider' branch above the change. Verified: - test_model_resolver.py 32 passed (30 existing + 2 new RED tests) - test_custom_provider_bare_model_reasoning.py 34 passed - Pre-existing F401 (api/config.py:23,24,28) are not introduced by this change; same 3 errors exist on upstream/master. Refs #4210 * chore(release): reposition #4210 CHANGELOG entry under v0.51.426 (OM) --------- Co-authored-by: b3nw Co-authored-by: nesquena-hermes --- CHANGELOG.md | 6 ++++++ api/config.py | 10 ++++++++- tests/test_model_resolver.py | 39 ++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d0a8f8b..5244c587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.426] — 2026-06-15 — Release OM (custom-provider model-prefix routing fix, #4210) + +### Fixed + +- **Custom providers with no `base_url` no longer get hijacked to OpenRouter when the model id has a known-provider prefix.** A bare `custom` or named `custom:` provider pointed at a vendor-routing proxy (e.g. `custom:llm-proxy` with no configured `base_url`, since the proxy URL is supplied at request time) used to receive an OpenRouter redirect for model ids like `x-ai/grok-2` or `google/gemma-2` whenever the prefix was a member of `_PROVIDER_MODELS`. The backend then failed to initialize with `RuntimeError: No LLM provider configured` because the user had no OpenRouter credentials configured. `resolve_model_provider()` now applies the same custom-provider exemption the `config_base_url` branch already carries (sibling of #3872 / v0.51.349), so the request stays on the user's selected custom provider with the full model id preserved. (#4210) + ## [v0.51.425] — 2026-06-15 — Release OL (data-integrity batch: empty-pending guard #4205 + cron-reply fix #3975) ### Fixed diff --git a/api/config.py b/api/config.py index 22d32da4..c0073975 100644 --- a/api/config.py +++ b/api/config.py @@ -2187,7 +2187,15 @@ def resolve_model_provider(model_id: str) -> tuple: # If prefix does NOT match config provider, the user picked a cross-provider model # from the OpenRouter dropdown (e.g. config=anthropic but picked openai/gpt-5.4-mini). # In this case always route through openrouter with the full provider/model string. - if prefix in _PROVIDER_MODELS and prefix != config_provider: + # Exception (#4210): a custom provider (bare ``custom`` or named ``custom:``) + # is a vendor-routing proxy, not a first-party provider — its model ids commonly + # contain a known-provider prefix that the proxy uses for upstream routing, not + # an OpenRouter dropdown selection. Keep the request on the custom provider; the + # base_url-set sibling of this exception lives earlier in the ``config_base_url`` + # branch (#3872). + _cp_lower_cross = (config_provider or "").strip().lower() + _is_custom_cross = _cp_lower_cross == "custom" or _cp_lower_cross.startswith("custom:") + if prefix in _PROVIDER_MODELS and prefix != config_provider and not _is_custom_cross: return model_id, "openrouter", None return model_id, config_provider, config_base_url diff --git a/tests/test_model_resolver.py b/tests/test_model_resolver.py index 404143b6..df80b94c 100644 --- a/tests/test_model_resolver.py +++ b/tests/test_model_resolver.py @@ -709,3 +709,42 @@ def test_custom_endpoint_slash_model_routes_to_custom_not_openrouter(): assert model_or == 'google/gemma-4-26b-a4b', ( "Model name should be preserved for openrouter, got '{}'.".format(model_or) ) + + +# ── #4210: custom provider (no base_url) must not be hijacked to openrouter +# when the model id has a known-provider prefix (sibling of #3872, which +# only covered the base_url-set variant). Bug-report case 1. + +def test_custom_provider_no_base_url_with_known_prefix_keeps_custom_and_full_id_4210(): + """#4210: provider=custom:llm-proxy (no base_url) + 'x-ai/grok-2' must NOT + be redirected to openrouter. The prefix is intrinsic to the custom proxy's + routing; the user did not pick anything from the OpenRouter dropdown.""" + model, provider, base_url = _resolve_with_config( + 'x-ai/grok-2', + provider='custom:llm-proxy', + default='x-ai/grok-2', + ) + assert provider == 'custom:llm-proxy', ( + "Custom provider must not be hijacked to openrouter when no base_url is " + "set; got provider={!r} model={!r}".format(provider, model) + ) + assert model == 'x-ai/grok-2', ( + "Custom provider must preserve the full model id; got model={!r}".format(model) + ) + assert base_url is None + + +def test_bare_custom_provider_no_base_url_with_known_prefix_keeps_custom_and_full_id_4210(): + """#4210 sibling: bare 'custom' (no 'custom:') with no base_url + must also not be hijacked to openrouter for a known-prefix model id.""" + model, provider, base_url = _resolve_with_config( + 'google/gemma-2-9b', + provider='custom', + default='google/gemma-2-9b', + ) + assert provider == 'custom', ( + "Bare 'custom' provider must not be hijacked to openrouter when no " + "base_url is set; got provider={!r}".format(provider) + ) + assert model == 'google/gemma-2-9b' + assert base_url is None