mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
fix(router): resolve realtime session model to routed deployment
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
5f2986a1f3
commit
00a2059174
2 changed files with 98 additions and 0 deletions
|
|
@ -29,6 +29,7 @@ import anyio
|
|||
import httpx
|
||||
import openai
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
from typing_extensions import overload
|
||||
|
||||
import litellm
|
||||
|
|
@ -342,6 +343,26 @@ def _stream_chunks_have_generated_content(chunks: Sequence[ModelResponseStream])
|
|||
return False
|
||||
|
||||
|
||||
_NO_SESSION_KWARGS: Final[Mapping[str, Mapping[str, object]]] = MappingProxyType({})
|
||||
_SESSION_ADAPTER: Final = TypeAdapter(Mapping[str, object])
|
||||
|
||||
|
||||
def _with_router_resolved_session_model(session: object, model_name: str) -> Mapping[str, Mapping[str, object]]:
|
||||
"""
|
||||
Realtime client-secret requests carry the model inside ``session`` as well, and the caller's copy of it still
|
||||
holds the pre-routing model group name, so it has to follow the deployment the router just picked.
|
||||
|
||||
Returns kwargs to merge into the downstream call, empty when there is no session model to resolve.
|
||||
"""
|
||||
try:
|
||||
typed_session: Final = _SESSION_ADAPTER.validate_python(session)
|
||||
except ValidationError:
|
||||
return _NO_SESSION_KWARGS
|
||||
if "model" not in typed_session:
|
||||
return _NO_SESSION_KWARGS
|
||||
return MappingProxyType({"session": {**typed_session, "model": model_name}})
|
||||
|
||||
|
||||
class RoutingArgs(enum.Enum):
|
||||
ttl = 60 # 1min (RPM/TPM expire key)
|
||||
|
||||
|
|
@ -4685,6 +4706,7 @@ class Router:
|
|||
"caching": self.cache_responses,
|
||||
**kwargs,
|
||||
"model": model_name,
|
||||
**_with_router_resolved_session_model(kwargs.get("session"), model_name),
|
||||
}
|
||||
# Only set custom_llm_provider if it's not None
|
||||
if custom_llm_provider is not None:
|
||||
|
|
|
|||
|
|
@ -1298,6 +1298,82 @@ async def test_ageneric_api_call_deployment_model_overrides_alias():
|
|||
), f"Expected deployment model 'vertex_ai/gemini-2.5-flash', got '{captured['model']}'"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ageneric_api_call_resolves_realtime_session_model():
|
||||
"""
|
||||
Regression for #36742: realtime client secret requests carry the model inside `session` too, and the proxy
|
||||
fills it with the pre-routing model group name. The underlying litellm function reads session.model first,
|
||||
so it must see the resolved deployment, while a caller's nested transcription model stays untouched.
|
||||
"""
|
||||
captured: dict = {}
|
||||
|
||||
async def capture_kwargs(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"result": "ok"}
|
||||
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "my-realtime-group",
|
||||
"litellm_params": {
|
||||
"model": "openai/gpt-realtime-2.1-mini",
|
||||
"api_key": "fake-key",
|
||||
},
|
||||
"model_info": {"mode": "realtime"},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
await router._ageneric_api_call_with_fallbacks(
|
||||
model="my-realtime-group",
|
||||
original_function=capture_kwargs,
|
||||
session={
|
||||
"type": "realtime",
|
||||
"model": "my-realtime-group",
|
||||
"audio": {"input": {"transcription": {"model": "gpt-4o-transcribe"}}},
|
||||
},
|
||||
)
|
||||
|
||||
assert captured["model"] == "openai/gpt-realtime-2.1-mini"
|
||||
assert captured["session"]["model"] == "openai/gpt-realtime-2.1-mini"
|
||||
assert captured["session"]["audio"]["input"]["transcription"]["model"] == "gpt-4o-transcribe"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ageneric_api_call_does_not_add_session_model():
|
||||
"""
|
||||
A session that never carried a model must not gain one from routing: the underlying function then falls back
|
||||
to the resolved `model` kwarg itself, and the outgoing session body keeps the caller's shape.
|
||||
"""
|
||||
captured: dict = {}
|
||||
|
||||
async def capture_kwargs(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"result": "ok"}
|
||||
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "my-realtime-group",
|
||||
"litellm_params": {
|
||||
"model": "openai/gpt-realtime-2.1-mini",
|
||||
"api_key": "fake-key",
|
||||
},
|
||||
"model_info": {"mode": "realtime"},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
await router._ageneric_api_call_with_fallbacks(
|
||||
model="my-realtime-group",
|
||||
original_function=capture_kwargs,
|
||||
session={"type": "realtime"},
|
||||
)
|
||||
|
||||
assert captured["model"] == "openai/gpt-realtime-2.1-mini"
|
||||
assert captured["session"] == {"type": "realtime"}
|
||||
|
||||
|
||||
def test_router_get_model_access_groups_team_only_models():
|
||||
"""
|
||||
Test that Router.get_model_access_groups returns the correct response for team-only models
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue