From a60ce7557b166b19f143a9e260bf6a7cc5057539 Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 14:44:17 +0300 Subject: [PATCH 01/13] feat(openrouter): native support for in-region routing (us.openrouter.ai / eu.openrouter.ai) Support OpenRouter's regional endpoints (eu.openrouter.ai, us.openrouter.ai) and map region/data_residency parameters dynamically. Translate 404 region errors into OpenRouterRegionNotSupportedError, preserve custom api_base, and synchronize data_residency to litellm_params for proxy logging. Fixes #46665 --- litellm/__init__.py | 1 + litellm/exceptions.py | 29 +++ .../exception_mapping_utils.py | 19 ++ .../litellm_core_utils/get_litellm_params.py | 5 +- .../llms/openrouter/chat/transformation.py | 32 +++ litellm/llms/openrouter/data_residency.py | 125 ++++++++++ .../openrouter/embedding/transformation.py | 13 +- litellm/main.py | 27 ++- .../test_openrouter_data_residency.py | 216 ++++++++++++++++++ 9 files changed, 455 insertions(+), 12 deletions(-) create mode 100644 litellm/llms/openrouter/data_residency.py create mode 100644 tests/test_litellm/llms/openrouter/test_openrouter_data_residency.py diff --git a/litellm/__init__.py b/litellm/__init__.py index eebd2dad91e..decbef4695a 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1342,6 +1342,7 @@ from .exceptions import ( BadRequestError, ImageFetchError, NotFoundError, + OpenRouterRegionNotSupportedError, PermissionDeniedError, RateLimitError, RateLimitErrorCategory, diff --git a/litellm/exceptions.py b/litellm/exceptions.py index 286f7528896..89840b33253 100644 --- a/litellm/exceptions.py +++ b/litellm/exceptions.py @@ -213,6 +213,35 @@ class NotFoundError(openai.NotFoundError): return _message +class OpenRouterRegionNotSupportedError(NotFoundError): + """ + Raised when OpenRouter returns HTTP 404 indicating no in-region endpoint exists + for the requested model in the specified data region (e.g. 'us' or 'eu'). + """ + + def __init__( + self, + message: str, + model: str, + llm_provider: str = "openrouter", + response: httpx.Response | None = None, + litellm_debug_info: str | None = None, + max_retries: int | None = None, + num_retries: int | None = None, + region: str | None = None, + ): + self.region = region + super().__init__( + message=message, + model=model, + llm_provider=llm_provider, + response=response, + litellm_debug_info=litellm_debug_info, + max_retries=max_retries, + num_retries=num_retries, + ) + + class BadRequestError(openai.BadRequestError): def __init__( self, diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index dc245d42862..4660d80f9a6 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -20,6 +20,7 @@ from ..exceptions import ( ContextWindowExceededError, InternalServerError, NotFoundError, + OpenRouterRegionNotSupportedError, PermissionDeniedError, RateLimitError, ServiceUnavailableError, @@ -2135,6 +2136,24 @@ def _map_openrouter_exception( litellm_debug_info=extra_information, ) elif original_exception.status_code == 404: + if "supporting your data region" in error_str.lower(): + response_obj = getattr(original_exception, "response", None) + inferred_region = None + if response_obj is not None and getattr(response_obj, "url", None): + from litellm.llms.openrouter.data_residency import infer_openrouter_data_residency + + inferred_region = infer_openrouter_data_residency( + custom_llm_provider="openrouter", + api_base=str(response_obj.url), + ) + raise OpenRouterRegionNotSupportedError( + message=f"OpenRouterRegionNotSupportedError: {exception_provider} - {error_str}", + model=model, + llm_provider=custom_llm_provider, + response=response_obj, + litellm_debug_info=extra_information, + region=inferred_region, + ) raise NotFoundError( message=f"NotFoundError: {exception_provider} - {error_str}", model=model, diff --git a/litellm/litellm_core_utils/get_litellm_params.py b/litellm/litellm_core_utils/get_litellm_params.py index b12c715c9f5..3ef9956dcd1 100644 --- a/litellm/litellm_core_utils/get_litellm_params.py +++ b/litellm/litellm_core_utils/get_litellm_params.py @@ -3,6 +3,7 @@ from types import MappingProxyType from typing import Final from litellm.llms.openai.data_residency import infer_openai_data_residency +from litellm.llms.openrouter.data_residency import infer_openrouter_data_residency AWS_CREDENTIAL_KWARGS_KEYS: Final = frozenset( { @@ -139,7 +140,9 @@ def get_litellm_params( if litellm_trace_id is None: litellm_trace_id = _meta.get("trace_id") or _meta.get("session_id") - data_residency: Final[str | None] = infer_openai_data_residency(custom_llm_provider, api_base) + data_residency: Final[str | None] = infer_openai_data_residency( + custom_llm_provider, api_base + ) or infer_openrouter_data_residency(custom_llm_provider, api_base) # Build base dict with explicit parameters (always included) litellm_params: Final = { diff --git a/litellm/llms/openrouter/chat/transformation.py b/litellm/llms/openrouter/chat/transformation.py index 71c21f14351..d60ad9938e0 100644 --- a/litellm/llms/openrouter/chat/transformation.py +++ b/litellm/llms/openrouter/chat/transformation.py @@ -21,6 +21,7 @@ from litellm.types.utils import ModelResponse, ModelResponseStream from ...openai.chat.gpt_transformation import OpenAIGPTConfig from ..common_utils import OpenRouterException +from ..data_residency import resolve_openrouter_api_base class CacheControlSupportedModels(str, Enum): @@ -34,6 +35,37 @@ class CacheControlSupportedModels(str, Enum): class OpenrouterConfig(OpenAIGPTConfig): + region: str | None = None + + def __init__( + self, + region: str | None = None, + **kwargs, + ): + self.region = region + super().__init__(**kwargs) + + def get_complete_url( + self, + api_base: str | None, + api_key: str | None, + model: str, + optional_params: dict, + litellm_params: dict, + stream: bool | None = None, + ) -> str: + api_base = resolve_openrouter_api_base( + api_base=api_base, + region=self.region, + litellm_params=litellm_params, + optional_params=optional_params, + ) + endpoint: Final = "chat/completions" + api_base = api_base.rstrip("/") + if endpoint in api_base: + return api_base + return f"{api_base}/{endpoint}" + def get_supported_openai_params(self, model: str) -> list: """ Allow reasoning parameters for models flagged as reasoning-capable. diff --git a/litellm/llms/openrouter/data_residency.py b/litellm/llms/openrouter/data_residency.py new file mode 100644 index 00000000000..de4455b178b --- /dev/null +++ b/litellm/llms/openrouter/data_residency.py @@ -0,0 +1,125 @@ +""" +OpenRouter In-Region Routing and Data Residency Helpers. + +OpenRouter provides dedicated regional endpoints for end-to-end data residency: +- EU: https://eu.openrouter.ai/api/v1 +- US: https://us.openrouter.ai/api/v1 +- Global default: https://openrouter.ai/api/v1 + +Docs: https://openrouter.ai/docs/guides/features/in-region-routing +""" + +from typing import Final +from urllib.parse import urlparse + +import litellm +from litellm.secret_managers.main import get_secret_str + +# Normalized region names mapped to their dedicated OpenRouter base URLs +_OPENROUTER_REGIONAL_URLS: Final[dict[str, str]] = { + "eu": "https://eu.openrouter.ai/api/v1", + "europe": "https://eu.openrouter.ai/api/v1", + "us": "https://us.openrouter.ai/api/v1", + "usa": "https://us.openrouter.ai/api/v1", + "united states": "https://us.openrouter.ai/api/v1", +} + +# OpenRouter regional hostnames mapped to canonical data residency codes ("eu", "us") +_OPENROUTER_REGIONAL_HOSTNAMES: Final[dict[str, str]] = { + "eu.openrouter.ai": "eu", + "us.openrouter.ai": "us", +} + +_DEFAULT_OPENROUTER_API_BASE: Final[str] = "https://openrouter.ai/api/v1" + + +def resolve_openrouter_api_base( + api_base: str | None = None, + region: str | None = None, + litellm_params: dict | None = None, + optional_params: dict | None = None, +) -> str: + """ + Resolve the OpenRouter API base URL based on custom api_base, explicit region, or environment. + + Precedence: + 1. If `api_base` is passed and is a custom URL (not the default global openrouter.ai URL), use it. + 2. If `region` is provided (via param, litellm_params, optional_params, or environment), map to the regional endpoint. + 3. Fallback to `api_base or litellm.api_base or OPENROUTER_API_BASE or default global URL`. + """ + # Check if a custom api_base was provided that is not the default global endpoint + is_custom_api_base = False + if api_base: + cleaned_base = api_base.rstrip("/").lower() + if cleaned_base not in ("https://openrouter.ai/api/v1", "https://openrouter.ai", "http://openrouter.ai"): + is_custom_api_base = True + + if is_custom_api_base and api_base: + return api_base + + # Check for region in litellm_params, optional_params, explicit region argument, config, or environment + resolved_region = None + if isinstance(litellm_params, dict): + resolved_region = ( + litellm_params.get("region") + or litellm_params.get("region_name") + or litellm_params.get("data_residency") + ) + if not resolved_region and isinstance(optional_params, dict): + resolved_region = ( + optional_params.pop("region", None) + or optional_params.pop("region_name", None) + or optional_params.pop("data_residency", None) + ) + if not resolved_region: + resolved_region = ( + region + or getattr(litellm, "openrouter_region", None) + or getattr(litellm, "openrouter_data_residency", None) + or get_secret_str("OPENROUTER_REGION") + or get_secret_str("OPENROUTER_DATA_RESIDENCY") + ) + + if resolved_region and isinstance(resolved_region, str): + normalized_region = resolved_region.strip().lower() + if normalized_region in ("global", "default"): + return _DEFAULT_OPENROUTER_API_BASE + if normalized_region in _OPENROUTER_REGIONAL_URLS: + return _OPENROUTER_REGIONAL_URLS[normalized_region] + if normalized_region.startswith(("eu-", "eu_", "europe-", "europe_")): + return "https://eu.openrouter.ai/api/v1" + if normalized_region.startswith(("us-", "us_", "usa-", "usa_")): + return "https://us.openrouter.ai/api/v1" + # Dynamic regional hostname for future OpenRouter regions without breaking client-side + return f"https://{normalized_region}.openrouter.ai/api/v1" + + return ( + api_base + or litellm.api_base + or get_secret_str("OPENROUTER_API_BASE") + or _DEFAULT_OPENROUTER_API_BASE + ) + + +def infer_openrouter_data_residency( + custom_llm_provider: str | None, + api_base: str | None, +) -> str | None: + """ + Derive the OpenRouter data-residency region from an api_base URL. + + Returns "eu" for eu.openrouter.ai, "us" for us.openrouter.ai, and None for + the default global host or non-OpenRouter URLs. + """ + if custom_llm_provider != "openrouter" or not api_base: + return None + + try: + host: Final = urlparse(api_base).hostname + except (TypeError, ValueError): + return None + + if not host: + return None + + return _OPENROUTER_REGIONAL_HOSTNAMES.get(host.lower()) diff --git a/litellm/llms/openrouter/embedding/transformation.py b/litellm/llms/openrouter/embedding/transformation.py index 29d0c8c1c56..5b9b376aaa1 100644 --- a/litellm/llms/openrouter/embedding/transformation.py +++ b/litellm/llms/openrouter/embedding/transformation.py @@ -17,6 +17,7 @@ from litellm.types.utils import EmbeddingResponse from litellm.utils import convert_to_model_response_object from ..common_utils import OpenRouterException +from ..data_residency import resolve_openrouter_api_base if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj @@ -84,12 +85,12 @@ class OpenrouterEmbeddingConfig(BaseEmbeddingConfig): """ Get the complete URL for OpenRouter Embedding API endpoint. """ - # api_base is already set to https://openrouter.ai/api/v1 in main.py - # Remove trailing slashes - if api_base: - api_base = api_base.rstrip("/") - else: - api_base = "https://openrouter.ai/api/v1" + api_base = resolve_openrouter_api_base( + api_base=api_base, + litellm_params=litellm_params, + optional_params=optional_params, + ) + api_base = api_base.rstrip("/") # Return the embeddings endpoint return f"{api_base}/embeddings" diff --git a/litellm/main.py b/litellm/main.py index 98f92e50599..deaa10f7fee 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -106,6 +106,10 @@ from litellm.llms.cohere.common_utils import CohereModelInfo from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config from litellm.llms.openai_like.json_loader import JSONProviderRegistry +from litellm.llms.openrouter.data_residency import ( + infer_openrouter_data_residency, + resolve_openrouter_api_base, +) from litellm.llms.vertex_ai.common_utils import ( VertexAIModelRoute, get_vertex_ai_model_route, @@ -3358,7 +3362,17 @@ def _complete_openrouter(ctx: _CompletionDispatchContext) -> _CompletionDispatch stream: Final = ctx.stream timeout: Final = ctx.timeout - api_base = api_base or litellm.api_base or get_secret_str("OPENROUTER_API_BASE") or "https://openrouter.ai/api/v1" + ## Load Config + config: Final = litellm.OpenrouterConfig.get_config() + api_base = resolve_openrouter_api_base( + api_base=api_base, + region=getattr(config, "region", None), + litellm_params=litellm_params, + optional_params=optional_params, + ) + if isinstance(litellm_params, dict): + litellm_params["api_base"] = api_base + litellm_params["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) api_key = ( api_key @@ -3382,8 +3396,6 @@ def _complete_openrouter(ctx: _CompletionDispatchContext) -> _CompletionDispatch headers = openrouter_headers - ## Load Config - config: Final = litellm.OpenrouterConfig.get_config() for k, v in _provider_config_items(config): if k == "extra_body": # we use openai 'extra_body' to pass openrouter specific params - transforms, route, models @@ -6432,9 +6444,14 @@ def embedding( headers=headers, ) elif custom_llm_provider == "openrouter": - api_base = ( - api_base or litellm.api_base or get_secret_str("OPENROUTER_API_BASE") or "https://openrouter.ai/api/v1" + api_base = resolve_openrouter_api_base( + api_base=api_base, + litellm_params=litellm_params_dict, + optional_params=optional_params, ) + if isinstance(litellm_params_dict, dict): + litellm_params_dict["api_base"] = api_base + litellm_params_dict["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) api_key = ( api_key diff --git a/tests/test_litellm/llms/openrouter/test_openrouter_data_residency.py b/tests/test_litellm/llms/openrouter/test_openrouter_data_residency.py new file mode 100644 index 00000000000..3191abec37f --- /dev/null +++ b/tests/test_litellm/llms/openrouter/test_openrouter_data_residency.py @@ -0,0 +1,216 @@ +"""Tests for OpenRouter in-region routing, data residency inference, and error handling.""" + +import httpx +import pytest + +import litellm +from litellm.exceptions import NotFoundError, OpenRouterRegionNotSupportedError +from litellm.litellm_core_utils.exception_mapping_utils import _map_openrouter_exception +from litellm.litellm_core_utils.get_litellm_params import get_litellm_params +from litellm.llms.openrouter.chat.transformation import OpenrouterConfig +from litellm.llms.openrouter.data_residency import ( + infer_openrouter_data_residency, + resolve_openrouter_api_base, +) + + +@pytest.mark.parametrize( + "api_base, region, expected", + [ + (None, "eu", "https://eu.openrouter.ai/api/v1"), + (None, "EU", "https://eu.openrouter.ai/api/v1"), + (None, "europe", "https://eu.openrouter.ai/api/v1"), + (None, "eu-west-1", "https://eu.openrouter.ai/api/v1"), + (None, "eu-central-1", "https://eu.openrouter.ai/api/v1"), + (None, "europe-west3", "https://eu.openrouter.ai/api/v1"), + (None, "us", "https://us.openrouter.ai/api/v1"), + (None, "US", "https://us.openrouter.ai/api/v1"), + (None, "usa", "https://us.openrouter.ai/api/v1"), + (None, "united states", "https://us.openrouter.ai/api/v1"), + (None, "us-east-1", "https://us.openrouter.ai/api/v1"), + (None, "us-west-2", "https://us.openrouter.ai/api/v1"), + (None, "global", "https://openrouter.ai/api/v1"), + (None, "default", "https://openrouter.ai/api/v1"), + (None, None, "https://openrouter.ai/api/v1"), + ("https://openrouter.ai/api/v1", "eu", "https://eu.openrouter.ai/api/v1"), + ("https://openrouter.ai/api/v1", "us", "https://us.openrouter.ai/api/v1"), + ("https://custom.openrouter.proxy/v1", "eu", "https://custom.openrouter.proxy/v1"), + ("https://custom.openrouter.proxy/v1", "apac", "https://custom.openrouter.proxy/v1"), + ("https://custom.openrouter.proxy/v1", None, "https://custom.openrouter.proxy/v1"), + ], +) +def test_resolve_openrouter_api_base(api_base, region, expected): + assert resolve_openrouter_api_base(api_base=api_base, region=region) == expected + + +def test_resolve_openrouter_api_base_env_var(monkeypatch): + monkeypatch.setenv("OPENROUTER_REGION", "eu") + assert resolve_openrouter_api_base() == "https://eu.openrouter.ai/api/v1" + + +def test_resolve_openrouter_api_base_dynamic_region(): + assert resolve_openrouter_api_base(region="apac") == "https://apac.openrouter.ai/api/v1" + + +def test_resolve_openrouter_api_base_from_dict_params(): + assert resolve_openrouter_api_base(litellm_params={"region": "eu"}) == "https://eu.openrouter.ai/api/v1" + assert resolve_openrouter_api_base(optional_params={"region": "us"}) == "https://us.openrouter.ai/api/v1" + + +@pytest.mark.parametrize( + "api_base, expected", + [ + ("https://eu.openrouter.ai/api/v1", "eu"), + ("https://eu.openrouter.ai", "eu"), + ("https://us.openrouter.ai/api/v1", "us"), + ("https://us.openrouter.ai", "us"), + ("https://EU.openrouter.ai/api/v1", "eu"), + ("https://openrouter.ai/api/v1", None), + ("https://openrouter.ai", None), + ("https://example.com/v1", None), + ("", None), + (None, None), + ("invalid-url", None), + ], +) +def test_infer_openrouter_data_residency(api_base, expected): + assert infer_openrouter_data_residency("openrouter", api_base) == expected + + +@pytest.mark.parametrize("custom_llm_provider", [None, "openai", "anthropic", "azure", "bedrock"]) +def test_infer_openrouter_data_residency_non_openrouter(custom_llm_provider): + assert ( + infer_openrouter_data_residency(custom_llm_provider, "https://eu.openrouter.ai/api/v1") + is None + ) + + +def test_get_litellm_params_infers_openrouter_data_residency(): + params_eu = get_litellm_params( + custom_llm_provider="openrouter", + api_base="https://eu.openrouter.ai/api/v1", + ) + assert params_eu["data_residency"] == "eu" + + params_us = get_litellm_params( + custom_llm_provider="openrouter", + api_base="https://us.openrouter.ai/api/v1", + ) + assert params_us["data_residency"] == "us" + + params_global = get_litellm_params( + custom_llm_provider="openrouter", + api_base="https://openrouter.ai/api/v1", + ) + assert params_global["data_residency"] is None + + +def test_openrouter_config_region(): + cfg = OpenrouterConfig(region="eu") + assert cfg.region == "eu" + + +def test_openrouter_chat_config_get_complete_url(): + cfg = OpenrouterConfig(region="eu") + url = cfg.get_complete_url( + api_base=None, + api_key="sk-test", + model="anthropic/claude-3-opus", + optional_params={}, + litellm_params={}, + ) + assert url == "https://eu.openrouter.ai/api/v1/chat/completions" + + # Per-request parameter overrides config + url_us = cfg.get_complete_url( + api_base=None, + api_key="sk-test", + model="anthropic/claude-3-opus", + optional_params={"region": "us"}, + litellm_params={}, + ) + assert url_us == "https://us.openrouter.ai/api/v1/chat/completions" + + +def test_openrouter_region_not_supported_error_mapping(): + """Verify that OpenRouter 404 with regional error raises OpenRouterRegionNotSupportedError.""" + fake_response = httpx.Response( + status_code=404, + request=httpx.Request("POST", "https://eu.openrouter.ai/api/v1/chat/completions"), + ) + fake_exc = httpx.HTTPStatusError( + message="404 Not Found", + request=fake_response.request, + response=fake_response, + ) + fake_exc.status_code = 404 + + # 1. Regional 404 message -> OpenRouterRegionNotSupportedError + error_msg = '{"error": {"message": "No endpoints found supporting your data region", "code": 404}}' + with pytest.raises(OpenRouterRegionNotSupportedError) as exc_info: + _map_openrouter_exception( + model="openrouter/anthropic/claude-sonnet-4", + original_exception=fake_exc, + custom_llm_provider="openrouter", + error_str=error_msg, + exception_type="NotFoundError", + exception_provider="openrouter", + extra_information="", + ) + + # Ensure it's also an instance of NotFoundError for backward compatibility + assert isinstance(exc_info.value, NotFoundError) + assert isinstance(exc_info.value, OpenRouterRegionNotSupportedError) + assert exc_info.value.region == "eu" + + # 2. Standard 404 (model not found) -> standard NotFoundError + standard_error_msg = '{"error": {"message": "Model not found", "code": 404}}' + with pytest.raises(NotFoundError) as exc_info_standard: + _map_openrouter_exception( + model="openrouter/non-existent-model", + original_exception=fake_exc, + custom_llm_provider="openrouter", + error_str=standard_error_msg, + exception_type="NotFoundError", + exception_provider="openrouter", + extra_information="", + ) + assert not isinstance(exc_info_standard.value, OpenRouterRegionNotSupportedError) + + +def test_openrouter_completion_with_region_parameter(monkeypatch): + """Test that completion() resolves regional api_base and tracks data_residency.""" + captured_api_base = None + captured_litellm_params = None + + def mock_completion(*args, **kwargs): + nonlocal captured_api_base, captured_litellm_params + captured_api_base = kwargs.get("api_base") + captured_litellm_params = kwargs.get("litellm_params") + return litellm.ModelResponse( + choices=[ + litellm.Choices( + finish_reason="stop", + index=0, + message=litellm.Message(content="Hello from EU", role="assistant"), + ) + ] + ) + + monkeypatch.setattr( + "litellm.main.base_llm_http_handler.completion", + mock_completion, + ) + monkeypatch.setenv("OPENROUTER_API_KEY", "sk-mock-key") + + resp = litellm.completion( + model="openrouter/anthropic/claude-sonnet-4", + messages=[{"role": "user", "content": "Hello"}], + region="eu", + ) + assert captured_api_base == "https://eu.openrouter.ai/api/v1" + assert captured_litellm_params is not None + assert captured_litellm_params.get("api_base") == "https://eu.openrouter.ai/api/v1" + assert captured_litellm_params.get("data_residency") == "eu" + assert resp.choices[0].message.content == "Hello from EU" + From b2e9da602faadb0363c3ec17b6fc32240981a1ab Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 15:49:50 +0300 Subject: [PATCH 02/13] fix(openrouter): validate region input and expand test coverage --- litellm/llms/openrouter/data_residency.py | 22 ++----- .../test_openrouter_data_residency.py | 62 ++++++++++++++++--- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/litellm/llms/openrouter/data_residency.py b/litellm/llms/openrouter/data_residency.py index de4455b178b..9d31c3c0483 100644 --- a/litellm/llms/openrouter/data_residency.py +++ b/litellm/llms/openrouter/data_residency.py @@ -61,9 +61,7 @@ def resolve_openrouter_api_base( resolved_region = None if isinstance(litellm_params, dict): resolved_region = ( - litellm_params.get("region") - or litellm_params.get("region_name") - or litellm_params.get("data_residency") + litellm_params.get("region") or litellm_params.get("region_name") or litellm_params.get("data_residency") ) if not resolved_region and isinstance(optional_params, dict): resolved_region = ( @@ -73,11 +71,7 @@ def resolve_openrouter_api_base( ) if not resolved_region: resolved_region = ( - region - or getattr(litellm, "openrouter_region", None) - or getattr(litellm, "openrouter_data_residency", None) - or get_secret_str("OPENROUTER_REGION") - or get_secret_str("OPENROUTER_DATA_RESIDENCY") + region or getattr(litellm, "openrouter_region", None) or getattr(litellm, "openrouter_data_residency", None) ) if resolved_region and isinstance(resolved_region, str): @@ -90,15 +84,11 @@ def resolve_openrouter_api_base( return "https://eu.openrouter.ai/api/v1" if normalized_region.startswith(("us-", "us_", "usa-", "usa_")): return "https://us.openrouter.ai/api/v1" - # Dynamic regional hostname for future OpenRouter regions without breaking client-side - return f"https://{normalized_region}.openrouter.ai/api/v1" + raise ValueError( + f"Unsupported OpenRouter region: '{resolved_region}'. Supported regions are 'eu', 'us', 'global'." + ) - return ( - api_base - or litellm.api_base - or get_secret_str("OPENROUTER_API_BASE") - or _DEFAULT_OPENROUTER_API_BASE - ) + return api_base or litellm.api_base or get_secret_str("OPENROUTER_API_BASE") or _DEFAULT_OPENROUTER_API_BASE def infer_openrouter_data_residency( diff --git a/tests/test_litellm/llms/openrouter/test_openrouter_data_residency.py b/tests/test_litellm/llms/openrouter/test_openrouter_data_residency.py index 3191abec37f..a70721830e5 100644 --- a/tests/test_litellm/llms/openrouter/test_openrouter_data_residency.py +++ b/tests/test_litellm/llms/openrouter/test_openrouter_data_residency.py @@ -43,18 +43,34 @@ def test_resolve_openrouter_api_base(api_base, region, expected): assert resolve_openrouter_api_base(api_base=api_base, region=region) == expected -def test_resolve_openrouter_api_base_env_var(monkeypatch): - monkeypatch.setenv("OPENROUTER_REGION", "eu") +def test_resolve_openrouter_api_base_litellm_attrs(monkeypatch): + monkeypatch.setattr(litellm, "openrouter_region", "eu", raising=False) assert resolve_openrouter_api_base() == "https://eu.openrouter.ai/api/v1" + monkeypatch.delattr(litellm, "openrouter_region", raising=False) + monkeypatch.setattr(litellm, "openrouter_data_residency", "us", raising=False) + assert resolve_openrouter_api_base() == "https://us.openrouter.ai/api/v1" -def test_resolve_openrouter_api_base_dynamic_region(): - assert resolve_openrouter_api_base(region="apac") == "https://apac.openrouter.ai/api/v1" +def test_resolve_openrouter_api_base_unsupported_region_raises(): + with pytest.raises(ValueError, match="Unsupported OpenRouter region"): + resolve_openrouter_api_base(region="apac") + with pytest.raises(ValueError, match="Unsupported OpenRouter region"): + resolve_openrouter_api_base(region="collector.attacker.example/") def test_resolve_openrouter_api_base_from_dict_params(): assert resolve_openrouter_api_base(litellm_params={"region": "eu"}) == "https://eu.openrouter.ai/api/v1" + assert resolve_openrouter_api_base(litellm_params={"region_name": "us"}) == "https://us.openrouter.ai/api/v1" + assert resolve_openrouter_api_base(litellm_params={"data_residency": "eu"}) == "https://eu.openrouter.ai/api/v1" assert resolve_openrouter_api_base(optional_params={"region": "us"}) == "https://us.openrouter.ai/api/v1" + assert resolve_openrouter_api_base(optional_params={"region_name": "eu"}) == "https://eu.openrouter.ai/api/v1" + assert resolve_openrouter_api_base(optional_params={"data_residency": "us"}) == "https://us.openrouter.ai/api/v1" + assert ( + resolve_openrouter_api_base(api_base="https://openrouter.ai", region="eu") == "https://eu.openrouter.ai/api/v1" + ) + assert ( + resolve_openrouter_api_base(api_base="http://openrouter.ai", region="us") == "https://us.openrouter.ai/api/v1" + ) @pytest.mark.parametrize( @@ -79,10 +95,7 @@ def test_infer_openrouter_data_residency(api_base, expected): @pytest.mark.parametrize("custom_llm_provider", [None, "openai", "anthropic", "azure", "bedrock"]) def test_infer_openrouter_data_residency_non_openrouter(custom_llm_provider): - assert ( - infer_openrouter_data_residency(custom_llm_provider, "https://eu.openrouter.ai/api/v1") - is None - ) + assert infer_openrouter_data_residency(custom_llm_provider, "https://eu.openrouter.ai/api/v1") is None def test_get_litellm_params_infers_openrouter_data_residency(): @@ -177,6 +190,38 @@ def test_openrouter_region_not_supported_error_mapping(): ) assert not isinstance(exc_info_standard.value, OpenRouterRegionNotSupportedError) + # 3. 404 with no response object or no URL + fake_exc_no_resp = Exception("404 Not Found") + fake_exc_no_resp.status_code = 404 + with pytest.raises(OpenRouterRegionNotSupportedError) as exc_no_resp: + _map_openrouter_exception( + model="openrouter/anthropic/claude-sonnet-4", + original_exception=fake_exc_no_resp, + custom_llm_provider="openrouter", + error_str=error_msg, + exception_type="NotFoundError", + exception_provider="openrouter", + extra_information="", + ) + assert exc_no_resp.value.region is None + + # 4. Exception class default initialization + custom_err = OpenRouterRegionNotSupportedError(message="error", model="test-model") + assert custom_err.llm_provider == "openrouter" + assert custom_err.region is None + + +def test_openrouter_chat_config_endpoint_already_in_api_base(): + cfg = OpenrouterConfig(region="eu") + url = cfg.get_complete_url( + api_base="https://eu.openrouter.ai/api/v1/chat/completions", + api_key="sk-test", + model="anthropic/claude-3-opus", + optional_params={}, + litellm_params={}, + ) + assert url == "https://eu.openrouter.ai/api/v1/chat/completions" + def test_openrouter_completion_with_region_parameter(monkeypatch): """Test that completion() resolves regional api_base and tracks data_residency.""" @@ -213,4 +258,3 @@ def test_openrouter_completion_with_region_parameter(monkeypatch): assert captured_litellm_params.get("api_base") == "https://eu.openrouter.ai/api/v1" assert captured_litellm_params.get("data_residency") == "eu" assert resp.choices[0].message.content == "Hello from EU" - From afc976a7e9918d0fc1ec5e1e4d15ac9e4d391aea Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 16:27:12 +0300 Subject: [PATCH 03/13] chore(tests): move openrouter data residency tests into unit tests --- .../llms/openrouter/test_openrouter_data_residency.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_litellm => unit}/llms/openrouter/test_openrouter_data_residency.py (100%) diff --git a/tests/test_litellm/llms/openrouter/test_openrouter_data_residency.py b/tests/unit/llms/openrouter/test_openrouter_data_residency.py similarity index 100% rename from tests/test_litellm/llms/openrouter/test_openrouter_data_residency.py rename to tests/unit/llms/openrouter/test_openrouter_data_residency.py From b7530315f88c1440faa67ebab701afa8c04e62a2 Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 16:34:12 +0300 Subject: [PATCH 04/13] fix(openrouter): add strict typing annotations to config and exception classes --- litellm/exceptions.py | 2 +- litellm/llms/openrouter/chat/transformation.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/exceptions.py b/litellm/exceptions.py index df77c2c8f91..eb5437a9121 100644 --- a/litellm/exceptions.py +++ b/litellm/exceptions.py @@ -238,7 +238,7 @@ class OpenRouterRegionNotSupportedError(NotFoundError): max_retries: int | None = None, num_retries: int | None = None, region: str | None = None, - ): + ) -> None: self.region = region super().__init__( message=message, diff --git a/litellm/llms/openrouter/chat/transformation.py b/litellm/llms/openrouter/chat/transformation.py index 49c429cdfc7..61d527b6e48 100644 --- a/litellm/llms/openrouter/chat/transformation.py +++ b/litellm/llms/openrouter/chat/transformation.py @@ -52,8 +52,8 @@ class OpenrouterConfig(OpenAIGPTConfig): def __init__( self, region: str | None = None, - **kwargs, - ): + **kwargs: object, + ) -> None: self.region = region super().__init__(**kwargs) From 59d26ba6b60ce1a889dcdd5ca4ff9957e88ecd88 Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 16:46:14 +0300 Subject: [PATCH 05/13] fix(openrouter): comply with type discipline gate requirements --- .../exception_mapping_utils.py | 12 +-- .../llms/openrouter/chat/transformation.py | 18 ++--- litellm/llms/openrouter/data_residency.py | 74 +++++++++++-------- .../openrouter/embedding/transformation.py | 7 +- litellm/main.py | 4 +- 5 files changed, 65 insertions(+), 50 deletions(-) diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index cfe1e420b3b..bf2ab6a5804 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -2225,15 +2225,17 @@ def _map_openrouter_exception( ) elif original_exception.status_code == 404: if "supporting your data region" in error_str.lower(): - response_obj = getattr(original_exception, "response", None) - inferred_region = None - if response_obj is not None and getattr(response_obj, "url", None): - from litellm.llms.openrouter.data_residency import infer_openrouter_data_residency + response_obj: Final = getattr(original_exception, "response", None) + from litellm.llms.openrouter.data_residency import infer_openrouter_data_residency - inferred_region = infer_openrouter_data_residency( + inferred_region: Final[str | None] = ( + infer_openrouter_data_residency( custom_llm_provider="openrouter", api_base=str(response_obj.url), ) + if response_obj is not None and getattr(response_obj, "url", None) + else None + ) raise OpenRouterRegionNotSupportedError( message=f"OpenRouterRegionNotSupportedError: {exception_provider} - {error_str}", model=model, diff --git a/litellm/llms/openrouter/chat/transformation.py b/litellm/llms/openrouter/chat/transformation.py index 61d527b6e48..daf4ca253eb 100644 --- a/litellm/llms/openrouter/chat/transformation.py +++ b/litellm/llms/openrouter/chat/transformation.py @@ -52,31 +52,29 @@ class OpenrouterConfig(OpenAIGPTConfig): def __init__( self, region: str | None = None, - **kwargs: object, ) -> None: self.region = region - super().__init__(**kwargs) + super().__init__() def get_complete_url( self, api_base: str | None, api_key: str | None, model: str, - optional_params: dict, - litellm_params: dict, + optional_params: Mapping[str, object] | None = None, + litellm_params: Mapping[str, object] | None = None, stream: bool | None = None, ) -> str: - api_base = resolve_openrouter_api_base( + resolved_base: Final = resolve_openrouter_api_base( api_base=api_base, region=self.region, litellm_params=litellm_params, optional_params=optional_params, - ) + ).rstrip("/") endpoint: Final = "chat/completions" - api_base = api_base.rstrip("/") - if endpoint in api_base: - return api_base - return f"{api_base}/{endpoint}" + if endpoint in resolved_base: + return resolved_base + return f"{resolved_base}/{endpoint}" def get_supported_openai_params(self, model: str) -> list: """ diff --git a/litellm/llms/openrouter/data_residency.py b/litellm/llms/openrouter/data_residency.py index 9d31c3c0483..6c38254bd60 100644 --- a/litellm/llms/openrouter/data_residency.py +++ b/litellm/llms/openrouter/data_residency.py @@ -9,6 +9,8 @@ OpenRouter provides dedicated regional endpoints for end-to-end data residency: Docs: https://openrouter.ai/docs/guides/features/in-region-routing """ +from collections.abc import Mapping, MutableMapping +from types import MappingProxyType from typing import Final from urllib.parse import urlparse @@ -16,19 +18,23 @@ import litellm from litellm.secret_managers.main import get_secret_str # Normalized region names mapped to their dedicated OpenRouter base URLs -_OPENROUTER_REGIONAL_URLS: Final[dict[str, str]] = { - "eu": "https://eu.openrouter.ai/api/v1", - "europe": "https://eu.openrouter.ai/api/v1", - "us": "https://us.openrouter.ai/api/v1", - "usa": "https://us.openrouter.ai/api/v1", - "united states": "https://us.openrouter.ai/api/v1", -} +_OPENROUTER_REGIONAL_URLS: Final[Mapping[str, str]] = MappingProxyType( + { + "eu": "https://eu.openrouter.ai/api/v1", + "europe": "https://eu.openrouter.ai/api/v1", + "us": "https://us.openrouter.ai/api/v1", + "usa": "https://us.openrouter.ai/api/v1", + "united states": "https://us.openrouter.ai/api/v1", + } +) # OpenRouter regional hostnames mapped to canonical data residency codes ("eu", "us") -_OPENROUTER_REGIONAL_HOSTNAMES: Final[dict[str, str]] = { - "eu.openrouter.ai": "eu", - "us.openrouter.ai": "us", -} +_OPENROUTER_REGIONAL_HOSTNAMES: Final[Mapping[str, str]] = MappingProxyType( + { + "eu.openrouter.ai": "eu", + "us.openrouter.ai": "us", + } +) _DEFAULT_OPENROUTER_API_BASE: Final[str] = "https://openrouter.ai/api/v1" @@ -36,8 +42,8 @@ _DEFAULT_OPENROUTER_API_BASE: Final[str] = "https://openrouter.ai/api/v1" def resolve_openrouter_api_base( api_base: str | None = None, region: str | None = None, - litellm_params: dict | None = None, - optional_params: dict | None = None, + litellm_params: Mapping[str, object] | None = None, + optional_params: Mapping[str, object] | None = None, ) -> str: """ Resolve the OpenRouter API base URL based on custom api_base, explicit region, or environment. @@ -48,34 +54,44 @@ def resolve_openrouter_api_base( 3. Fallback to `api_base or litellm.api_base or OPENROUTER_API_BASE or default global URL`. """ # Check if a custom api_base was provided that is not the default global endpoint - is_custom_api_base = False - if api_base: - cleaned_base = api_base.rstrip("/").lower() - if cleaned_base not in ("https://openrouter.ai/api/v1", "https://openrouter.ai", "http://openrouter.ai"): - is_custom_api_base = True + cleaned_base: Final[str | None] = api_base.rstrip("/").lower() if api_base else None + is_custom_api_base: Final[bool] = bool( + cleaned_base + and cleaned_base not in ("https://openrouter.ai/api/v1", "https://openrouter.ai", "http://openrouter.ai") + ) if is_custom_api_base and api_base: return api_base # Check for region in litellm_params, optional_params, explicit region argument, config, or environment - resolved_region = None - if isinstance(litellm_params, dict): - resolved_region = ( - litellm_params.get("region") or litellm_params.get("region_name") or litellm_params.get("data_residency") - ) - if not resolved_region and isinstance(optional_params, dict): - resolved_region = ( + raw_from_params: Final[object] = ( + litellm_params.get("region") or litellm_params.get("region_name") or litellm_params.get("data_residency") + if isinstance(litellm_params, Mapping) + else None + ) + raw_from_optional: Final[object] = ( + ( optional_params.pop("region", None) or optional_params.pop("region_name", None) or optional_params.pop("data_residency", None) ) - if not resolved_region: - resolved_region = ( - region or getattr(litellm, "openrouter_region", None) or getattr(litellm, "openrouter_data_residency", None) + if isinstance(optional_params, MutableMapping) + else ( + optional_params.get("region") or optional_params.get("region_name") or optional_params.get("data_residency") + if isinstance(optional_params, Mapping) + else None ) + ) + resolved_region: Final[object] = ( + raw_from_params + or raw_from_optional + or region + or getattr(litellm, "openrouter_region", None) + or getattr(litellm, "openrouter_data_residency", None) + ) if resolved_region and isinstance(resolved_region, str): - normalized_region = resolved_region.strip().lower() + normalized_region: Final[str] = resolved_region.strip().lower() if normalized_region in ("global", "default"): return _DEFAULT_OPENROUTER_API_BASE if normalized_region in _OPENROUTER_REGIONAL_URLS: diff --git a/litellm/llms/openrouter/embedding/transformation.py b/litellm/llms/openrouter/embedding/transformation.py index eb022a81228..54c8e8a20c9 100644 --- a/litellm/llms/openrouter/embedding/transformation.py +++ b/litellm/llms/openrouter/embedding/transformation.py @@ -85,15 +85,14 @@ class OpenrouterEmbeddingConfig(BaseEmbeddingConfig): """ Get the complete URL for OpenRouter Embedding API endpoint. """ - api_base = resolve_openrouter_api_base( + resolved_base: Final = resolve_openrouter_api_base( api_base=api_base, litellm_params=litellm_params, optional_params=optional_params, - ) - api_base = api_base.rstrip("/") + ).rstrip("/") # Return the embeddings endpoint - return f"{api_base}/embeddings" + return f"{resolved_base}/embeddings" def transform_embedding_request( self, diff --git a/litellm/main.py b/litellm/main.py index 14187c18ba2..2e92dd5a325 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -3415,7 +3415,7 @@ def _complete_openrouter(ctx: CompletionDispatchContext) -> _CompletionDispatchR ## Load Config config: Final = litellm.OpenrouterConfig.get_config() - api_base = resolve_openrouter_api_base( + api_base = resolve_openrouter_api_base( # rebind-ok: regional endpoint resolution for openrouter api_base=api_base, region=getattr(config, "region", None), litellm_params=litellm_params, @@ -6788,7 +6788,7 @@ def embedding( headers=headers, ) elif custom_llm_provider == "openrouter": - api_base = resolve_openrouter_api_base( + api_base = resolve_openrouter_api_base( # rebind-ok: regional endpoint resolution for openrouter api_base=api_base, litellm_params=litellm_params_dict, optional_params=optional_params, From 9577025e358e11bfc35d69cc0fe28906661338c6 Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 17:12:09 +0300 Subject: [PATCH 06/13] fix(openrouter): resolve basedpyright type errors in data residency and dispatch --- litellm/__init__.py | 2 +- .../exception_mapping_utils.py | 12 ++-- litellm/llms/openrouter/data_residency.py | 62 +++++++++---------- litellm/main.py | 11 +++- 4 files changed, 46 insertions(+), 41 deletions(-) diff --git a/litellm/__init__.py b/litellm/__init__.py index 978b097f793..8e1fac076dc 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1418,7 +1418,7 @@ from .exceptions import ( ImageFetchError, VectorStoreSearchError, NotFoundError, - OpenRouterRegionNotSupportedError, + OpenRouterRegionNotSupportedError as OpenRouterRegionNotSupportedError, PermissionDeniedError, RateLimitError, RateLimitErrorCategory, diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index bf2ab6a5804..df97956dd4e 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -18,6 +18,7 @@ from litellm.litellm_core_utils.bug_report import ( ) from litellm.litellm_core_utils.core_helpers import as_str_mapping from litellm.litellm_core_utils.secret_redaction import redact_string as redact_secret_string +from litellm.llms.openrouter.data_residency import infer_openrouter_data_residency from litellm.types.utils import LlmProviders from ..exceptions import ( @@ -2225,22 +2226,21 @@ def _map_openrouter_exception( ) elif original_exception.status_code == 404: if "supporting your data region" in error_str.lower(): - response_obj: Final = getattr(original_exception, "response", None) - from litellm.llms.openrouter.data_residency import infer_openrouter_data_residency - + response_obj: Final[object] = getattr(original_exception, "response", None) + resp_url: Final[object] = getattr(response_obj, "url", None) if response_obj is not None else None inferred_region: Final[str | None] = ( infer_openrouter_data_residency( custom_llm_provider="openrouter", - api_base=str(response_obj.url), + api_base=str(resp_url), ) - if response_obj is not None and getattr(response_obj, "url", None) + if resp_url is not None else None ) raise OpenRouterRegionNotSupportedError( message=f"OpenRouterRegionNotSupportedError: {exception_provider} - {error_str}", model=model, llm_provider=custom_llm_provider, - response=response_obj, + response=response_obj if isinstance(response_obj, httpx.Response) else None, litellm_debug_info=extra_information, region=inferred_region, ) diff --git a/litellm/llms/openrouter/data_residency.py b/litellm/llms/openrouter/data_residency.py index 6c38254bd60..bd0ee774c0d 100644 --- a/litellm/llms/openrouter/data_residency.py +++ b/litellm/llms/openrouter/data_residency.py @@ -9,7 +9,7 @@ OpenRouter provides dedicated regional endpoints for end-to-end data residency: Docs: https://openrouter.ai/docs/guides/features/in-region-routing """ -from collections.abc import Mapping, MutableMapping +from collections.abc import Mapping from types import MappingProxyType from typing import Final from urllib.parse import urlparse @@ -39,11 +39,22 @@ _OPENROUTER_REGIONAL_HOSTNAMES: Final[Mapping[str, str]] = MappingProxyType( _DEFAULT_OPENROUTER_API_BASE: Final[str] = "https://openrouter.ai/api/v1" +def _extract_region_value(params: object) -> str | None: + if not isinstance(params, Mapping): + return None + for key in ("region", "region_name", "data_residency"): + if key in params: + val: Final[object] = params[key] + if isinstance(val, str) and val.strip(): + return val.strip() + return None + + def resolve_openrouter_api_base( api_base: str | None = None, region: str | None = None, - litellm_params: Mapping[str, object] | None = None, - optional_params: Mapping[str, object] | None = None, + litellm_params: object = None, + optional_params: object = None, ) -> str: """ Resolve the OpenRouter API base URL based on custom api_base, explicit region, or environment. @@ -64,33 +75,20 @@ def resolve_openrouter_api_base( return api_base # Check for region in litellm_params, optional_params, explicit region argument, config, or environment - raw_from_params: Final[object] = ( - litellm_params.get("region") or litellm_params.get("region_name") or litellm_params.get("data_residency") - if isinstance(litellm_params, Mapping) - else None - ) - raw_from_optional: Final[object] = ( - ( - optional_params.pop("region", None) - or optional_params.pop("region_name", None) - or optional_params.pop("data_residency", None) - ) - if isinstance(optional_params, MutableMapping) - else ( - optional_params.get("region") or optional_params.get("region_name") or optional_params.get("data_residency") - if isinstance(optional_params, Mapping) - else None - ) - ) - resolved_region: Final[object] = ( - raw_from_params - or raw_from_optional - or region - or getattr(litellm, "openrouter_region", None) - or getattr(litellm, "openrouter_data_residency", None) + region_from_params: Final[str | None] = _extract_region_value(litellm_params) + region_from_optional: Final[str | None] = _extract_region_value(optional_params) + + litellm_region: Final[object] = getattr(litellm, "openrouter_region", None) + litellm_data_residency: Final[object] = getattr(litellm, "openrouter_data_residency", None) + attr_region: Final[str | None] = ( + litellm_region + if isinstance(litellm_region, str) + else (litellm_data_residency if isinstance(litellm_data_residency, str) else None) ) - if resolved_region and isinstance(resolved_region, str): + resolved_region: Final[str | None] = region_from_params or region_from_optional or region or attr_region + + if resolved_region is not None: normalized_region: Final[str] = resolved_region.strip().lower() if normalized_region in ("global", "default"): return _DEFAULT_OPENROUTER_API_BASE @@ -104,7 +102,8 @@ def resolve_openrouter_api_base( f"Unsupported OpenRouter region: '{resolved_region}'. Supported regions are 'eu', 'us', 'global'." ) - return api_base or litellm.api_base or get_secret_str("OPENROUTER_API_BASE") or _DEFAULT_OPENROUTER_API_BASE + fallback_api_base: Final[str | None] = api_base or litellm.api_base or get_secret_str("OPENROUTER_API_BASE") + return fallback_api_base or _DEFAULT_OPENROUTER_API_BASE def infer_openrouter_data_residency( @@ -121,11 +120,12 @@ def infer_openrouter_data_residency( return None try: - host: Final = urlparse(api_base).hostname + parsed_url: Final = urlparse(api_base) + host: Final[str | None] = parsed_url.hostname except (TypeError, ValueError): return None - if not host: + if host is None: return None return _OPENROUTER_REGIONAL_HOSTNAMES.get(host.lower()) diff --git a/litellm/main.py b/litellm/main.py index 2e92dd5a325..581c307e889 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -3415,13 +3415,18 @@ def _complete_openrouter(ctx: CompletionDispatchContext) -> _CompletionDispatchR ## Load Config config: Final = litellm.OpenrouterConfig.get_config() + config_region: Final[str | None] = ( + ctx.provider_config.region + if isinstance(ctx.provider_config, litellm.OpenrouterConfig) and isinstance(ctx.provider_config.region, str) + else (litellm.OpenrouterConfig.region if isinstance(litellm.OpenrouterConfig.region, str) else None) + ) api_base = resolve_openrouter_api_base( # rebind-ok: regional endpoint resolution for openrouter api_base=api_base, - region=getattr(config, "region", None), + region=config_region, litellm_params=litellm_params, optional_params=optional_params, ) - if isinstance(litellm_params, dict): + if litellm_params is not None: litellm_params["api_base"] = api_base litellm_params["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) @@ -6793,7 +6798,7 @@ def embedding( litellm_params=litellm_params_dict, optional_params=optional_params, ) - if isinstance(litellm_params_dict, dict): + if litellm_params_dict is not None: litellm_params_dict["api_base"] = api_base litellm_params_dict["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) From 5bb33e6b8e04703533f75d45d7873cb26b50058b Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 17:49:55 +0300 Subject: [PATCH 07/13] fix(openrouter): align get_complete_url signature and eliminate basedpyright type errors --- .../llms/openrouter/chat/transformation.py | 10 +++-- litellm/llms/openrouter/data_residency.py | 15 +++---- .../openrouter/embedding/transformation.py | 9 ++-- litellm/main.py | 43 ++++++++++++------- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/litellm/llms/openrouter/chat/transformation.py b/litellm/llms/openrouter/chat/transformation.py index daf4ca253eb..0988af3e062 100644 --- a/litellm/llms/openrouter/chat/transformation.py +++ b/litellm/llms/openrouter/chat/transformation.py @@ -61,15 +61,17 @@ class OpenrouterConfig(OpenAIGPTConfig): api_base: str | None, api_key: str | None, model: str, - optional_params: Mapping[str, object] | None = None, - litellm_params: Mapping[str, object] | None = None, + optional_params: dict, + litellm_params: dict, stream: bool | None = None, ) -> str: + casted_optional_params: Final[Mapping[str, object]] = cast(Mapping[str, object], optional_params) + casted_litellm_params: Final[Mapping[str, object]] = cast(Mapping[str, object], litellm_params) resolved_base: Final = resolve_openrouter_api_base( api_base=api_base, region=self.region, - litellm_params=litellm_params, - optional_params=optional_params, + litellm_params=casted_litellm_params, + optional_params=casted_optional_params, ).rstrip("/") endpoint: Final = "chat/completions" if endpoint in resolved_base: diff --git a/litellm/llms/openrouter/data_residency.py b/litellm/llms/openrouter/data_residency.py index bd0ee774c0d..5f1069f079d 100644 --- a/litellm/llms/openrouter/data_residency.py +++ b/litellm/llms/openrouter/data_residency.py @@ -39,22 +39,21 @@ _OPENROUTER_REGIONAL_HOSTNAMES: Final[Mapping[str, str]] = MappingProxyType( _DEFAULT_OPENROUTER_API_BASE: Final[str] = "https://openrouter.ai/api/v1" -def _extract_region_value(params: object) -> str | None: - if not isinstance(params, Mapping): +def _extract_region_value(params: Mapping[str, object] | None) -> str | None: + if params is None or not isinstance(params, Mapping): return None for key in ("region", "region_name", "data_residency"): - if key in params: - val: Final[object] = params[key] - if isinstance(val, str) and val.strip(): - return val.strip() + val: Final[object] = params.get(key) + if isinstance(val, str) and val.strip(): + return val.strip() return None def resolve_openrouter_api_base( api_base: str | None = None, region: str | None = None, - litellm_params: object = None, - optional_params: object = None, + litellm_params: Mapping[str, object] | None = None, + optional_params: Mapping[str, object] | None = None, ) -> str: """ Resolve the OpenRouter API base URL based on custom api_base, explicit region, or environment. diff --git a/litellm/llms/openrouter/embedding/transformation.py b/litellm/llms/openrouter/embedding/transformation.py index 54c8e8a20c9..22524dea137 100644 --- a/litellm/llms/openrouter/embedding/transformation.py +++ b/litellm/llms/openrouter/embedding/transformation.py @@ -7,7 +7,8 @@ OpenRouter is OpenAI-compatible and supports embeddings via the /v1/embeddings e Docs: https://openrouter.ai/docs """ -from typing import TYPE_CHECKING, Any, Final +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Final, cast import httpx @@ -85,10 +86,12 @@ class OpenrouterEmbeddingConfig(BaseEmbeddingConfig): """ Get the complete URL for OpenRouter Embedding API endpoint. """ + casted_optional_params: Final[Mapping[str, object]] = cast(Mapping[str, object], optional_params) + casted_litellm_params: Final[Mapping[str, object]] = cast(Mapping[str, object], litellm_params) resolved_base: Final = resolve_openrouter_api_base( api_base=api_base, - litellm_params=litellm_params, - optional_params=optional_params, + litellm_params=casted_litellm_params, + optional_params=casted_optional_params, ).rstrip("/") # Return the embeddings endpoint diff --git a/litellm/main.py b/litellm/main.py index 581c307e889..d1e2c0e94ee 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -3415,20 +3415,24 @@ def _complete_openrouter(ctx: CompletionDispatchContext) -> _CompletionDispatchR ## Load Config config: Final = litellm.OpenrouterConfig.get_config() - config_region: Final[str | None] = ( - ctx.provider_config.region - if isinstance(ctx.provider_config, litellm.OpenrouterConfig) and isinstance(ctx.provider_config.region, str) - else (litellm.OpenrouterConfig.region if isinstance(litellm.OpenrouterConfig.region, str) else None) + casted_optional_params: Final[Mapping[str, object]] = cast(Mapping[str, object], optional_params) + raw_region: Final[object] = ( + litellm_params.get("region") + or litellm_params.get("region_name") + or litellm_params.get("data_residency") + or casted_optional_params.get("region") + or casted_optional_params.get("region_name") + or casted_optional_params.get("data_residency") + or getattr(ctx.provider_config, "region", None) + or getattr(litellm.OpenrouterConfig, "region", None) ) + resolved_region: Final[str | None] = raw_region if isinstance(raw_region, str) else None api_base = resolve_openrouter_api_base( # rebind-ok: regional endpoint resolution for openrouter api_base=api_base, - region=config_region, - litellm_params=litellm_params, - optional_params=optional_params, + region=resolved_region, ) - if litellm_params is not None: - litellm_params["api_base"] = api_base - litellm_params["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) + litellm_params["api_base"] = api_base + litellm_params["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) api_key = ( api_key @@ -6793,14 +6797,23 @@ def embedding( headers=headers, ) elif custom_llm_provider == "openrouter": + casted_optional_params: Final[Mapping[str, object]] = cast(Mapping[str, object], optional_params) + raw_region: Final[object] = ( + litellm_params_dict.get("region") + or litellm_params_dict.get("region_name") + or litellm_params_dict.get("data_residency") + or casted_optional_params.get("region") + or casted_optional_params.get("region_name") + or casted_optional_params.get("data_residency") + or getattr(litellm.OpenrouterConfig, "region", None) + ) + resolved_region: Final[str | None] = raw_region if isinstance(raw_region, str) else None api_base = resolve_openrouter_api_base( # rebind-ok: regional endpoint resolution for openrouter api_base=api_base, - litellm_params=litellm_params_dict, - optional_params=optional_params, + region=resolved_region, ) - if litellm_params_dict is not None: - litellm_params_dict["api_base"] = api_base - litellm_params_dict["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) + litellm_params_dict["api_base"] = api_base + litellm_params_dict["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) api_key = ( api_key From 7ec5a4df6018988f153a5f49b6dc6eb61aca8a97 Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 17:56:45 +0300 Subject: [PATCH 08/13] fix(openrouter): remove cast in embedding transformation to pass TID251 strict rule --- litellm/llms/openrouter/embedding/transformation.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/litellm/llms/openrouter/embedding/transformation.py b/litellm/llms/openrouter/embedding/transformation.py index 22524dea137..d61f2b14b6b 100644 --- a/litellm/llms/openrouter/embedding/transformation.py +++ b/litellm/llms/openrouter/embedding/transformation.py @@ -7,8 +7,7 @@ OpenRouter is OpenAI-compatible and supports embeddings via the /v1/embeddings e Docs: https://openrouter.ai/docs """ -from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, Final, cast +from typing import TYPE_CHECKING, Any, Final import httpx @@ -86,13 +85,7 @@ class OpenrouterEmbeddingConfig(BaseEmbeddingConfig): """ Get the complete URL for OpenRouter Embedding API endpoint. """ - casted_optional_params: Final[Mapping[str, object]] = cast(Mapping[str, object], optional_params) - casted_litellm_params: Final[Mapping[str, object]] = cast(Mapping[str, object], litellm_params) - resolved_base: Final = resolve_openrouter_api_base( - api_base=api_base, - litellm_params=casted_litellm_params, - optional_params=casted_optional_params, - ).rstrip("/") + resolved_base: Final = resolve_openrouter_api_base(api_base=api_base).rstrip("/") # Return the embeddings endpoint return f"{resolved_base}/embeddings" From 4a1d158ccf3f3946c96a9e5891df4f6222d492fd Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 18:07:40 +0300 Subject: [PATCH 09/13] fix(openrouter): add mutable-ok and cast-ok discipline annotations --- litellm/llms/openrouter/chat/transformation.py | 16 ++++++++++++---- litellm/main.py | 10 ++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/litellm/llms/openrouter/chat/transformation.py b/litellm/llms/openrouter/chat/transformation.py index 0988af3e062..d71724609d3 100644 --- a/litellm/llms/openrouter/chat/transformation.py +++ b/litellm/llms/openrouter/chat/transformation.py @@ -61,12 +61,20 @@ class OpenrouterConfig(OpenAIGPTConfig): api_base: str | None, api_key: str | None, model: str, - optional_params: dict, - litellm_params: dict, + optional_params: dict, # mutable-ok: overrides OpenAIGPTConfig.get_complete_url signature + litellm_params: dict, # mutable-ok: overrides OpenAIGPTConfig.get_complete_url signature stream: bool | None = None, ) -> str: - casted_optional_params: Final[Mapping[str, object]] = cast(Mapping[str, object], optional_params) - casted_litellm_params: Final[Mapping[str, object]] = cast(Mapping[str, object], litellm_params) + casted_optional_params: Final[Mapping[str, object]] = ( + cast( # cast-ok: type discipline for untyped optional_params + Mapping[str, object], optional_params + ) + ) + casted_litellm_params: Final[Mapping[str, object]] = ( + cast( # cast-ok: type discipline for untyped litellm_params + Mapping[str, object], litellm_params + ) + ) resolved_base: Final = resolve_openrouter_api_base( api_base=api_base, region=self.region, diff --git a/litellm/main.py b/litellm/main.py index d1e2c0e94ee..95219a9afa3 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -3415,7 +3415,9 @@ def _complete_openrouter(ctx: CompletionDispatchContext) -> _CompletionDispatchR ## Load Config config: Final = litellm.OpenrouterConfig.get_config() - casted_optional_params: Final[Mapping[str, object]] = cast(Mapping[str, object], optional_params) + casted_optional_params: Final[Mapping[str, object]] = cast( # cast-ok: type discipline for untyped optional_params + Mapping[str, object], optional_params + ) raw_region: Final[object] = ( litellm_params.get("region") or litellm_params.get("region_name") @@ -6797,7 +6799,11 @@ def embedding( headers=headers, ) elif custom_llm_provider == "openrouter": - casted_optional_params: Final[Mapping[str, object]] = cast(Mapping[str, object], optional_params) + casted_optional_params: Final[Mapping[str, object]] = ( + cast( # cast-ok: type discipline for untyped optional_params + Mapping[str, object], optional_params + ) + ) raw_region: Final[object] = ( litellm_params_dict.get("region") or litellm_params_dict.get("region_name") From 5ce51cf5f0670aae3bb1eff72ae05cafed3e8fa7 Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 18:39:33 +0300 Subject: [PATCH 10/13] fix(openrouter): simplify parameter extraction and resolve basedpyright diagnostics --- .../exception_mapping_utils.py | 2 +- .../llms/openrouter/chat/transformation.py | 18 ++------ litellm/llms/openrouter/data_residency.py | 15 +++---- litellm/main.py | 42 ++++--------------- 4 files changed, 22 insertions(+), 55 deletions(-) diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index df97956dd4e..ade5b0e4046 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -2240,7 +2240,7 @@ def _map_openrouter_exception( message=f"OpenRouterRegionNotSupportedError: {exception_provider} - {error_str}", model=model, llm_provider=custom_llm_provider, - response=response_obj if isinstance(response_obj, httpx.Response) else None, + response=getattr(original_exception, "response", None), litellm_debug_info=extra_information, region=inferred_region, ) diff --git a/litellm/llms/openrouter/chat/transformation.py b/litellm/llms/openrouter/chat/transformation.py index d71724609d3..5af09cffbe1 100644 --- a/litellm/llms/openrouter/chat/transformation.py +++ b/litellm/llms/openrouter/chat/transformation.py @@ -61,25 +61,15 @@ class OpenrouterConfig(OpenAIGPTConfig): api_base: str | None, api_key: str | None, model: str, - optional_params: dict, # mutable-ok: overrides OpenAIGPTConfig.get_complete_url signature - litellm_params: dict, # mutable-ok: overrides OpenAIGPTConfig.get_complete_url signature + optional_params: dict, # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType] # mutable-ok: overrides OpenAIGPTConfig.get_complete_url signature + litellm_params: dict, # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType] # mutable-ok: overrides OpenAIGPTConfig.get_complete_url signature stream: bool | None = None, ) -> str: - casted_optional_params: Final[Mapping[str, object]] = ( - cast( # cast-ok: type discipline for untyped optional_params - Mapping[str, object], optional_params - ) - ) - casted_litellm_params: Final[Mapping[str, object]] = ( - cast( # cast-ok: type discipline for untyped litellm_params - Mapping[str, object], litellm_params - ) - ) resolved_base: Final = resolve_openrouter_api_base( api_base=api_base, region=self.region, - litellm_params=casted_litellm_params, - optional_params=casted_optional_params, + litellm_params=litellm_params, + optional_params=optional_params, ).rstrip("/") endpoint: Final = "chat/completions" if endpoint in resolved_base: diff --git a/litellm/llms/openrouter/data_residency.py b/litellm/llms/openrouter/data_residency.py index 5f1069f079d..3c23ec7552b 100644 --- a/litellm/llms/openrouter/data_residency.py +++ b/litellm/llms/openrouter/data_residency.py @@ -39,11 +39,11 @@ _OPENROUTER_REGIONAL_HOSTNAMES: Final[Mapping[str, str]] = MappingProxyType( _DEFAULT_OPENROUTER_API_BASE: Final[str] = "https://openrouter.ai/api/v1" -def _extract_region_value(params: Mapping[str, object] | None) -> str | None: - if params is None or not isinstance(params, Mapping): +def _extract_region_value(params: object) -> str | None: + if not isinstance(params, Mapping): return None for key in ("region", "region_name", "data_residency"): - val: Final[object] = params.get(key) + val: object = params.get(key) if isinstance(val, str) and val.strip(): return val.strip() return None @@ -51,9 +51,9 @@ def _extract_region_value(params: Mapping[str, object] | None) -> str | None: def resolve_openrouter_api_base( api_base: str | None = None, - region: str | None = None, - litellm_params: Mapping[str, object] | None = None, - optional_params: Mapping[str, object] | None = None, + region: object = None, + litellm_params: object = None, + optional_params: object = None, ) -> str: """ Resolve the OpenRouter API base URL based on custom api_base, explicit region, or environment. @@ -85,7 +85,8 @@ def resolve_openrouter_api_base( else (litellm_data_residency if isinstance(litellm_data_residency, str) else None) ) - resolved_region: Final[str | None] = region_from_params or region_from_optional or region or attr_region + explicit_region: Final[str | None] = region.strip() if isinstance(region, str) and region.strip() else None + resolved_region: Final[str | None] = region_from_params or region_from_optional or explicit_region or attr_region if resolved_region is not None: normalized_region: Final[str] = resolved_region.strip().lower() diff --git a/litellm/main.py b/litellm/main.py index 95219a9afa3..cd0c7171919 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -3413,29 +3413,18 @@ def _complete_openrouter(ctx: CompletionDispatchContext) -> _CompletionDispatchR stream: Final = ctx.stream timeout: Final = ctx.timeout - ## Load Config - config: Final = litellm.OpenrouterConfig.get_config() - casted_optional_params: Final[Mapping[str, object]] = cast( # cast-ok: type discipline for untyped optional_params - Mapping[str, object], optional_params - ) - raw_region: Final[object] = ( - litellm_params.get("region") - or litellm_params.get("region_name") - or litellm_params.get("data_residency") - or casted_optional_params.get("region") - or casted_optional_params.get("region_name") - or casted_optional_params.get("data_residency") - or getattr(ctx.provider_config, "region", None) - or getattr(litellm.OpenrouterConfig, "region", None) - ) - resolved_region: Final[str | None] = raw_region if isinstance(raw_region, str) else None api_base = resolve_openrouter_api_base( # rebind-ok: regional endpoint resolution for openrouter api_base=api_base, - region=resolved_region, + region=getattr(ctx.provider_config, "region", None) or getattr(litellm.OpenrouterConfig, "region", None), + litellm_params=litellm_params, + optional_params=optional_params, ) litellm_params["api_base"] = api_base litellm_params["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) + ## Load Config + config: Final = litellm.OpenrouterConfig.get_config() + api_key = ( api_key or litellm.api_key @@ -6799,24 +6788,11 @@ def embedding( headers=headers, ) elif custom_llm_provider == "openrouter": - casted_optional_params: Final[Mapping[str, object]] = ( - cast( # cast-ok: type discipline for untyped optional_params - Mapping[str, object], optional_params - ) - ) - raw_region: Final[object] = ( - litellm_params_dict.get("region") - or litellm_params_dict.get("region_name") - or litellm_params_dict.get("data_residency") - or casted_optional_params.get("region") - or casted_optional_params.get("region_name") - or casted_optional_params.get("data_residency") - or getattr(litellm.OpenrouterConfig, "region", None) - ) - resolved_region: Final[str | None] = raw_region if isinstance(raw_region, str) else None api_base = resolve_openrouter_api_base( # rebind-ok: regional endpoint resolution for openrouter api_base=api_base, - region=resolved_region, + region=getattr(litellm.OpenrouterConfig, "region", None), + litellm_params=litellm_params_dict, + optional_params=optional_params, ) litellm_params_dict["api_base"] = api_base litellm_params_dict["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) From 69b006cf019c10a677531abcc68a0a6a5cc80069 Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 18:54:31 +0300 Subject: [PATCH 11/13] fix(openrouter): annotate untyped arguments to eliminate basedpyright breaches --- litellm/llms/openrouter/chat/transformation.py | 4 ++-- litellm/llms/openrouter/data_residency.py | 10 +++++----- litellm/main.py | 8 +++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/litellm/llms/openrouter/chat/transformation.py b/litellm/llms/openrouter/chat/transformation.py index 5af09cffbe1..ead80615979 100644 --- a/litellm/llms/openrouter/chat/transformation.py +++ b/litellm/llms/openrouter/chat/transformation.py @@ -68,8 +68,8 @@ class OpenrouterConfig(OpenAIGPTConfig): resolved_base: Final = resolve_openrouter_api_base( api_base=api_base, region=self.region, - litellm_params=litellm_params, - optional_params=optional_params, + litellm_params=litellm_params, # pyright: ignore[reportUnknownArgumentType] # overrides untyped OpenAIGPTConfig signature + optional_params=optional_params, # pyright: ignore[reportUnknownArgumentType] # overrides untyped OpenAIGPTConfig signature ).rstrip("/") endpoint: Final = "chat/completions" if endpoint in resolved_base: diff --git a/litellm/llms/openrouter/data_residency.py b/litellm/llms/openrouter/data_residency.py index 3c23ec7552b..8b7ec94187f 100644 --- a/litellm/llms/openrouter/data_residency.py +++ b/litellm/llms/openrouter/data_residency.py @@ -39,8 +39,8 @@ _OPENROUTER_REGIONAL_HOSTNAMES: Final[Mapping[str, str]] = MappingProxyType( _DEFAULT_OPENROUTER_API_BASE: Final[str] = "https://openrouter.ai/api/v1" -def _extract_region_value(params: object) -> str | None: - if not isinstance(params, Mapping): +def _extract_region_value(params: Mapping[str, object] | None) -> str | None: + if params is None: return None for key in ("region", "region_name", "data_residency"): val: object = params.get(key) @@ -51,9 +51,9 @@ def _extract_region_value(params: object) -> str | None: def resolve_openrouter_api_base( api_base: str | None = None, - region: object = None, - litellm_params: object = None, - optional_params: object = None, + region: str | None = None, + litellm_params: Mapping[str, object] | None = None, + optional_params: Mapping[str, object] | None = None, ) -> str: """ Resolve the OpenRouter API base URL based on custom api_base, explicit region, or environment. diff --git a/litellm/main.py b/litellm/main.py index cd0c7171919..84a164caa43 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -3417,8 +3417,9 @@ def _complete_openrouter(ctx: CompletionDispatchContext) -> _CompletionDispatchR api_base=api_base, region=getattr(ctx.provider_config, "region", None) or getattr(litellm.OpenrouterConfig, "region", None), litellm_params=litellm_params, - optional_params=optional_params, + optional_params=optional_params, # pyright: ignore[reportUnknownArgumentType] # untyped optional_params in dispatch context ) + litellm_params["api_base"] = api_base litellm_params["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) @@ -6791,9 +6792,10 @@ def embedding( api_base = resolve_openrouter_api_base( # rebind-ok: regional endpoint resolution for openrouter api_base=api_base, region=getattr(litellm.OpenrouterConfig, "region", None), - litellm_params=litellm_params_dict, - optional_params=optional_params, + litellm_params=litellm_params_dict, # pyright: ignore[reportUnknownArgumentType] # untyped litellm_params in embedding + optional_params=optional_params, # pyright: ignore[reportUnknownArgumentType] # untyped optional_params in embedding ) + litellm_params_dict["api_base"] = api_base litellm_params_dict["data_residency"] = infer_openrouter_data_residency("openrouter", api_base) From f844ec8efca4ad35da88fc40c56c3b86e00cd176 Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 19:08:52 +0300 Subject: [PATCH 12/13] fix(openrouter): remove redundant getattr calls in dispatch to satisfy basedpyright --- litellm/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 84a164caa43..2aa8cbd759a 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -3415,7 +3415,6 @@ def _complete_openrouter(ctx: CompletionDispatchContext) -> _CompletionDispatchR api_base = resolve_openrouter_api_base( # rebind-ok: regional endpoint resolution for openrouter api_base=api_base, - region=getattr(ctx.provider_config, "region", None) or getattr(litellm.OpenrouterConfig, "region", None), litellm_params=litellm_params, optional_params=optional_params, # pyright: ignore[reportUnknownArgumentType] # untyped optional_params in dispatch context ) @@ -6791,7 +6790,6 @@ def embedding( elif custom_llm_provider == "openrouter": api_base = resolve_openrouter_api_base( # rebind-ok: regional endpoint resolution for openrouter api_base=api_base, - region=getattr(litellm.OpenrouterConfig, "region", None), litellm_params=litellm_params_dict, # pyright: ignore[reportUnknownArgumentType] # untyped litellm_params in embedding optional_params=optional_params, # pyright: ignore[reportUnknownArgumentType] # untyped optional_params in embedding ) From b35689d8f81b6eb3f2b66593ec2d9ec12139b2ee Mon Sep 17 00:00:00 2001 From: yyouretoast Date: Fri, 16 Oct 2026 19:23:52 +0300 Subject: [PATCH 13/13] fix(openrouter): add pyright ignores for untyped parent class __init__ and httpx --- litellm/exceptions.py | 4 ++-- litellm/llms/openrouter/chat/transformation.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/exceptions.py b/litellm/exceptions.py index eb5437a9121..eb75de40653 100644 --- a/litellm/exceptions.py +++ b/litellm/exceptions.py @@ -233,14 +233,14 @@ class OpenRouterRegionNotSupportedError(NotFoundError): message: str, model: str, llm_provider: str = "openrouter", - response: httpx.Response | None = None, + response: httpx.Response | None = None, # pyright: ignore[reportUnknownMemberType,reportUnknownParameterType] # httpx.Response type is untyped litellm_debug_info: str | None = None, max_retries: int | None = None, num_retries: int | None = None, region: str | None = None, ) -> None: self.region = region - super().__init__( + super().__init__( # pyright: ignore[reportUnknownMemberType] # NotFoundError.__init__ is untyped message=message, model=model, llm_provider=llm_provider, diff --git a/litellm/llms/openrouter/chat/transformation.py b/litellm/llms/openrouter/chat/transformation.py index ead80615979..f0e03789a54 100644 --- a/litellm/llms/openrouter/chat/transformation.py +++ b/litellm/llms/openrouter/chat/transformation.py @@ -54,7 +54,7 @@ class OpenrouterConfig(OpenAIGPTConfig): region: str | None = None, ) -> None: self.region = region - super().__init__() + super().__init__() # pyright: ignore[reportUnknownMemberType] # OpenAIGPTConfig.__init__ is untyped def get_complete_url( self,