mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(gemini-realtime): keep the client's voice on Vertex AI native-audio Live
Vertex AI Live accepts a speechConfig on setup for native-audio models, so stripping it silently discarded the voice a client asked for. Confirmed against a live BidiGenerateContent session on gemini-live-2.5-flash-native-audio and on gemini-live-2.5-flash-preview-native-audio-09-2025: both return setupComplete with speechConfig present. The strip stays in place for Google AI Studio, which was never verified to accept it, via an overridable predicate matching the existing _include_function_response_id pattern. The responseModalities TEXT to AUDIO coercion is unchanged, since Vertex does reject TEXT on these models.
This commit is contained in:
parent
ace28fd97a
commit
5d8769bbaf
3 changed files with 59 additions and 3 deletions
|
|
@ -92,6 +92,10 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
|||
"""Google AI Studio Gemini 3.5+ accepts ``id`` on functionResponses; Vertex AI rejects it."""
|
||||
return True
|
||||
|
||||
def _strip_native_audio_speech_config(self) -> bool:
|
||||
"""Google AI Studio native-audio Live rejects ``speechConfig`` on setup; Vertex AI accepts it."""
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _usage_detail_alias(details: Any, defaults: dict[str, int]) -> dict[str, Any]:
|
||||
if not isinstance(details, dict):
|
||||
|
|
@ -382,8 +386,7 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
|||
without_text: Final = [modality for modality in normalized if modality != "TEXT"]
|
||||
return without_text if without_text else ["AUDIO"]
|
||||
|
||||
@staticmethod
|
||||
def _finalize_gemini_live_setup(model: str, setup: dict[str, Any]) -> dict[str, Any]:
|
||||
def _finalize_gemini_live_setup(self, model: str, setup: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Drop fields Gemini Live native-audio rejects on ``setup``."""
|
||||
generation_config: Final = setup.get("generationConfig")
|
||||
if isinstance(generation_config, dict):
|
||||
|
|
@ -392,7 +395,7 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
|||
generation_config["responseModalities"] = GeminiRealtimeConfig._coerce_response_modalities(
|
||||
model, modalities
|
||||
)
|
||||
if GeminiRealtimeConfig._is_native_audio_model(model):
|
||||
if self._strip_native_audio_speech_config() and GeminiRealtimeConfig._is_native_audio_model(model):
|
||||
generation_config.pop("speechConfig", None)
|
||||
return setup
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ class VertexAIRealtimeConfig(GeminiRealtimeConfig):
|
|||
def _include_function_response_id(self) -> bool:
|
||||
return False
|
||||
|
||||
def _strip_native_audio_speech_config(self) -> bool:
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# URL
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -462,3 +462,53 @@ def test_vertex_function_call_output_omits_id():
|
|||
assert "id" not in function_response
|
||||
assert function_response["name"] == "terminate_call"
|
||||
assert function_response["response"] == {"status": "ok"}
|
||||
|
||||
|
||||
def test_vertex_native_audio_keeps_requested_voice(patch_native_audio_cost_map_entry):
|
||||
"""Regression: Vertex Live accepts speechConfig on native audio, so the client's voice must survive.
|
||||
|
||||
Stripping it silently dropped voice selection for every Vertex native-audio
|
||||
session. TEXT is still coerced away, which Vertex does reject.
|
||||
"""
|
||||
cfg = VertexAIRealtimeConfig(
|
||||
access_token="tok", project="my-proj", location="us-central1"
|
||||
)
|
||||
session_update = {
|
||||
"type": "session.update",
|
||||
"session": {
|
||||
"output_modalities": ["text"],
|
||||
"audio": {"output": {"voice": "Aoede"}},
|
||||
},
|
||||
}
|
||||
|
||||
messages = cfg.transform_realtime_request(
|
||||
json.dumps(session_update),
|
||||
_NATIVE_AUDIO_MODEL,
|
||||
session_configuration_request=None,
|
||||
)
|
||||
|
||||
generation_config = json.loads(messages[0])["setup"]["generationConfig"]
|
||||
assert generation_config["speechConfig"]["voiceConfig"]["prebuiltVoiceConfig"]["voiceName"] == "Aoede"
|
||||
assert generation_config["responseModalities"] == ["AUDIO"]
|
||||
|
||||
|
||||
def test_google_ai_studio_native_audio_still_strips_voice(patch_native_audio_cost_map_entry):
|
||||
"""Google AI Studio's own native-audio Live is untouched: only Vertex was verified to accept speechConfig."""
|
||||
from litellm.llms.gemini.realtime.transformation import GeminiRealtimeConfig
|
||||
|
||||
messages = GeminiRealtimeConfig().transform_realtime_request(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "session.update",
|
||||
"session": {
|
||||
"output_modalities": ["audio"],
|
||||
"audio": {"output": {"voice": "Aoede"}},
|
||||
},
|
||||
}
|
||||
),
|
||||
_NATIVE_AUDIO_MODEL,
|
||||
session_configuration_request=None,
|
||||
)
|
||||
|
||||
generation_config = json.loads(messages[0])["setup"]["generationConfig"]
|
||||
assert "speechConfig" not in generation_config
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue