feat(vertex_ai): Vertex AI Gemini Live via unified /realtime endpoint - #22153
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ime endpoint Adds VertexAIRealtimeConfig which translates the OpenAI Realtime WebSocket protocol to Vertex AI BidiGenerateContent. Supports voice in/voice out (16 kHz mic → 24 kHz speaker) and text in/text out through the proxy's /realtime endpoint. Key changes: - New litellm/llms/vertex_ai/realtime/transformation.py with VertexAIRealtimeConfig - Builds correct wss:// URL (regional + global) - OAuth2 Bearer token auth (not API key) - Full model path (projects/.../publishers/google/models/...) - Ignores session.update (Vertex AI only accepts one setup message) - realtime_api/main.py: vertex_ai branch resolves OAuth token + constructs config - llm_http_handler.py: auto-sends session setup before bidirectional_forward - gemini/realtime/transformation.py: fix crashes on empty turnComplete events - realtime_streaming.py: try/except guard so bad messages don't kill the loop - proxy_server.py: add missing websockets.exceptions import
2f9096d to
f6d0775
Compare
Greptile SummaryAdds Vertex AI Gemini Live (
Confidence Score: 3/5
|
| Filename | Overview |
|---|---|
| litellm/llms/vertex_ai/realtime/transformation.py | New VertexAIRealtimeConfig extending GeminiRealtimeConfig. Well-structured with proper URL construction, OAuth2 auth, and session setup. Minor: double JSON parse on non-session.update messages, inline imports in session_configuration_request. |
| litellm/litellm_core_utils/realtime_streaming.py | New _send_to_backend method routes guardrail messages through provider transform, but this causes guardrail warning messages and create_response=false injection to be silently dropped for Vertex AI, making realtime guardrails ineffective. |
| litellm/llms/gemini/realtime/transformation.py | Good fixes: UUID fallbacks for None IDs prevent crashes on early turnComplete; proper model path normalization for Vertex AI; response.create and unknown events now handled gracefully instead of forwarding raw JSON as text. |
| litellm/realtime_api/main.py | Vertex AI branch properly resolves OAuth token, project, and location before creating config. Health check branch added. Follows established patterns from other providers (azure, bedrock, xai). |
| litellm/llms/custom_httpx/llm_http_handler.py | Clean addition: auto-sends session setup message for providers that require it, then stores it on the RealTimeStreaming instance. No issues found. |
| tests/test_litellm/llms/vertex_ai/realtime/test_vertex_ai_realtime_transformation.py | 6 well-structured unit tests covering URL construction, auth headers, session config format, and full text-in/text-out round trip with mocked WebSockets. All mock-based, no real network calls. |
Sequence Diagram
sequenceDiagram
participant Client as Client
participant Proxy as LiteLLM Proxy
participant Transform as VertexAIRealtimeConfig
participant VertexAI as Vertex AI
Client->>Proxy: WebSocket /realtime
Proxy->>Transform: get_complete_url + validate_environment
Proxy->>VertexAI: WSS connect
Transform->>VertexAI: setup message with model path and VAD
VertexAI-->>Transform: setupComplete
Transform-->>Client: session.created
Client->>Transform: conversation.item.create
Transform->>VertexAI: realtime_input.text
VertexAI-->>Transform: serverContent.modelTurn text
Transform-->>Client: response.text.delta
VertexAI-->>Transform: serverContent.generationComplete
Transform-->>Client: response.text.done
VertexAI-->>Transform: serverContent.turnComplete
Transform-->>Client: response.done
Client->>Transform: input_audio_buffer.append
Transform->>VertexAI: realtime_input.audio PCM16
VertexAI-->>Transform: serverContent.modelTurn inlineData
Transform-->>Client: response.audio.delta
Client->>Transform: session.update
Note right of Transform: Silently dropped
Last reviewed commit: 58109ac
| from litellm.llms.gemini.realtime.transformation import GeminiRealtimeConfig | ||
| from litellm.llms.vertex_ai.vertex_llm_base import VertexBase | ||
|
|
||
| vertex_llm_base = VertexBase() |
There was a problem hiding this comment.
Unused import and module-level instantiation
VertexBase is imported and instantiated as vertex_llm_base at module level but is never referenced anywhere in this file. The actual VertexBase usage happens in realtime_api/main.py which has its own instance. This creates an unnecessary object at import time.
| from litellm.llms.gemini.realtime.transformation import GeminiRealtimeConfig | |
| from litellm.llms.vertex_ai.vertex_llm_base import VertexBase | |
| vertex_llm_base = VertexBase() | |
| from litellm.llms.gemini.realtime.transformation import GeminiRealtimeConfig |
| from litellm.types.llms.gemini import BidiGenerateContentSetup | ||
| from litellm.types.llms.vertex_ai import GeminiResponseModalities |
There was a problem hiding this comment.
Inline imports in method body
Per the CLAUDE.md style guide: "Avoid imports within methods — place all imports at the top of the file (module-level)." The imports of BidiGenerateContentSetup and GeminiResponseModalities should be moved to the top of the file.
# At the top of the file, add:
from litellm.types.llms.gemini import BidiGenerateContentSetup
from litellm.types.llms.vertex_ai import GeminiResponseModalities
Context Used: Context from dashboard - CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| else: | ||
| # Pass through any other message types as raw text | ||
| realtime_input_dict["text"] = message |
There was a problem hiding this comment.
Unrecognized message types sent as raw text to Gemini
The else branch sends the entire raw JSON string (e.g. {"type":"input_audio_buffer.commit","event_id":"..."}) as a text input to the model. For OpenAI Realtime events that have no Gemini equivalent (like input_audio_buffer.commit, input_audio_buffer.clear), this will cause the model to receive the raw JSON as a user text message, which is likely not the intended behavior.
Consider either explicitly handling known no-op events (returning []) or at minimum logging a warning, similar to how response.create and session.update are handled:
| else: | |
| # Pass through any other message types as raw text | |
| realtime_input_dict["text"] = message | |
| else: | |
| # Drop unrecognized OpenAI event types that have no Gemini equivalent | |
| return [] |
| try: | ||
| await self._handle_provider_config_message(raw_response) | ||
| except Exception as e: | ||
| verbose_logger.exception( | ||
| f"Error processing backend message, skipping: {e}" | ||
| ) | ||
| continue |
There was a problem hiding this comment.
Broad exception catch silently swallows all backend errors
While adding a try/except here prevents a single malformed message from killing the session (good), catching all Exception types means that serious errors (e.g. auth failures, protocol violations, or bugs in transformation code) will also be silently swallowed, with the loop continue-ing past them. Consider narrowing this to catch only expected transformation errors (like ValueError, KeyError, json.JSONDecodeError) so that unexpected failures still propagate and are visible.
| elif _custom_llm_provider == "vertex_ai": | ||
| vertex_credentials = ( | ||
| kwargs.get("vertex_credentials") | ||
| or kwargs.get("vertex_ai_credentials") | ||
| or get_secret_str("VERTEXAI_CREDENTIALS") | ||
| ) | ||
| vertex_project = ( | ||
| kwargs.get("vertex_project") | ||
| or kwargs.get("vertex_ai_project") | ||
| or litellm.vertex_project | ||
| or get_secret_str("VERTEXAI_PROJECT") | ||
| ) | ||
| vertex_location = ( | ||
| kwargs.get("vertex_location") | ||
| or kwargs.get("vertex_ai_location") | ||
| or litellm.vertex_location | ||
| or get_secret_str("VERTEXAI_LOCATION") | ||
| ) | ||
|
|
||
| resolved_location = vertex_llm_base.get_vertex_region( | ||
| vertex_region=vertex_location, model=model | ||
| ) | ||
|
|
||
| access_token, resolved_project = await vertex_llm_base._ensure_access_token_async( | ||
| credentials=vertex_credentials, | ||
| project_id=vertex_project, | ||
| custom_llm_provider="vertex_ai", | ||
| ) | ||
|
|
||
| vertex_realtime_config = VertexAIRealtimeConfig( | ||
| access_token=access_token, | ||
| project=resolved_project, | ||
| location=resolved_location, | ||
| ) | ||
|
|
||
| await base_llm_http_handler.async_realtime( | ||
| model=model, | ||
| websocket=websocket, | ||
| logging_obj=litellm_logging_obj, | ||
| provider_config=vertex_realtime_config, | ||
| api_base=dynamic_api_base or litellm_params.api_base, | ||
| api_key=None, | ||
| client=client, | ||
| timeout=timeout, | ||
| headers=headers, | ||
| ) |
There was a problem hiding this comment.
_realtime_health_check not updated for vertex_ai
The _realtime_health_check function (line 271) does not have a vertex_ai branch and will raise ValueError("Unsupported model: ...") if called for a Vertex AI realtime model. If health checks are run for deployed realtime models, this will fail at runtime. Consider adding a vertex_ai case or at least a graceful skip.
|
@greptile-apps review |
Additional Comments (1)
When The UUIDs generated inside
|
… return_additional_content_done_events
|
@greptile-apps review |
Additional Comments (2)
When guardrails are active, this block sends an OpenAI-format For Vertex AI specifically, sending a raw Consider routing these guardrail messages through
Consider using |
…r.strip misuse for model prefix
|
@greptile-apps review |
…ardrail block sends through _send_to_backend
|
@greptile-apps review |
Additional Comments (2)
For Vertex AI, Consider also stripping the Vertex AI resource prefix, e.g.:
Consider using |
…ck in return_additional_content_done_events
|
@greptile-apps review |
Additional Comments (2)
Consider guarding the parse or checking the type after the parent has parsed:
Consider adding a guard: if not access_token:
raise ValueError(
"Could not resolve Vertex AI access token. "
"Set GOOGLE_APPLICATION_CREDENTIALS or run inside a GCP environment."
) |
Additional Comments (3)
When a guardrail blocks content, The guardrail correctly returns
When realtime guardrails are registered, this code sends Since Vertex AI has server-side VAD enabled and auto-responds after each turn, guardrails cannot actually prevent the LLM from responding before the guardrail check completes. This makes realtime guardrails ineffective for Vertex AI sessions. Consider documenting this limitation or implementing an alternative mechanism (e.g., client-side buffering).
Consider passing the already-parsed |
…#22153) * feat(vertex_ai): add Vertex AI Gemini Live support via unified /realtime endpoint Adds VertexAIRealtimeConfig which translates the OpenAI Realtime WebSocket protocol to Vertex AI BidiGenerateContent. Supports voice in/voice out (16 kHz mic → 24 kHz speaker) and text in/text out through the proxy's /realtime endpoint. Key changes: - New litellm/llms/vertex_ai/realtime/transformation.py with VertexAIRealtimeConfig - Builds correct wss:// URL (regional + global) - OAuth2 Bearer token auth (not API key) - Full model path (projects/.../publishers/google/models/...) - Ignores session.update (Vertex AI only accepts one setup message) - realtime_api/main.py: vertex_ai branch resolves OAuth token + constructs config - llm_http_handler.py: auto-sends session setup before bidirectional_forward - gemini/realtime/transformation.py: fix crashes on empty turnComplete events - realtime_streaming.py: try/except guard so bad messages don't kill the loop - proxy_server.py: add missing websockets.exceptions import * docs: add vertex_realtime to sidebars * fix: drop unknown event types in Gemini transform; add vertex_ai health check * fix: propagate UUID fallback IDs from transform_content_done_event to return_additional_content_done_events * fix: route guardrail backend sends through provider transform; fix str.strip misuse for model prefix * fix: handle Vertex AI full resource path in session.created; route guardrail block sends through _send_to_backend * fix: remove unused VertexBase in transformation.py; apply UUID fallback in return_additional_content_done_events
…BerriAI#22153) * feat(vertex_ai): add Vertex AI Gemini Live support via unified /realtime endpoint Adds VertexAIRealtimeConfig which translates the OpenAI Realtime WebSocket protocol to Vertex AI BidiGenerateContent. Supports voice in/voice out (16 kHz mic → 24 kHz speaker) and text in/text out through the proxy's /realtime endpoint. Key changes: - New litellm/llms/vertex_ai/realtime/transformation.py with VertexAIRealtimeConfig - Builds correct wss:// URL (regional + global) - OAuth2 Bearer token auth (not API key) - Full model path (projects/.../publishers/google/models/...) - Ignores session.update (Vertex AI only accepts one setup message) - realtime_api/main.py: vertex_ai branch resolves OAuth token + constructs config - llm_http_handler.py: auto-sends session setup before bidirectional_forward - gemini/realtime/transformation.py: fix crashes on empty turnComplete events - realtime_streaming.py: try/except guard so bad messages don't kill the loop - proxy_server.py: add missing websockets.exceptions import * docs: add vertex_realtime to sidebars * fix: drop unknown event types in Gemini transform; add vertex_ai health check * fix: propagate UUID fallback IDs from transform_content_done_event to return_additional_content_done_events * fix: route guardrail backend sends through provider transform; fix str.strip misuse for model prefix * fix: handle Vertex AI full resource path in session.created; route guardrail block sends through _send_to_backend * fix: remove unused VertexBase in transformation.py; apply UUID fallback in return_additional_content_done_events
Relevant issues
Closes: N/A
What this does
Adds Vertex AI Gemini Live (
gemini-2.0-flash-live-001) support through LiteLLM's unified/realtimeWebSocket endpoint. Clients use the standard OpenAI Realtime protocol — the proxy handles translation to/from Vertex AI's BidiGenerateContent format.Tested end-to-end: text in/text out + voice in/voice out (mic → 16 kHz PCM16 → Vertex AI → 24 kHz PCM16 → speaker).
Changes
New:
litellm/llms/vertex_ai/realtime/transformation.py—VertexAIRealtimeConfigwss://{location}-aiplatform.googleapis.com/.../BidiGenerateContentURLprojects/{project}/locations/{location}/publishers/google/models/{model}session.update— Vertex AI only accepts onesetupmessage per connection; sending a second causes a 1007 disconnectaudio/pcm;rate=16000realtime_api/main.py— addsvertex_aibranch that resolves the OAuth token viaVertexBase._ensure_access_token_asyncand constructsVertexAIRealtimeConfigllm_http_handler.py— auto-sends the provider's session setup message beforebidirectional_forward()whenrequires_session_configuration()returns True (needed for Gemini/Vertex AI Live)gemini/realtime/transformation.py— fixes two crashes:transform_response_done_eventraisedValueErrorwhenturnCompletearrived before any audio (IDs were None); now generates UUIDs insteadtransform_content_done_eventresponse.create(Vertex AI responds automatically)conversation.item.createto extract actual user textrealtime_streaming.py— try/except guard in backend loop so a single malformed message doesn't kill the whole session; also fix pre-existing Pyright type annotation errors on_collect_tool_calls_from_response_done/_collect_user_input_from_backend_eventproxy_server.py— add missingimport websockets.exceptions(causedNameErroron every WebSocket close)provider_endpoints_support.json— markvertex_aiwith"realtime": truedocs/my-website/docs/providers/vertex_realtime.md— usage docs with Python, Node.js, and OpenAI SDK examplesPre-Submission checklist
tests/test_litellm/llms/vertex_ai/realtime/test_vertex_ai_realtime_transformation.py) — 6 unit tests covering URL construction, auth headers, session config format, and full text-in/text-out round trip with mocked WebSocketsmake test-unitpassesType
Changes
litellm/llms/vertex_ai/realtime/transformation.pylitellm/llms/vertex_ai/realtime/__init__.pylitellm/realtime_api/main.pylitellm/llms/custom_httpx/llm_http_handler.pylitellm/llms/gemini/realtime/transformation.pylitellm/litellm_core_utils/realtime_streaming.pylitellm/proxy/proxy_server.pyprovider_endpoints_support.jsondocs/my-website/docs/providers/vertex_realtime.md