Skip to content

Commit 463040f

Browse files
allen-stephencopybara-github
authored andcommitted
feat(live): support Live API translation config in RunConfig
Merge #6083 ### Link to Issue or Description of Change Add support for live translation with the Live API -> https://ai.google.dev/gemini-api/docs/live-api/live-translate ### Testing Plan **Unit Tests:** - [X] I have added or updated unit tests for my change. - [X] All unit tests pass locally. ### Checklist - [X] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document. - [X] I have performed a self-review of my own code. - [X] I have commented my code, particularly in hard-to-understand areas. - [X] I have added tests that prove my fix is effective or that my feature works. - [X] New and existing unit tests pass locally with my changes. - [X] I have manually tested my changes end-to-end. - [X] Any dependent changes have been merged and published in downstream modules. COPYBARA_INTEGRATE_REVIEW=#6083 from allen-stephen:feat/enable-live-translate 6a0f5b7 PiperOrigin-RevId: 930782786
1 parent d88192c commit 463040f

8 files changed

Lines changed: 108 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ dependencies = [
3838
"click>=8.1.8,<9",
3939
"fastapi>=0.133,<1",
4040
"google-auth[pyopenssl]>=2.47",
41-
"google-genai>=2.4,<3",
41+
"google-genai>=2.8,<3",
4242
"graphviz>=0.20.2,<1",
4343
"httpx>=0.27,<1",
4444
"jsonschema>=4.23,<5",

src/google/adk/agents/run_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ class RunConfig(BaseModel):
239239
realtime_input_config: Optional[types.RealtimeInputConfig] = None
240240
"""Realtime input config for live agents with audio input from user."""
241241

242+
translation_config: Optional[types.TranslationConfig] = None
243+
"""Configures real-time speech-to-speech translation.
244+
245+
Only supported by translation models such as
246+
`gemini-3.5-live-translate-preview`.
247+
"""
248+
242249
enable_affective_dialog: Optional[bool] = None
243250
"""If enabled, the model will detect emotions and adapt its responses accordingly."""
244251

src/google/adk/flows/llm_flows/basic.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ def _build_basic_request(
8383
llm_request.live_connect_config.realtime_input_config = (
8484
invocation_context.run_config.realtime_input_config
8585
)
86+
llm_request.live_connect_config.translation_config = (
87+
invocation_context.run_config.translation_config
88+
)
8689
active_model_name = (
8790
getattr(getattr(agent, 'canonical_live_model', None), 'model', None)
8891
or llm_request.model

src/google/adk/models/gemini_llm_connection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ def __init__(
5353
self._is_gemini_3_1_flash_live = model_name_utils.is_gemini_3_1_flash_live(
5454
model_version
5555
)
56+
self._is_gemini_3_5_live_translate = (
57+
model_name_utils.is_gemini_3_5_live_translate(model_version)
58+
)
5659

5760
async def send_history(self, history: list[types.Content]):
5861
"""Sends the conversation history to the gemini model.
@@ -160,7 +163,7 @@ async def send_realtime(self, input: RealtimeInput):
160163
if isinstance(input, types.Blob):
161164
# The blob is binary and is very large. So let's not log it.
162165
logger.debug('Sending LLM Blob.')
163-
if self._is_gemini_3_1_flash_live:
166+
if self._is_gemini_3_1_flash_live or self._is_gemini_3_5_live_translate:
164167
if input.mime_type and input.mime_type.startswith('audio/'):
165168
await self._gemini_session.send_realtime_input(audio=input)
166169
elif input.mime_type and input.mime_type.startswith('image/'):

src/google/adk/utils/model_name_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,18 @@ def is_gemini_3_1_flash_live(model_string: Optional[str]) -> bool:
174174
return False
175175
model_name = extract_model_name(model_string)
176176
return model_name.startswith('gemini-3.1-flash-live')
177+
178+
179+
def is_gemini_3_5_live_translate(model_string: Optional[str]) -> bool:
180+
"""Check if the model is a Gemini 3.5 Live Translate model.
181+
182+
Args:
183+
model_string: The model name
184+
185+
Returns:
186+
True if it's a Gemini 3.5 Live Translate model, False otherwise
187+
"""
188+
if not model_string:
189+
return False
190+
model_name = extract_model_name(model_string)
191+
return model_name.startswith('gemini-3.5-live-translate')

tests/unittests/flows/llm_flows/test_basic_processor.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,43 @@ async def test_keeps_affective_dialog_and_proactivity_for_non_gemini_3_1(
254254

255255
assert llm_request.live_connect_config.enable_affective_dialog is True
256256
assert llm_request.live_connect_config.proactivity is not None
257+
258+
@pytest.mark.asyncio
259+
async def test_sets_translation_config(self):
260+
"""Translation config is forwarded to the live connect config."""
261+
agent = LlmAgent(
262+
name='test_agent',
263+
model='gemini-3.5-live-translate-preview',
264+
)
265+
invocation_context = await _create_invocation_context(agent)
266+
invocation_context.run_config = RunConfig(
267+
translation_config=types.TranslationConfig(
268+
target_language_code='pl',
269+
echo_target_language=True,
270+
),
271+
)
272+
llm_request = LlmRequest()
273+
processor = _BasicLlmRequestProcessor()
274+
275+
async for _ in processor.run_async(invocation_context, llm_request):
276+
pass
277+
278+
translation_config = llm_request.live_connect_config.translation_config
279+
assert translation_config.target_language_code == 'pl'
280+
assert translation_config.echo_target_language is True
281+
282+
@pytest.mark.asyncio
283+
async def test_translation_config_defaults_to_none(self):
284+
"""Without a translation config the live connect field stays None."""
285+
agent = LlmAgent(
286+
name='test_agent',
287+
model='gemini-2.5-flash-live',
288+
)
289+
invocation_context = await _create_invocation_context(agent)
290+
llm_request = LlmRequest()
291+
processor = _BasicLlmRequestProcessor()
292+
293+
async for _ in processor.run_async(invocation_context, llm_request):
294+
pass
295+
296+
assert llm_request.live_connect_config.translation_config is None

tests/unittests/models/test_gemini_llm_connection.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,24 @@ async def test_send_realtime_default_behavior(
7171
mock_gemini_session.send.assert_not_called()
7272

7373

74+
@pytest.mark.asyncio
75+
async def test_send_realtime_audio_uses_audio_channel_for_live_translate(
76+
mock_gemini_session, test_blob
77+
):
78+
"""Live Translate models stream audio via the dedicated `audio=` channel."""
79+
connection = GeminiLlmConnection(
80+
mock_gemini_session,
81+
api_backend=GoogleLLMVariant.GEMINI_API,
82+
model_version='gemini-3.5-live-translate-preview',
83+
)
84+
85+
await connection.send_realtime(test_blob)
86+
87+
mock_gemini_session.send_realtime_input.assert_called_once_with(
88+
audio=test_blob
89+
)
90+
91+
7492
@pytest.mark.asyncio
7593
async def test_send_history(gemini_connection, mock_gemini_session):
7694
"""Test send_history method."""

tests/unittests/utils/test_model_name_utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from google.adk.utils.model_name_utils import extract_model_name
1818
from google.adk.utils.model_name_utils import is_gemini_1_model
1919
from google.adk.utils.model_name_utils import is_gemini_3_1_flash_live
20+
from google.adk.utils.model_name_utils import is_gemini_3_5_live_translate
2021
from google.adk.utils.model_name_utils import is_gemini_eap_or_2_or_above
2122
from google.adk.utils.model_name_utils import is_gemini_model
2223
from google.adk.utils.model_name_utils import is_gemini_model_id_check_disabled
@@ -366,3 +367,22 @@ def test_is_gemini_3_1_flash_live_edge_cases(self):
366367
"""Test edge cases."""
367368
assert is_gemini_3_1_flash_live(None) is False
368369
assert is_gemini_3_1_flash_live('') is False
370+
371+
372+
class TestIsGemini35LiveTranslate:
373+
"""Test the is_gemini_3_5_live_translate function."""
374+
375+
def test_is_gemini_3_5_live_translate_simple_name(self):
376+
"""Test with simple model name format."""
377+
assert is_gemini_3_5_live_translate('gemini-3.5-live-translate') is True
378+
assert is_gemini_3_5_live_translate('gemini-3.5-flash-live') is False
379+
380+
def test_is_gemini_3_5_live_translate_path_based_name(self):
381+
"""Test with path-based format (Vertex AI etc.)."""
382+
vertex_path = 'projects/123/locations/us-central1/publishers/google/models/gemini-3.5-live-translate-preview'
383+
assert is_gemini_3_5_live_translate(vertex_path) is True
384+
385+
def test_is_gemini_3_5_live_translate_edge_cases(self):
386+
"""Test edge cases."""
387+
assert is_gemini_3_5_live_translate(None) is False
388+
assert is_gemini_3_5_live_translate('') is False

0 commit comments

Comments
 (0)