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

Aberta
#3,350 1 comentário 0 reações 1 responsável Ver no GitHub

Mantenedores costumam responder em até 1 dia

@Venkaiahbabuneelam já está trabalhando nisso.

Desde 11/11/2026.

  • #3351 de @copybara-service — aberto

Avaliação

Esta issue ainda não foi avaliada.

Descrição

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.

Linguagem predominante
Python
Estrelas
4.1k
Forks
1.1k
Merge médio
1d 20h
PRs com merge (30d)
90

Preparar o ambiente

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de googleapis/python-genai

Todas as issues de googleapis/python-genai

Issues semelhantes

Mais issues de Python

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.