11from __future__ import annotations
22
3+ from typing import Literal , cast
4+
35from openai import AsyncOpenAI
46
57from ..exceptions import UserError
68from .interface import Model , ModelProvider
79from .openai_provider import OpenAIProvider
810
11+ MultiProviderOpenAIPrefixMode = Literal ["alias" , "model_id" ]
12+ MultiProviderUnknownPrefixMode = Literal ["error" , "model_id" ]
13+
914
1015class MultiProviderMap :
1116 """A map of model name prefixes to ModelProviders."""
@@ -57,7 +62,11 @@ class MultiProvider(ModelProvider):
5762 - "openai/" prefix or no prefix -> OpenAIProvider. e.g. "openai/gpt-4.1", "gpt-4.1"
5863 - "litellm/" prefix -> LitellmProvider. e.g. "litellm/openai/gpt-4.1"
5964
60- You can override or customize this mapping.
65+ You can override or customize this mapping. The ``openai`` prefix is ambiguous for some
66+ OpenAI-compatible backends because a string like ``openai/gpt-4.1`` could mean either "route
67+ to the OpenAI provider and use model ``gpt-4.1``" or "send the literal model ID
68+ ``openai/gpt-4.1`` to the configured OpenAI-compatible endpoint." The prefix mode options let
69+ callers opt into the second behavior without breaking the historical alias semantics.
6170 """
6271
6372 def __init__ (
@@ -72,6 +81,8 @@ def __init__(
7281 openai_use_responses : bool | None = None ,
7382 openai_use_responses_websocket : bool | None = None ,
7483 openai_websocket_base_url : str | None = None ,
84+ openai_prefix_mode : MultiProviderOpenAIPrefixMode = "alias" ,
85+ unknown_prefix_mode : MultiProviderUnknownPrefixMode = "error" ,
7586 ) -> None :
7687 """Create a new OpenAI provider.
7788
@@ -92,6 +103,15 @@ def __init__(
92103 responses API.
93104 openai_websocket_base_url: The websocket base URL to use for the OpenAI provider.
94105 If not provided, the provider will use `OPENAI_WEBSOCKET_BASE_URL` when set.
106+ openai_prefix_mode: Controls how ``openai/...`` model strings are interpreted.
107+ ``"alias"`` preserves the historical behavior and strips the ``openai/`` prefix
108+ before calling the OpenAI provider. ``"model_id"`` keeps the full string and is
109+ useful for OpenAI-compatible endpoints that expect literal namespaced model IDs.
110+ unknown_prefix_mode: Controls how prefixes outside the explicit provider map and
111+ built-in fallbacks are handled. ``"error"`` preserves the historical fail-fast
112+ behavior and raises ``UserError``. ``"model_id"`` passes the full string through to
113+ the OpenAI provider so OpenAI-compatible endpoints can receive namespaced model IDs
114+ such as ``openrouter/openai/gpt-4o``.
95115 """
96116 self .provider_map = provider_map
97117 self .openai_provider = OpenAIProvider (
@@ -104,6 +124,8 @@ def __init__(
104124 use_responses = openai_use_responses ,
105125 use_responses_websocket = openai_use_responses_websocket ,
106126 )
127+ self ._openai_prefix_mode = self ._validate_openai_prefix_mode (openai_prefix_mode )
128+ self ._unknown_prefix_mode = self ._validate_unknown_prefix_mode (unknown_prefix_mode )
107129
108130 self ._fallback_providers : dict [str , ModelProvider ] = {}
109131
@@ -124,6 +146,20 @@ def _create_fallback_provider(self, prefix: str) -> ModelProvider:
124146 else :
125147 raise UserError (f"Unknown prefix: { prefix } " )
126148
149+ @staticmethod
150+ def _validate_openai_prefix_mode (mode : str ) -> MultiProviderOpenAIPrefixMode :
151+ if mode not in {"alias" , "model_id" }:
152+ raise UserError ("MultiProvider openai_prefix_mode must be one of: 'alias', 'model_id'." )
153+ return cast (MultiProviderOpenAIPrefixMode , mode )
154+
155+ @staticmethod
156+ def _validate_unknown_prefix_mode (mode : str ) -> MultiProviderUnknownPrefixMode :
157+ if mode not in {"error" , "model_id" }:
158+ raise UserError (
159+ "MultiProvider unknown_prefix_mode must be one of: 'error', 'model_id'."
160+ )
161+ return cast (MultiProviderUnknownPrefixMode , mode )
162+
127163 def _get_fallback_provider (self , prefix : str | None ) -> ModelProvider :
128164 if prefix is None or prefix == "openai" :
129165 return self .openai_provider
@@ -133,6 +169,31 @@ def _get_fallback_provider(self, prefix: str | None) -> ModelProvider:
133169 self ._fallback_providers [prefix ] = self ._create_fallback_provider (prefix )
134170 return self ._fallback_providers [prefix ]
135171
172+ def _resolve_prefixed_model (
173+ self ,
174+ * ,
175+ original_model_name : str ,
176+ prefix : str ,
177+ stripped_model_name : str | None ,
178+ ) -> tuple [ModelProvider , str | None ]:
179+ # Explicit provider_map entries are the least surprising routing mechanism, so they always
180+ # win over the built-in OpenAI alias and unknown-prefix fallback behavior.
181+ if self .provider_map and (provider := self .provider_map .get_provider (prefix )):
182+ return provider , stripped_model_name
183+
184+ if prefix == "litellm" :
185+ return self ._get_fallback_provider (prefix ), stripped_model_name
186+
187+ if prefix == "openai" :
188+ if self ._openai_prefix_mode == "alias" :
189+ return self .openai_provider , stripped_model_name
190+ return self .openai_provider , original_model_name
191+
192+ if self ._unknown_prefix_mode == "model_id" :
193+ return self .openai_provider , original_model_name
194+
195+ raise UserError (f"Unknown prefix: { prefix } " )
196+
136197 def get_model (self , model_name : str | None ) -> Model :
137198 """Returns a Model based on the model name. The model name can have a prefix, ending with
138199 a "/", which will be used to look up the ModelProvider. If there is no prefix, we will use
@@ -144,12 +205,21 @@ def get_model(self, model_name: str | None) -> Model:
144205 Returns:
145206 A Model.
146207 """
147- prefix , model_name = self ._get_prefix_and_model_name (model_name )
208+ # Bare model names are always delegated directly to the OpenAI provider. That provider can
209+ # still point at an OpenAI-compatible endpoint via ``base_url``.
210+ if model_name is None :
211+ return self .openai_provider .get_model (None )
148212
149- if prefix and self .provider_map and (provider := self .provider_map .get_provider (prefix )):
150- return provider .get_model (model_name )
151- else :
152- return self ._get_fallback_provider (prefix ).get_model (model_name )
213+ prefix , stripped_model_name = self ._get_prefix_and_model_name (model_name )
214+ if prefix is None :
215+ return self .openai_provider .get_model (stripped_model_name )
216+
217+ provider , resolved_model_name = self ._resolve_prefixed_model (
218+ original_model_name = model_name ,
219+ prefix = prefix ,
220+ stripped_model_name = stripped_model_name ,
221+ )
222+ return provider .get_model (resolved_model_name )
153223
154224 async def aclose (self ) -> None :
155225 """Close cached resources held by child providers."""
0 commit comments