-
-
Notifications
You must be signed in to change notification settings - Fork 11.6k
feat(vertex_ai): Vertex AI Gemini Live via unified /realtime endpoint #22153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f6d0775
feat(vertex_ai): add Vertex AI Gemini Live support via unified /realt…
ishaan-jaff 7f017f7
docs: add vertex_realtime to sidebars
ishaan-jaff 836c9ef
fix: drop unknown event types in Gemini transform; add vertex_ai heal…
ishaan-jaff e534d54
fix: propagate UUID fallback IDs from transform_content_done_event to…
ishaan-jaff 5c55c91
fix: route guardrail backend sends through provider transform; fix st…
ishaan-jaff cc3c8e4
fix: handle Vertex AI full resource path in session.created; route gu…
ishaan-jaff 58109ac
fix: remove unused VertexBase in transformation.py; apply UUID fallba…
ishaan-jaff File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| # Vertex AI Gemini Live - Realtime API | ||
|
|
||
| Use Vertex AI's Gemini Live API (BidiGenerateContent) through LiteLLM's unified `/realtime` endpoint, which speaks the OpenAI Realtime protocol. | ||
|
|
||
| | Feature | Supported | | ||
| |---------|-----------| | ||
| | Proxy (`/realtime`) | ✅ | | ||
| | Voice in / Voice out | ✅ | | ||
| | Text in / Text out | ✅ | | ||
| | Server VAD | ✅ | | ||
| | Output transcription | ✅ | | ||
|
|
||
| ## Setup | ||
|
|
||
| ### 1. Auth | ||
|
|
||
| LiteLLM uses your Google Cloud credentials (OAuth2 Bearer token), not an API key. | ||
|
|
||
| ```bash | ||
| gcloud auth application-default login | ||
| ``` | ||
|
|
||
| Or set a service-account key file: | ||
|
|
||
| ```bash | ||
| export GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa-key.json | ||
| ``` | ||
|
|
||
| ### 2. Proxy config | ||
|
|
||
| ```yaml | ||
| model_list: | ||
| - model_name: vertex-gemini-live | ||
| litellm_params: | ||
| model: vertex_ai/gemini-2.0-flash-live-001 | ||
| vertex_project: your-gcp-project-id | ||
| vertex_location: us-east4 # or any supported region, or "global" | ||
|
|
||
| general_settings: | ||
| master_key: sk-your-key | ||
| ``` | ||
|
|
||
| ### 3. Start the proxy | ||
|
|
||
| ```bash | ||
| litellm --config config.yaml --port 4000 | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Python (websockets) | ||
|
|
||
| ```python | ||
| import asyncio | ||
| import json | ||
| import websockets | ||
|
|
||
| PROXY_URL = "ws://localhost:4000/realtime?model=vertex-gemini-live" | ||
| API_KEY = "sk-your-key" | ||
|
|
||
| async def main(): | ||
| async with websockets.connect( | ||
| PROXY_URL, | ||
| additional_headers={"api-key": API_KEY}, | ||
| ) as ws: | ||
| # Wait for session.created | ||
| event = json.loads(await ws.recv()) | ||
| print(f"session.created: {event['session']['id']}") | ||
|
|
||
| # Send a text message | ||
| await ws.send(json.dumps({ | ||
| "type": "conversation.item.create", | ||
| "item": { | ||
| "type": "message", | ||
| "role": "user", | ||
| "content": [{"type": "input_text", "text": "Say hello in one sentence."}], | ||
| }, | ||
| })) | ||
|
|
||
| # Collect the response | ||
| async for raw in ws: | ||
| ev = json.loads(raw) | ||
| t = ev.get("type", "") | ||
| if t == "response.text.delta": | ||
| print(ev.get("delta", ""), end="", flush=True) | ||
| elif t == "response.done": | ||
| print("\n[done]") | ||
| break | ||
|
|
||
| asyncio.run(main()) | ||
| ``` | ||
|
|
||
| ### Node.js | ||
|
|
||
| ```js | ||
| const WebSocket = require("ws"); | ||
|
|
||
| const ws = new WebSocket( | ||
| "ws://localhost:4000/realtime?model=vertex-gemini-live", | ||
| { headers: { "api-key": "sk-your-key" } } | ||
| ); | ||
|
|
||
| ws.on("open", () => { | ||
| ws.send(JSON.stringify({ | ||
| type: "conversation.item.create", | ||
| item: { | ||
| type: "message", | ||
| role: "user", | ||
| content: [{ type: "input_text", text: "Say hello." }], | ||
| }, | ||
| })); | ||
| }); | ||
|
|
||
| ws.on("message", (data) => { | ||
| const ev = JSON.parse(data); | ||
| if (ev.type === "response.text.delta") process.stdout.write(ev.delta); | ||
| if (ev.type === "response.done") ws.close(); | ||
| }); | ||
| ``` | ||
|
|
||
| ### OpenAI SDK (Python) | ||
|
|
||
| ```python | ||
| import asyncio | ||
| from openai import AsyncOpenAI | ||
|
|
||
| client = AsyncOpenAI( | ||
| base_url="http://localhost:4000", | ||
| api_key="sk-your-key", | ||
| ) | ||
|
|
||
| async def main(): | ||
| async with client.beta.realtime.connect( | ||
| model="vertex-gemini-live" | ||
| ) as conn: | ||
| await conn.session.update(session={"modalities": ["text"]}) | ||
|
|
||
| await conn.conversation.item.create( | ||
| item={ | ||
| "type": "message", | ||
| "role": "user", | ||
| "content": [{"type": "input_text", "text": "Say hello."}], | ||
| } | ||
| ) | ||
|
|
||
| async for event in conn: | ||
| if event.type == "response.text.delta": | ||
| print(event.delta, end="", flush=True) | ||
| elif event.type == "response.done": | ||
| print() | ||
| break | ||
|
|
||
| asyncio.run(main()) | ||
| ``` | ||
|
|
||
| ## Voice in / Voice out | ||
|
|
||
| For a complete voice example see [`voice_realtime_test.py`](https://github.com/BerriAI/litellm/blob/main/voice_realtime_test.py). | ||
|
|
||
| Key settings for audio: | ||
| - Microphone input: **16 kHz** PCM16 (`audio/pcm;rate=16000`) | ||
| - Speaker output: **24 kHz** PCM16 (Vertex AI returns audio at 24 kHz) | ||
| - Server VAD is enabled by default with 800 ms silence threshold | ||
|
|
||
| ```python | ||
| # session.update with server VAD — the proxy ignores this for Vertex AI | ||
| # because VAD is already configured in the initial setup message. | ||
| await ws.send(json.dumps({ | ||
| "type": "session.update", | ||
| "session": { | ||
| "modalities": ["audio"], | ||
| "turn_detection": {"type": "server_vad", "silence_duration_ms": 800}, | ||
| }, | ||
| })) | ||
| ``` | ||
|
|
||
| ## Supported OpenAI Realtime Events | ||
|
|
||
| **Client → Proxy (→ Vertex AI)** | ||
|
|
||
| | OpenAI event | Notes | | ||
| |---|---| | ||
| | `input_audio_buffer.append` | Forwarded as `realtime_input.audio` | | ||
| | `conversation.item.create` | Forwarded as `realtime_input.text` | | ||
| | `session.update` | Silently ignored — Vertex AI does not support mid-session reconfiguration | | ||
| | `response.create` | Silently ignored — Vertex AI responds automatically after each turn | | ||
|
|
||
| **Vertex AI → Proxy (→ Client)** | ||
|
|
||
| | OpenAI event emitted | Vertex AI source | | ||
| |---|---| | ||
| | `session.created` | Synthesized after `setupComplete` | | ||
| | `response.text.delta` | `serverContent.modelTurn.parts[].text` | | ||
| | `response.audio.delta` | `serverContent.modelTurn.parts[].inlineData` | | ||
| | `response.audio_transcript.delta` | `serverContent.outputTranscription.text` | | ||
| | `conversation.item.input_audio_transcription.completed` | `serverContent.inputTranscription.text` | | ||
| | `response.done` | `serverContent.turnComplete` | | ||
|
|
||
| ## Limitations | ||
|
|
||
| - `session.update` is not forwarded (Vertex AI only accepts one setup message per connection). | ||
| - Tool calling / function calling is not yet supported. | ||
| - Audio transcription requires `outputAudioTranscription: {}` to be set in the initial setup (done automatically by LiteLLM). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
Exceptiontypes means that serious errors (e.g. auth failures, protocol violations, or bugs in transformation code) will also be silently swallowed, with the loopcontinue-ing past them. Consider narrowing this to catch only expected transformation errors (likeValueError,KeyError,json.JSONDecodeError) so that unexpected failures still propagate and are visible.