Skip to content
Merged
4 changes: 1 addition & 3 deletions litellm/llms/custom_httpx/llm_http_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5537,9 +5537,7 @@ async def async_realtime(
import websockets
from websockets.asyncio.client import ClientConnection

url = self._append_query_params(
provider_config.get_complete_url(api_base, model, api_key), query_params
)
url = provider_config.get_complete_url(api_base, model, api_key)
headers = provider_config.validate_environment(
headers=headers,
model=model,
Expand Down
11 changes: 7 additions & 4 deletions litellm/llms/gemini/realtime/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ def __init__(self):
# bypassing spend and budget accounting.
self._pending_usage_metadata: Optional[dict] = None

def _include_function_response_id(self) -> bool:
"""Google AI Studio Gemini 3.5+ accepts ``id`` on functionResponses; Vertex AI rejects it."""
return True

@staticmethod
def _usage_detail_alias(details: Any, defaults: Dict[str, int]) -> Dict[str, Any]:
if not isinstance(details, dict):
Expand Down Expand Up @@ -604,10 +608,9 @@ def _handle_function_call_output(self, item: dict) -> List[str]:
)

# Build Gemini toolResponse format
function_response = {
"id": call_id,
"response": output_dict,
}
function_response: dict[str, Any] = {"response": output_dict}
if self._include_function_response_id() and call_id:
function_response["id"] = call_id
if function_name:
function_response["name"] = function_name

Expand Down
3 changes: 3 additions & 0 deletions litellm/llms/vertex_ai/realtime/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def __init__(self, access_token: str, project: str, location: str) -> None:
self._project = project
self._location = location

def _include_function_response_id(self) -> bool:
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of weird to me to have a function return a bool and to override that in the child class. I would've thought a class attribute was better. But I read online that this is an accepted pattern so it's fine lgtm


# ------------------------------------------------------------------
# URL
# ------------------------------------------------------------------
Expand Down
14 changes: 8 additions & 6 deletions litellm/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5044,9 +5044,7 @@ def completion( # type: ignore
if LiteLLM_Proxy_MCP_Handler._should_use_litellm_mcp_gateway(
tools=tools_for_mcp
):
# Return coroutine - acompletion will await it
# completion() can return a coroutine when MCP tools are present, which acompletion() awaits
return acompletion_with_mcp( # type: ignore[return-value]
return acompletion_with_mcp( # pyright: ignore[reportReturnType] # MCP path returns a coroutine that acompletion() awaits; completion()'s sync return type omits it
model=model,
messages=messages,
functions=functions,
Expand Down Expand Up @@ -5218,12 +5216,16 @@ def completion( # type: ignore
logging: LiteLLMLoggingObj = cast(LiteLLMLoggingObj, litellm_logging_obj)
fallbacks = fallbacks or litellm.model_fallbacks
if fallbacks is not None:
return completion_with_fallbacks(**args)
return completion_with_fallbacks( # pyright: ignore[reportReturnType] # fallback runner is untyped; resolves to ModelResponse|CustomStreamWrapper at runtime
**args
)
if model_list is not None:
deployments = [
m["litellm_params"] for m in model_list if m["model_name"] == model
]
return litellm.batch_completion_models(deployments=deployments, **args)
return litellm.batch_completion_models( # pyright: ignore[reportReturnType] # batch path returns a list of responses, outside completion()'s single-response return type
deployments=deployments, **args
)
if litellm.model_alias_map and model in litellm.model_alias_map:
model = litellm.model_alias_map[
model
Expand Down Expand Up @@ -5545,7 +5547,7 @@ def completion( # type: ignore
else:
optional_params["reasoning_effort"] = {"summary": rs_val}

return responses_api_bridge.completion(
return responses_api_bridge.completion( # pyright: ignore[reportReturnType] # bridge returns a coroutine on the acompletion path; awaited by the async caller
model=model,
messages=messages,
headers=headers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,74 @@ def test_vertex_does_not_warn_when_dropping_non_guardrail_session_update(caplog)
"Vertex AI Realtime" in record.message and "session.update" in record.message
for record in caplog.records
)


async def test_async_realtime_does_not_forward_client_query_params_to_vertex_backend(
monkeypatch,
):
"""Regression: forwarding client ?model=/?intent= to the Vertex Live WSS URL causes 1007 errors.

Exercises ``async_realtime`` end-to-end so that re-adding ``_append_query_params``
(the reverted bug) would push ``model=``/``intent=`` onto the backend URL and fail here.
"""
import websockets

from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler

cfg = VertexAIRealtimeConfig(
access_token="tok", project="my-proj", location="us-central1"
)

captured = {}

def fake_connect(url, *args, **kwargs):
captured["url"] = url
raise RuntimeError("stop before establishing the backend connection")

monkeypatch.setattr(websockets, "connect", fake_connect)

await BaseLLMHTTPHandler().async_realtime(
model="gemini-live-2.5-flash-native-audio",
websocket=AsyncMock(),
logging_obj=MagicMock(),
provider_config=cfg,
headers={},
query_params={
"model": "gemini-live-2.5-flash-native-audio",
"intent": "chat",
},
)

assert "?" not in captured["url"]
assert "model=" not in captured["url"]
assert "intent=" not in captured["url"]


def test_vertex_function_call_output_omits_id():
"""Regression: Vertex Live rejects ``id`` on toolResponse.functionResponses (1007)."""
cfg = VertexAIRealtimeConfig(
access_token="tok", project="my-proj", location="us-central1"
)
cfg._tool_call_id_to_name["call_abc123"] = "terminate_call"

messages = cfg.transform_realtime_request(
json.dumps(
{
"type": "conversation.item.create",
"item": {
"type": "function_call_output",
"call_id": "call_abc123",
"output": '{"status": "ok"}',
},
}
),
"gemini-live-2.5-flash-native-audio",
session_configuration_request="existing",
)

assert len(messages) == 1
payload = json.loads(messages[0])
function_response = payload["toolResponse"]["functionResponses"][0]
assert "id" not in function_response
assert function_response["name"] == "terminate_call"
assert function_response["response"] == {"status": "ok"}
Loading