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

Đang mở
#3,350 1 bình luận 0 reaction 1 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@Venkaiahbabuneelam đang làm issue này rồi.

Từ ngày 11/11/2026.

  • #3351 của @copybara-service — đang mở

Đánh giá

Issue này chưa được đánh giá.

Mô tả

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.

Ngôn ngữ chính
Python
Star
4.1k
Fork
1.1k
Merge trung bình
1 ngày 20 giờ
Pull request đã merge (30 ngày)
90

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của googleapis/python-genai

Tất cả issue của googleapis/python-genai

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.