From 820bd70971076711cc072c39073fa5bc485fcee1 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:13:21 +0800 Subject: [PATCH] fix: handle legacy Lyria output accessors --- google/genai/_interactions/_legacy_lyria.py | 13 +++- .../genai/_interactions/types/interaction.py | 6 +- .../tests/interactions/test_legacy_lyria.py | 68 +++++++++++++++++++ 3 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 google/genai/tests/interactions/test_legacy_lyria.py diff --git a/google/genai/_interactions/_legacy_lyria.py b/google/genai/_interactions/_legacy_lyria.py index 4f5e92892..88b2aa56c 100644 --- a/google/genai/_interactions/_legacy_lyria.py +++ b/google/genai/_interactions/_legacy_lyria.py @@ -103,13 +103,22 @@ } +def _is_legacy_lyria_model(model: object) -> bool: + if not isinstance(model, str): + return False + return any( + model == known_model or model.endswith(f"/models/{known_model}") + for known_model in LEGACY_LYRIA_MODELS + ) + + def is_legacy_lyria_request(*, is_vertex: bool, model: object) -> bool: """Return True iff the (client, model) combination needs the shim active. Used at request issue time (in the resource layer) to decide whether to pick the `LegacyLyriaInteractionStream` subclass for streaming requests. """ - return bool(is_vertex) and isinstance(model, str) and model in LEGACY_LYRIA_MODELS + return bool(is_vertex) and _is_legacy_lyria_model(model) def is_legacy_lyria_response_body(data: object) -> bool: @@ -125,7 +134,7 @@ def is_legacy_lyria_response_body(data: object) -> bool: return False typed_data: Dict[str, Any] = cast("Dict[str, Any]", data) model = typed_data.get("model") - return isinstance(model, str) and model in LEGACY_LYRIA_MODELS + return _is_legacy_lyria_model(model) def maybe_remap_legacy_sse_event(data: Dict[str, Any]) -> Dict[str, Any]: diff --git a/google/genai/_interactions/types/interaction.py b/google/genai/_interactions/types/interaction.py index 41c241f2b..42c437401 100644 --- a/google/genai/_interactions/types/interaction.py +++ b/google/genai/_interactions/types/interaction.py @@ -430,7 +430,7 @@ def output_text(self) -> str: @property def output_image(self) -> Optional[ImageContent]: """The last image generated by the model in response to the current request.""" - for step in reversed(self.steps): + for step in reversed(self.steps or []): if isinstance(step, UserInputStep): break if isinstance(step, ModelOutputStep) and step.content: @@ -442,7 +442,7 @@ def output_image(self) -> Optional[ImageContent]: @property def output_audio(self) -> Optional[AudioContent]: """The last audio generated by the model in response to the current request.""" - for step in reversed(self.steps): + for step in reversed(self.steps or []): if isinstance(step, UserInputStep): break if isinstance(step, ModelOutputStep) and step.content: @@ -454,7 +454,7 @@ def output_audio(self) -> Optional[AudioContent]: @property def output_video(self) -> Optional[VideoContent]: """The last video generated by the model in response to the current request.""" - for step in reversed(self.steps): + for step in reversed(self.steps or []): if isinstance(step, UserInputStep): break if isinstance(step, ModelOutputStep) and step.content: diff --git a/google/genai/tests/interactions/test_legacy_lyria.py b/google/genai/tests/interactions/test_legacy_lyria.py new file mode 100644 index 000000000..831bd3d58 --- /dev/null +++ b/google/genai/tests/interactions/test_legacy_lyria.py @@ -0,0 +1,68 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from ..._interactions._legacy_lyria import ( + is_legacy_lyria_request, + is_legacy_lyria_response_body, +) +from ..._interactions.types.interaction import Interaction + + +_LYRIA_MODEL_PATH = ( + "projects/123/locations/global/publishers/google/models/lyria-3-clip-preview" +) + + +def test_legacy_lyria_vertex_model_path_rewrites_outputs_to_steps(): + interaction = Interaction.model_validate( + { + "id": "interaction-1", + "created": "2026-01-01T00:00:00Z", + "updated": "2026-01-01T00:00:01Z", + "status": "completed", + "model": _LYRIA_MODEL_PATH, + "outputs": [ + { + "type": "audio", + "data": "abc", + "mime_type": "audio/wav", + } + ], + } + ) + + assert len(interaction.steps) == 1 + assert interaction.output_audio is not None + assert interaction.output_audio.data == "abc" + assert interaction.model_extra is None or "outputs" not in interaction.model_extra + + +def test_legacy_lyria_model_path_detection(): + assert is_legacy_lyria_request(is_vertex=True, model=_LYRIA_MODEL_PATH) + assert is_legacy_lyria_response_body({"model": _LYRIA_MODEL_PATH}) + + +def test_output_accessors_tolerate_missing_steps(): + interaction = Interaction.construct( + id="interaction-1", + created="2026-01-01T00:00:00Z", + updated="2026-01-01T00:00:01Z", + status="completed", + steps=None, + ) + + assert interaction.output_image is None + assert interaction.output_audio is None + assert interaction.output_video is None