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

Offen
#3,350 1 Kommentar 0 Reaktionen 1 zugewiesene Person Auf GitHub ansehen

Maintainer antworten meist innerhalb von 1 Tag

@Venkaiahbabuneelam arbeitet bereits daran.

Seit 11.11.2026.

  • #3351 von @copybara-service — offen

Bewertung

Dieses Issue wurde noch nicht bewertet.

Beschreibung

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.

Vorherrschende Sprache
Python
Sterne
4.1k
Forks
1.1k
Ø Merge
1 T. 20 Std.
Gemergte PRs (30 T.)
90

Entwicklungsumgebung

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus googleapis/python-genai

Alle Issues in googleapis/python-genai

Ähnliche Issues

Weitere Issues zu Python

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.