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

未關閉
#3,350 1 則留言 0 個 reaction 已指派 1 人 在 GitHub 檢視

維護者通常 1 天內回覆

@Venkaiahbabuneelam 已經在處理了。

開始於 2026年11月11日。

  • #3351 來自 @copybara-service —— 未關閉

評估

這個 Issue 還沒有評估資料。

描述

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.

主要語言
Python
星號
4.1k
分支
1.1k
平均合併
1 天 20 小時
30 天內合併 PR
90

環境準備

  • 沒有 Dockerfile 或 Docker Compose 檔案
  • 沒有 Pull Request 範本
  • 閱讀貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

googleapis/python-genai 的其他 Issue

查看 googleapis/python-genai 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。