mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(router): keep a model's own provider prefix for generic SDK calls
Generic passthrough calls inferred the provider from the bare model name, so an azure_ai/gpt-* deployment on an Azure OpenAI host flipped to azure and get_llm_provider re-prefixed the deployment name into azure_ai/gpt-5.4-mini, a 404 DeploymentNotFound. provider_for_generic_call takes the declared custom_llm_provider first, then the model's own prefix, and only infers for unprefixed models
This commit is contained in:
parent
cf275cf442
commit
a276690ce2
4 changed files with 91 additions and 21 deletions
|
|
@ -135,6 +135,7 @@ from litellm.router_utils.common_utils import (
|
|||
_is_proxy_admin_request,
|
||||
filter_team_based_models,
|
||||
filter_web_search_deployments,
|
||||
provider_for_generic_call,
|
||||
resolve_model_group_alias,
|
||||
truncate_fallback_error_detail,
|
||||
warn_on_provider_credential_mismatch,
|
||||
|
|
@ -5045,17 +5046,7 @@ class Router:
|
|||
kwargs=kwargs, model=model, model_name=model_name
|
||||
)
|
||||
|
||||
# Get custom_llm_provider from deployment params
|
||||
try:
|
||||
custom_llm_provider = data.get("custom_llm_provider")
|
||||
_, inferred_custom_llm_provider, _, _ = get_llm_provider(
|
||||
model=data["model"],
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
api_base=data.get("api_base"),
|
||||
)
|
||||
custom_llm_provider = custom_llm_provider or inferred_custom_llm_provider
|
||||
except Exception:
|
||||
custom_llm_provider = None
|
||||
custom_llm_provider: Final = provider_for_generic_call(data)
|
||||
|
||||
response_kwargs: Final = {
|
||||
**data,
|
||||
|
|
@ -5566,16 +5557,7 @@ class Router:
|
|||
# Perform pre-call checks for routing strategy
|
||||
self.routing_strategy_pre_call_checks(deployment=deployment)
|
||||
|
||||
try:
|
||||
custom_llm_provider = data.get("custom_llm_provider")
|
||||
_, inferred_custom_llm_provider, _, _ = get_llm_provider(
|
||||
model=data["model"],
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
api_base=data.get("api_base"),
|
||||
)
|
||||
custom_llm_provider = custom_llm_provider or inferred_custom_llm_provider
|
||||
except Exception:
|
||||
custom_llm_provider = None
|
||||
custom_llm_provider: Final = provider_for_generic_call(data)
|
||||
|
||||
response: Final = original_function(
|
||||
**{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Final
|
|||
if TYPE_CHECKING:
|
||||
from litellm.types.llms.openai import OpenAIFileObject
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger, verbose_router_logger
|
||||
from litellm.constants import ROUTER_FALLBACK_ERROR_DETAIL_MAX_CHARS
|
||||
from litellm.exceptions import BadRequestError
|
||||
|
|
@ -244,6 +245,32 @@ PROVIDER_SCOPED_CREDENTIAL_PARAMS: Final[Mapping[str, frozenset[str]]] = Mapping
|
|||
)
|
||||
|
||||
|
||||
def provider_for_generic_call(litellm_params: Mapping[str, object]) -> str | None:
|
||||
"""
|
||||
The provider the router hands a deployment's generic SDK call, or None when it cannot be resolved.
|
||||
|
||||
A model that carries its own provider prefix keeps that prefix even where get_llm_provider
|
||||
would resolve it to a sibling provider (azure_ai/<openai model> on an Azure OpenAI host
|
||||
resolves to azure): the SDK call still receives the prefixed model, and an explicit provider
|
||||
that contradicts the prefix makes get_llm_provider re-prefix it into a deployment name that
|
||||
does not exist upstream.
|
||||
"""
|
||||
declared: Final = litellm_params.get("custom_llm_provider")
|
||||
if isinstance(declared, str) and declared:
|
||||
return declared
|
||||
model: Final = litellm_params.get("model")
|
||||
if not isinstance(model, str) or not model:
|
||||
return None
|
||||
prefix: Final = model.split("/", 1)[0]
|
||||
if "/" in model and prefix in litellm.provider_list:
|
||||
return prefix
|
||||
try:
|
||||
_, inferred, _, _ = get_llm_provider(model=model)
|
||||
except BadRequestError:
|
||||
return None
|
||||
return inferred
|
||||
|
||||
|
||||
def warn_on_provider_credential_mismatch(model_name: str, litellm_params: Mapping[str, object]) -> str | None:
|
||||
"""
|
||||
Warn when a deployment carries one provider's credentials but resolves to another.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from litellm.router_utils.common_utils import (
|
|||
add_model_file_id_mappings,
|
||||
filter_team_based_models,
|
||||
filter_web_search_deployments,
|
||||
provider_for_generic_call,
|
||||
resolve_model_group_alias,
|
||||
truncate_fallback_error_detail,
|
||||
PROVIDER_SCOPED_CREDENTIAL_PARAMS,
|
||||
|
|
@ -756,3 +757,20 @@ class TestWarnOnProviderCredentialMismatch:
|
|||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("litellm_params", "expected"),
|
||||
[
|
||||
({"model": "azure_ai/gpt-5.4-mini", "custom_llm_provider": "azure"}, "azure"),
|
||||
({"model": "azure_ai/gpt-5.4-mini", "api_base": "https://my-resource.openai.azure.com"}, "azure_ai"),
|
||||
({"model": "cohere/command-r"}, "cohere"),
|
||||
({"model": "gpt-5.4-mini"}, "openai"),
|
||||
({"model": "no-provider-knows-this-model"}, None),
|
||||
({"api_base": "https://my-resource.openai.azure.com"}, None),
|
||||
],
|
||||
ids=["declared_wins", "prefix_beats_host_flip", "prefix_beats_cohere_chat_flip", "unprefixed_inferred", "unknown", "no_model"],
|
||||
)
|
||||
def test_provider_for_generic_call(litellm_params, expected, monkeypatch):
|
||||
monkeypatch.setenv("AZURE_AI_API_BASE", "https://unrelated.openai.azure.com")
|
||||
assert provider_for_generic_call(litellm_params) == expected
|
||||
|
|
|
|||
|
|
@ -12938,3 +12938,46 @@ async def test_router_retry_policy_controls_upstream_attempt_count(
|
|||
await router.acompletion(model="gpt-5.6", messages=[{"role": "user", "content": "hi"}])
|
||||
|
||||
assert upstream.call_count == expected_upstream_calls
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generic_call_keeps_the_deployment_name_of_an_azure_ai_model_on_an_azure_openai_host(monkeypatch):
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "aoai-gpt",
|
||||
"litellm_params": {
|
||||
"model": "azure_ai/gpt-5.4-mini",
|
||||
"api_base": "https://my-resource.openai.azure.com",
|
||||
"api_key": "deployment-key",
|
||||
},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
with respx.mock(assert_all_called=True) as respx_mock:
|
||||
upstream = respx_mock.post(host="my-resource.openai.azure.com", path__regex=r"^/openai/.*responses$").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "resp_1",
|
||||
"object": "response",
|
||||
"created_at": 1,
|
||||
"status": "completed",
|
||||
"model": "gpt-5.4-mini",
|
||||
"output": [
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_1",
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"content": [{"type": "output_text", "text": "hi", "annotations": []}],
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
await router.aresponses(model="aoai-gpt", input="hi")
|
||||
|
||||
assert json.loads(upstream.calls.last.request.content)["model"] == "gpt-5.4-mini"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue