From 3aea9c81c9f7bdebdc544b6f14d688059f75cc72 Mon Sep 17 00:00:00 2001 From: Chesars Date: Thu, 19 Feb 2026 15:50:39 -0300 Subject: [PATCH] fix(openrouter): prevent double-stripping of native model names in get_llm_provider Move the fix to the OpenRouter level: define native OpenRouter models (openrouter/auto, openrouter/free, openrouter/bodybuilder) and check them in get_llm_provider() before the provider_list stripping logic. This prevents the second strip across all bridges without modifying each adapter/handler individually. Fixes #16353 --- .../get_llm_provider_logic.py | 7 ++ litellm/llms/openrouter/common_utils.py | 10 +++ tests/litellm/llms/openrouter/__init__.py | 0 .../test_openrouter_native_models.py | 75 +++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 tests/litellm/llms/openrouter/__init__.py create mode 100644 tests/litellm/llms/openrouter/test_openrouter_native_models.py diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 718773a1b1..eeae959aeb 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -3,6 +3,7 @@ from typing import Optional, Tuple import litellm from litellm.constants import REPLICATE_MODEL_NAME_WITH_ID_LENGTH from litellm.llms.openai_like.json_loader import JSONProviderRegistry +from litellm.llms.openrouter.common_utils import NATIVE_OPENROUTER_MODELS from litellm.secret_managers.main import get_secret, get_secret_str from ..types.router import LiteLLM_Params @@ -165,6 +166,12 @@ def get_llm_provider( # noqa: PLR0915 dynamic_api_key=dynamic_api_key, ) + # Check native OpenRouter models before provider_list stripping. + # These models have IDs like "openrouter/free" which would be + # incorrectly stripped to just "free" by the logic below. + if model in NATIVE_OPENROUTER_MODELS: + return model, "openrouter", dynamic_api_key, api_base + # check if llm provider part of model name if ( diff --git a/litellm/llms/openrouter/common_utils.py b/litellm/llms/openrouter/common_utils.py index 96e53a5aae..d4054278cf 100644 --- a/litellm/llms/openrouter/common_utils.py +++ b/litellm/llms/openrouter/common_utils.py @@ -1,5 +1,15 @@ from litellm.llms.base_llm.chat.transformation import BaseLLMException +# Native OpenRouter models whose IDs start with "openrouter/". +# When used via LiteLLM (openrouter/openrouter/free), get_llm_provider() +# must not strip the inner "openrouter/" prefix on its second invocation. +# See: https://github.com/BerriAI/litellm/issues/16353 +NATIVE_OPENROUTER_MODELS = { + "openrouter/auto", + "openrouter/free", + "openrouter/bodybuilder", +} + class OpenRouterException(BaseLLMException): pass diff --git a/tests/litellm/llms/openrouter/__init__.py b/tests/litellm/llms/openrouter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/litellm/llms/openrouter/test_openrouter_native_models.py b/tests/litellm/llms/openrouter/test_openrouter_native_models.py new file mode 100644 index 0000000000..a6d2608a78 --- /dev/null +++ b/tests/litellm/llms/openrouter/test_openrouter_native_models.py @@ -0,0 +1,75 @@ +""" +Tests for native OpenRouter model name handling in get_llm_provider. + +OpenRouter's native models (openrouter/auto, openrouter/free, +openrouter/bodybuilder) should not have their "openrouter/" prefix +stripped when passed to get_llm_provider(), since that prefix is part +of the actual model ID on OpenRouter's API. + +""" + +import pytest + +import litellm + + +class TestNativeOpenRouterModelsNotStripped: + """get_llm_provider must preserve native OpenRouter model names.""" + + @pytest.mark.parametrize( + "model", + [ + "openrouter/auto", + "openrouter/free", + "openrouter/bodybuilder", + ], + ) + def test_native_model_not_stripped(self, model): + """Native OpenRouter model IDs are returned as-is.""" + result_model, provider, _, _ = litellm.get_llm_provider(model=model) + assert result_model == model + assert provider == "openrouter" + + @pytest.mark.parametrize( + "model,expected_model", + [ + ("openrouter/openrouter/free", "openrouter/free"), + ("openrouter/openrouter/auto", "openrouter/auto"), + ("openrouter/openrouter/bodybuilder", "openrouter/bodybuilder"), + ], + ) + def test_double_prefixed_model_strips_once_to_native(self, model, expected_model): + """openrouter/openrouter/free strips to openrouter/free (not further).""" + result_model, provider, _, _ = litellm.get_llm_provider(model=model) + assert result_model == expected_model + assert provider == "openrouter" + + @pytest.mark.parametrize( + "model,expected_model", + [ + ("openrouter/openrouter/free", "openrouter/free"), + ("openrouter/openrouter/auto", "openrouter/auto"), + ], + ) + def test_full_round_trip_no_double_strip(self, model, expected_model): + """Simulates the bridge flow: two consecutive get_llm_provider calls.""" + # First call (in adapter/handler) + model_after_first, provider, _, _ = litellm.get_llm_provider(model=model) + assert model_after_first == expected_model + + # Second call (inside litellm.completion) + model_after_second, provider2, _, _ = litellm.get_llm_provider( + model=model_after_first + ) + # Should stay as native model, not stripped further + assert model_after_second == expected_model + assert provider2 == "openrouter" + + def test_regular_openrouter_model_still_strips_normally(self): + """Non-native models like openrouter/anthropic/claude-3-haiku still strip normally.""" + model, provider, _, _ = litellm.get_llm_provider( + model="openrouter/anthropic/claude-3-haiku" + ) + assert provider == "openrouter" + # Should strip the openrouter/ prefix + assert model == "anthropic/claude-3-haiku"