Python: surface Gemini thought summaries as reasoning content (#7488)
Gemini thought-summary parts (part.thought=True) were dropped in _parse_parts, so reasoning never reached ChatResponse.contents. Emit them as text_reasoning content instead, matching OpenAIResponsesClient. Round-trip is safe: _convert_message_contents never re-emits reasoning text as a Part. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b8aa906e-1408-40c1-9a45-6deb40dc36f8
This commit is contained in:
@@ -78,8 +78,8 @@ class ThinkingConfig(TypedDict, total=False):
|
||||
Attributes:
|
||||
include_thoughts: Whether to include thought summaries in the response. Thought summaries
|
||||
are condensed representations of the model's internal reasoning and appear as response
|
||||
parts where ``part.thought`` is ``True``. Note: the framework currently excludes
|
||||
thought parts from ``ChatResponse.contents`` and does not surface them as output.
|
||||
parts where ``part.thought`` is ``True``. When set, the framework surfaces these parts
|
||||
as ``text_reasoning`` content in ``ChatResponse.contents``.
|
||||
thinking_budget: Token budget for Gemini 2.5 models. Set to ``0`` to disable
|
||||
thinking or ``-1`` to enable a dynamic budget.
|
||||
thinking_level: Thinking level for Gemini 2.5 models and later. One of
|
||||
@@ -1120,17 +1120,20 @@ class RawGeminiChatClient(
|
||||
)
|
||||
|
||||
def _parse_parts(self, parts: Sequence[types.Part]) -> list[Content]:
|
||||
"""Convert Gemini response parts to framework Content objects, skipping thought/reasoning parts.
|
||||
"""Convert Gemini response parts to framework Content objects.
|
||||
|
||||
Args:
|
||||
parts: Sequence of ``types.Part`` objects from a Gemini response candidate.
|
||||
|
||||
Returns:
|
||||
A list of framework ``Content`` objects (text, function_call, or function_result).
|
||||
A list of framework ``Content`` objects (text_reasoning, text, function_call, or
|
||||
function_result).
|
||||
"""
|
||||
contents: list[Content] = []
|
||||
for part in parts:
|
||||
if part.thought:
|
||||
if part.text:
|
||||
contents.append(Content.from_text_reasoning(text=part.text, raw_representation=part))
|
||||
continue
|
||||
if part.text is not None:
|
||||
contents.append(Content.from_text(text=part.text, raw_representation=part))
|
||||
|
||||
@@ -714,8 +714,8 @@ async def test_non_function_result_content_in_tool_message_is_skipped() -> None:
|
||||
# thinking parts
|
||||
|
||||
|
||||
async def test_thinking_parts_are_silently_skipped() -> None:
|
||||
"""Excludes thought-summary parts from ChatResponse.contents, returning only the final answer."""
|
||||
async def test_thinking_parts_are_surfaced_as_reasoning() -> None:
|
||||
"""Surfaces thought-summary parts as text_reasoning content alongside the final answer."""
|
||||
client, mock = _make_gemini_client()
|
||||
mock.aio.models.generate_content = AsyncMock(
|
||||
return_value=_make_response([
|
||||
@@ -728,10 +728,28 @@ async def test_thinking_parts_are_silently_skipped() -> None:
|
||||
messages=[Message(role="user", contents=[Content.from_text("What is the answer?")])]
|
||||
)
|
||||
|
||||
assert len(response.messages[0].contents) == 1
|
||||
contents = response.messages[0].contents
|
||||
assert len(contents) == 2
|
||||
assert contents[0].type == "text_reasoning"
|
||||
assert contents[0].text == "I should think first..."
|
||||
assert contents[1].type == "text"
|
||||
assert response.messages[0].text == "The answer is 42."
|
||||
|
||||
|
||||
async def test_empty_thinking_part_produces_no_reasoning_content() -> None:
|
||||
"""A thought part with no text yields no content rather than empty reasoning."""
|
||||
client, _ = _make_gemini_client()
|
||||
|
||||
contents = client._parse_parts([
|
||||
_make_part(text=None, thought=True),
|
||||
_make_part(text="The answer is 42."),
|
||||
])
|
||||
|
||||
assert len(contents) == 1
|
||||
assert contents[0].type == "text"
|
||||
assert contents[0].text == "The answer is 42."
|
||||
|
||||
|
||||
def test_function_call_part_preserves_thought_signature_from_raw_part() -> None:
|
||||
"""Reuses the original Gemini Part so tool loops retain thought_signature metadata."""
|
||||
client, _ = _make_gemini_client()
|
||||
|
||||
Reference in New Issue
Block a user