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