Skip to content

output_audio crashes with TypeError when Vertex AI returns legacy Lyria response (steps=None); output_text has the fix but output_audio doesn't #2534

@zJvco

Description

@zJvco

Is this a client library issue or a product issue?

Client library issue. Two related bugs in google-genai that together cause interaction.output_audio to raise TypeError when using Lyria models (lyria-3-clip-preview, lyria-3-pro-preview) via Vertex AI with ADC credentials.

Environment details

  • Programming language: Python
  • OS: Linux (Ubuntu / WSL2)
  • Language runtime version: Python 3.12
  • Package version: google-genai==2.7.0
  • Auth mode: Vertex AI with ADC (vertexai=True, project, location="global")

Steps to reproduce

  • Set up Vertex AI ADC credentials with location="global" (required for Lyria)
  • Call client.interactions.create() with a Lyria model (async client)
  • Access interaction.output_audio on the returned object
import asyncio
from google import genai

async def main():
    client = genai.Client(vertexai=True, project="YOUR_PROJECT_ID", location="global").aio

    interaction = await client.interactions.create(
        model="lyria-3-clip-preview",
        input="A short upbeat lo-fi hip hop piece with piano.",
    )

    # Crashes here:
    audio = interaction.output_audio

asyncio.run(main())

Error:

File ".../google/genai/_interactions/types/interaction.py", line 281, in output_audio
    for step in reversed(self.steps):
TypeError: 'NoneType' object is not reversible

Root cause analysis

Bug 1 - output_audio missing or [] guard (line 281 of interaction.py)
output_text already handles steps=None safely:

# output_text — line 248 ✓
for step in reversed(self.steps or []):

# output_audio — line 281 ✗
for step in reversed(self.steps):

# output_image — line 269 ✗
for step in reversed(self.steps):

output_audio and output_image are missing the or [] guard that output_text already has. A one-line fix in each would make them consistent.

Bug 2 - is_legacy_lyria_response_body doesn't match Vertex AI resource paths

_maybe_coerce_outputs (which converts the legacy outputs field into steps) only triggers when the response body's model field matches:

# _legacy_lyria.py
LEGACY_LYRIA_MODELS = frozenset({"lyria-3-pro-preview", "lyria-3-clip-preview"})

def is_legacy_lyria_response_body(data):
    model = typed_data.get("model")
    return isinstance(model, str) and model in LEGACY_LYRIA_MODELS

However, the Vertex AI endpoint returns model as a full resource path, e.g.:

projects/5786546123365/locations/global/publishers/google/models/lyria-3-clip-preview

This does not match "lyria-3-clip-preview" exactly, so _maybe_coerce_outputs skips the outputs → steps rewrite. As a result, steps stays None (Pydantic allows it despite the List[Step] annotation) and the outputs field is stored in pydantic_extra instead.
Combined effect: steps=None → output_audio crashes; audio data is accessible but stranded in interaction.model_extra["outputs"].

Expected behavior

interaction.output_audio should return the AudioContent object (or None) without raising TypeError, regardless of whether steps is None.

Suggested fixes

Bug 1 - add or [] to output_audio and output_image (same pattern already used in output_text):

# interaction.py line 281
for step in reversed(self.steps or []):  # was: reversed(self.steps)

# interaction.py line 269
for step in reversed(self.steps or []):  # was: reversed(self.steps)

Bug 2 - make is_legacy_lyria_response_body match the full resource path variant:

def is_legacy_lyria_response_body(data):
    model = typed_data.get("model")
    if not isinstance(model, str):
        return False
    # Match both short name and full Vertex AI resource path
    return any(model == m or model.endswith(f"/models/{m}") for m in LEGACY_LYRIA_MODELS)
Workaround (applied in our codebase)

try:
    audio = interaction.output_audio
except TypeError:
    # steps=None due to Bug 2; fall back to __pydantic_extra__["outputs"]
    from google.genai._interactions.types.audio_content import AudioContent as _AudioContent
    raw_outputs = (interaction.model_extra or {}).get("outputs") or []
    audio = None
    for item in reversed(raw_outputs):
        if isinstance(item, dict) and item.get("type") == "audio":
            audio = _AudioContent.model_validate(item)
            break
        elif hasattr(item, "type") and item.type == "audio":
            audio = item
            break

Metadata

Metadata

Assignees

Labels

priority: p2Moderately-important priority. Fix may not be included in next release.status:awaiting user responsetype: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions