Structured-output continuation merge raises AttributeError: __pydantic_generic_metadata__ when a hop has parsed JSON

Abierto
#3,350 1 comentario 0 reacciones 1 asignado Ver en GitHub

Los mantenedores suelen responder en 1 día

@Venkaiahbabuneelam ya está trabajando en esto.

Desde el 11/11/2026.

  • #3351 de @copybara-service — abierto

Evaluación

Este issue todavía no se ha evaluado.

Descripción

priority: p2 type: bug
Summary

Structured-output generation can fail inside merge_continuation_responses() when a continuation hop already has a non-null parsed dictionary. The merge serializes that derived field back into GenerateContentResponse._from_response(). Extra-field pruning then treats the dictionary as the bare pydantic.BaseModel and calls BaseModel.model_rebuild(), raising:

AttributeError: __pydantic_generic_metadata__

We originally encountered this SDK stack during an asynchronous Vertex AI generate_content() call with response_json_schema. The deterministic reproduction below uses synthetic responses only: no credentials, model access, network requests, or application data are required.

Environment details
  • Programming language: Python
  • OS for local reproduction: macOS (Darwin)
  • Language runtime version: Python 3.12.13
  • Package versions: google-genai==3.1.0, pydantic==2.13.5
  • Also reproduced against unmodified main at f1670003f5eaf9d367ab414b8c5d757c991d8cc0 with the same Python/Pydantic versions.
Steps to reproduce
  1. Install the released packages in an isolated Python 3.12 environment:

    python -m pip install "google-genai==3.1.0" "pydantic==2.13.5"
    
  2. Run the following script. It models a thought-only continuation hop followed by a complete JSON answer. The final hop's parsed value is produced by the SDK's own _from_response(), not assigned manually.

import platform
import traceback

import pydantic
from google import genai
from google.genai import _extra_utils, types

print(f"Python={platform.python_version()} OS={platform.system()}")
print(f"google-genai={genai.__version__} pydantic={pydantic.__version__}")

schema = {
    "type": "object",
    "properties": {"name": {"type": "string"}},
    "required": ["name"],
}
config = {"response_mime_type": "application/json", "response_json_schema": schema}

# A thought-only continuation hop, followed by a complete JSON response.
first = types.GenerateContentResponse(
    candidates=[types.Candidate(
        content=types.Content(role="model", parts=[
            types.Part(text="Thinking about the answer.", thought=True)
        ]),
        finish_reason="CONTINUATION",
    )]
)
last = types.GenerateContentResponse._from_response(
    response={"candidates": [{
        "content": {"role": "model", "parts": [{"text": '{"name":"Example"}'}]},
        "finish_reason": "STOP",
    }]},
    kwargs={"config": config},
)
print("last.parsed:", last.parsed)
assert last.parsed == {"name": "Example"}
print("one hop:", _extra_utils.merge_continuation_responses([last], config=config).parsed)
try:
    merged = _extra_utils.merge_continuation_responses([first, last], config=config)
    print("two hops:", merged.parsed)
except Exception:
    traceback.print_exc()
    raise SystemExit(1)
Expected behavior

Both the single-hop control and the two-hop merge return parsed == {"name": "Example"}. The derived parsed field should be recomputed from the merged answer without being treated as an API response field.

Actual behavior

The single-hop control succeeds; the two-hop merge raises the exception above and exits with status 1:

Python=3.12.13 OS=Darwin
google-genai=3.1.0 pydantic=2.13.5
last.parsed: {'name': 'Example'}
one hop: {'name': 'Example'}

Relevant stack frames from the local reproduction (environment-specific path prefixes omitted):

google/genai/_extra_utils.py:1338, in merge_continuation_responses
    reparsed = types.GenerateContentResponse._from_response(...)
google/genai/types.py:9000, in _from_response
    result = super()._from_response(response=response, kwargs=kwargs)
google/genai/_common.py:688, in _from_response
    _remove_extra_fields(cls, response)
google/genai/_common.py:393, in _remove_extra_fields
    _remove_extra_fields(model_type, value)
google/genai/_common.py:365, in _remove_extra_fields
    model.model_rebuild(raise_errors=False)
pydantic/main.py:683, in model_rebuild
    return _model_construction.complete_model_class(...)
pydantic/_internal/_model_construction.py:632, in complete_model_class
    typevars_map = get_model_typevars_map(cls)
pydantic/_internal/_generics.py:236, in get_model_typevars_map
    generic_metadata = cls.__pydantic_generic_metadata__
AttributeError: __pydantic_generic_metadata__
Analysis and possible fix
  • merge_continuation_responses() retains a hop's parsed value and calls _from_response(response=merged.model_dump(exclude_none=True), ...) to recompute structured output.
  • GenerateContentResponse.parsed is annotated as Optional[Union[pydantic.BaseModel, dict[Any, Any], Enum]].
  • _remove_extra_fields() takes the first union member, then recursively visits the actual dictionary as pydantic.BaseModel. Its model_rebuild() call on that base class causes this exception.

A narrowly scoped candidate is to exclude the derived parsed field from the serialization used for reparsing:

 reparsed = types.GenerateContentResponse._from_response(
-    response=merged.model_dump(exclude_none=True),
+    response=merged.model_dump(exclude_none=True, exclude={'parsed'}),
     kwargs={'config': config_dict},
 )

I verified that applying only this change to the checkout above makes the same reproduction return two hops: {'name': 'Example'}. This is a candidate fix, not a claim that the full SDK test suite has been validated.

A regression test should cover a non-null parsed dictionary on a hop, including the thought-only-continuation/final-JSON case above. The existing structured continuation test starts with two partial JSON responses whose parsed values are unset, so it does not exercise this case.

Lenguaje dominante
Python
Estrellas
4.1k
Forks
1.1k
Merge medio
1 d 20 h
PR fusionados (30 d)
90

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de googleapis/python-genai

Todos los issues de googleapis/python-genai

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.