From d292da2c142b617c1625cccf14df0f919c904c66 Mon Sep 17 00:00:00 2001 From: tombii Date: Fri, 27 Feb 2026 23:12:29 +0100 Subject: [PATCH 1/3] fix(openrouter): pattern-based fix for native OpenRouter model double-stripping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the hardcoded NATIVE_OPENROUTER_MODELS set approach with a pattern-based check in _get_openai_compatible_provider_info: after stripping the outer "openrouter/" provider prefix, if the remaining model name still starts with "openrouter/", return immediately without further stripping. This fixes openrouter/openrouter/aurora-alpha, openrouter/openrouter/polaris-alpha, and any future native OpenRouter models — not just the three hard-coded ones (auto, free, bodybuilder) from the previous approach. Fixes #16353 Co-Authored-By: Claude Sonnet 4.6 --- .../get_llm_provider_logic.py | 9 +++ .../test_openrouter_provider_routing.py | 73 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 8ab4ec15b07..cf78ec46150 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -504,6 +504,15 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915 custom_llm_provider = model.split("/", 1)[0] model = model.split("/", 1)[1] + # If the provider is openrouter and the remaining model name still starts + # with "openrouter/", that inner prefix is part of the actual model ID on + # the OpenRouter API (e.g. openrouter/openrouter/aurora-alpha → + # model="openrouter/aurora-alpha"). Return immediately so the prefix is + # not stripped a second time. + if custom_llm_provider == "openrouter" and model.startswith("openrouter/"): + dynamic_api_key = api_key or get_secret_str("OPENROUTER_API_KEY") + return model, custom_llm_provider, dynamic_api_key, api_base + # Check JSON providers FIRST (before hardcoded ones) from litellm.llms.openai_like.dynamic_config import create_config_class from litellm.llms.openai_like.json_loader import JSONProviderRegistry diff --git a/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py b/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py new file mode 100644 index 00000000000..862c6d4da98 --- /dev/null +++ b/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py @@ -0,0 +1,73 @@ +""" +Tests for OpenRouter model name routing in get_llm_provider. + +OpenRouter-native models have IDs that start with "openrouter/" (e.g. +openrouter/auto, openrouter/free, openrouter/aurora-alpha). When a user +configures such a model in LiteLLM they use the double-prefixed form +"openrouter/openrouter/aurora-alpha". get_llm_provider must strip only +the outer "openrouter/" provider prefix and leave the inner one intact, +so the correct model ID is sent to the OpenRouter API. + +See: https://github.com/BerriAI/litellm/issues/16353 +""" + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.abspath("../../../../..")) + +import litellm + + +class TestOpenRouterNativeModelRouting: + """get_llm_provider must not double-strip native OpenRouter model names.""" + + @pytest.mark.parametrize( + "input_model,expected_model", + [ + # Well-known native models + ("openrouter/openrouter/auto", "openrouter/auto"), + ("openrouter/openrouter/free", "openrouter/free"), + ("openrouter/openrouter/bodybuilder", "openrouter/bodybuilder"), + # Arbitrary native models — the fix must be pattern-based, not a hardcoded list + ("openrouter/openrouter/aurora-alpha", "openrouter/aurora-alpha"), + ("openrouter/openrouter/polaris-alpha", "openrouter/polaris-alpha"), + ("openrouter/openrouter/some-future-model", "openrouter/some-future-model"), + ], + ) + def test_double_prefixed_strips_once(self, input_model, expected_model): + """openrouter/openrouter/ should yield model=openrouter/.""" + result_model, provider, _, _ = litellm.get_llm_provider(model=input_model) + assert provider == "openrouter" + assert result_model == expected_model + + @pytest.mark.parametrize( + "input_model,expected_model", + [ + ("openrouter/openrouter/aurora-alpha", "openrouter/aurora-alpha"), + ("openrouter/openrouter/auto", "openrouter/auto"), + ], + ) + def test_no_double_strip_on_second_call(self, input_model, expected_model): + """Simulates two consecutive get_llm_provider calls (bridge → completion).""" + model_first, provider, _, _ = litellm.get_llm_provider(model=input_model) + assert model_first == expected_model + + model_second, provider2, _, _ = litellm.get_llm_provider(model=model_first) + assert provider2 == "openrouter" + assert model_second == expected_model + + @pytest.mark.parametrize( + "input_model,expected_model", + [ + ("openrouter/anthropic/claude-3-haiku", "anthropic/claude-3-haiku"), + ("openrouter/meta-llama/llama-3-70b-instruct", "meta-llama/llama-3-70b-instruct"), + ], + ) + def test_regular_models_still_strip_normally(self, input_model, expected_model): + """Non-native OpenRouter models should still have their prefix stripped.""" + result_model, provider, _, _ = litellm.get_llm_provider(model=input_model) + assert provider == "openrouter" + assert result_model == expected_model From 73de1acdc7582e9d4292a9055bc6c5859ecd8e39 Mon Sep 17 00:00:00 2001 From: tombii Date: Sat, 28 Feb 2026 21:38:08 +0100 Subject: [PATCH 2/3] fix: correct test_no_double_strip_on_second_call assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second get_llm_provider call on an already-resolved model like openrouter/aurora-alpha correctly strips the openrouter/ prefix to yield the bare model ID (aurora-alpha) — not the prefixed form. Update the parametrize signature to use separate expected_first/expected_second values and fix the assertions accordingly, with an explanatory docstring. Co-Authored-By: Claude Sonnet 4.6 --- .../test_openrouter_provider_routing.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py b/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py index 862c6d4da98..64fd77954a8 100644 --- a/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py +++ b/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py @@ -44,20 +44,31 @@ class TestOpenRouterNativeModelRouting: assert result_model == expected_model @pytest.mark.parametrize( - "input_model,expected_model", + "input_model,expected_first,expected_second", [ - ("openrouter/openrouter/aurora-alpha", "openrouter/aurora-alpha"), - ("openrouter/openrouter/auto", "openrouter/auto"), + # After the first call strips outer prefix: openrouter/openrouter/aurora-alpha + # → openrouter/aurora-alpha. A second call on that result splits at the + # first "/" giving provider=openrouter, model=aurora-alpha — which is the + # correct model ID to send to the OpenRouter API. + ("openrouter/openrouter/aurora-alpha", "openrouter/aurora-alpha", "aurora-alpha"), + ("openrouter/openrouter/auto", "openrouter/auto", "auto"), ], ) - def test_no_double_strip_on_second_call(self, input_model, expected_model): - """Simulates two consecutive get_llm_provider calls (bridge → completion).""" + def test_no_double_strip_on_second_call(self, input_model, expected_first, expected_second): + """Simulates two consecutive get_llm_provider calls (bridge → completion). + + The first call (bridge) converts openrouter/openrouter/ to + openrouter/. The second call (completion) further strips the + remaining openrouter/ provider prefix and returns — the bare + model ID that should be sent to the OpenRouter API. + """ model_first, provider, _, _ = litellm.get_llm_provider(model=input_model) - assert model_first == expected_model + assert provider == "openrouter" + assert model_first == expected_first model_second, provider2, _, _ = litellm.get_llm_provider(model=model_first) assert provider2 == "openrouter" - assert model_second == expected_model + assert model_second == expected_second @pytest.mark.parametrize( "input_model,expected_model", From 09ef5e67e51fbab4aa38cfffb9bc8cf65d2e120e Mon Sep 17 00:00:00 2001 From: Chesars Date: Mon, 2 Mar 2026 16:55:34 -0300 Subject: [PATCH 3/3] refactor: move native OpenRouter check to get_llm_provider before strip The previous check in _get_openai_compatible_provider_info() ran after the model name was already split, so it never caught the second get_llm_provider() call from the anthropic_messages bridge. Moved the check to get_llm_provider() before the provider-list stripping, using a pattern-based approach (custom_llm_provider == "openrouter" and model.startswith("openrouter/")) instead of a hardcoded set. This covers all current and future native OpenRouter models. Updated tests to verify the bridge double-call scenario with custom_llm_provider passed through. --- .../get_llm_provider_logic.py | 17 +++++---- .../test_openrouter_provider_routing.py | 36 +++++++++++-------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index cf78ec46150..82ae5a9ff0a 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -158,6 +158,14 @@ def get_llm_provider( # noqa: PLR0915 ): # handle scenario where model="azure/*" and custom_llm_provider="azure" model = custom_llm_provider + "/" + model + # Native OpenRouter models have IDs like "openrouter/free" where the + # "openrouter/" prefix is part of the actual model name on the API. + # When called from a bridge (e.g. anthropic_messages adapter), + # custom_llm_provider is already resolved, so return early to prevent + # the provider-list stripping below from removing the prefix. + if custom_llm_provider == "openrouter" and model.startswith("openrouter/"): + return model, custom_llm_provider, dynamic_api_key, api_base + if api_key and api_key.startswith("os.environ/"): dynamic_api_key = get_secret_str(api_key) @@ -504,15 +512,6 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915 custom_llm_provider = model.split("/", 1)[0] model = model.split("/", 1)[1] - # If the provider is openrouter and the remaining model name still starts - # with "openrouter/", that inner prefix is part of the actual model ID on - # the OpenRouter API (e.g. openrouter/openrouter/aurora-alpha → - # model="openrouter/aurora-alpha"). Return immediately so the prefix is - # not stripped a second time. - if custom_llm_provider == "openrouter" and model.startswith("openrouter/"): - dynamic_api_key = api_key or get_secret_str("OPENROUTER_API_KEY") - return model, custom_llm_provider, dynamic_api_key, api_base - # Check JSON providers FIRST (before hardcoded ones) from litellm.llms.openai_like.dynamic_config import create_config_class from litellm.llms.openai_like.json_loader import JSONProviderRegistry diff --git a/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py b/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py index 64fd77954a8..72cf2eec371 100644 --- a/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py +++ b/tests/test_litellm/llms/openrouter/test_openrouter_provider_routing.py @@ -44,31 +44,37 @@ class TestOpenRouterNativeModelRouting: assert result_model == expected_model @pytest.mark.parametrize( - "input_model,expected_first,expected_second", + "input_model", [ - # After the first call strips outer prefix: openrouter/openrouter/aurora-alpha - # → openrouter/aurora-alpha. A second call on that result splits at the - # first "/" giving provider=openrouter, model=aurora-alpha — which is the - # correct model ID to send to the OpenRouter API. - ("openrouter/openrouter/aurora-alpha", "openrouter/aurora-alpha", "aurora-alpha"), - ("openrouter/openrouter/auto", "openrouter/auto", "auto"), + "openrouter/openrouter/aurora-alpha", + "openrouter/openrouter/auto", + "openrouter/openrouter/free", + "openrouter/openrouter/some-future-model", ], ) - def test_no_double_strip_on_second_call(self, input_model, expected_first, expected_second): + def test_bridge_double_call_preserves_native_model(self, input_model): """Simulates two consecutive get_llm_provider calls (bridge → completion). - The first call (bridge) converts openrouter/openrouter/ to - openrouter/. The second call (completion) further strips the - remaining openrouter/ provider prefix and returns — the bare - model ID that should be sent to the OpenRouter API. + The first call (bridge) strips the outer prefix: + openrouter/openrouter/ → openrouter/ + + The second call (completion) receives custom_llm_provider="openrouter" + from the bridge, detects the native model, and preserves it: + openrouter/ → openrouter/ (no further stripping) """ + # First call: bridge resolves provider model_first, provider, _, _ = litellm.get_llm_provider(model=input_model) assert provider == "openrouter" - assert model_first == expected_first + expected_model = input_model.split("/", 1)[1] # openrouter/ + assert model_first == expected_model - model_second, provider2, _, _ = litellm.get_llm_provider(model=model_first) + # Second call: completion receives model + custom_llm_provider from bridge + model_second, provider2, _, _ = litellm.get_llm_provider( + model=model_first, + custom_llm_provider="openrouter", + ) assert provider2 == "openrouter" - assert model_second == expected_second + assert model_second == expected_model # preserved, not stripped further @pytest.mark.parametrize( "input_model,expected_model",