Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Report text parts with `thought=True` as `ReasoningPart` instead of `TextPart` in `generate_content` and `generate_content_stream` messages.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
InputMessage,
MessagePart,
OutputMessage,
ReasoningPart,
Role,
ServerToolCallPart,
ServerToolCallResponsePart,
Expand Down Expand Up @@ -91,6 +92,8 @@ def tool_call_id(name: str | None) -> str:
return f"{idx}"

if (text := part.text) is not None:
if part.thought:
return ReasoningPart(content=text)
return TextPart(content=text)

if inline_data := part.inline_data:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
GoogleMaps,
MediaModality,
ModalityTokenCount,
Part,
ToolDict,
)
from pydantic import BaseModel, Field
Expand Down Expand Up @@ -751,6 +752,24 @@ def test_span_attributes_with_content_capture(self):
'[{"name":"_mock_callable_tool","description":"Description of some tool.","parameters":null,"type":"function"},{"name":"mock_tool","description":"Description of mock tool.","parameters":null,"type":"function"},{"name":"google_maps","type":"google_maps"}]',
)

@patch.dict(
"os.environ",
{"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "SPAN_ONLY"},
)
def test_reports_thought_parts_as_reasoning(self):
self.configure_valid_response(
parts=[
Part(text="Let me think.", thought=True),
Part(text="It is sunny."),
]
)
self.generate_content(model="gemini-2.0-flash", contents="Hi")
span = self.otel.get_span_named("generate_content gemini-2.0-flash")
self.assertEqual(
span.attributes[gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES],
'[{"role":"assistant","parts":[{"content":"Let me think.","type":"reasoning"},{"content":"It is sunny.","type":"text"}],"finish_reason":"","name":null}]',
)

def test_log_has_extra_genai_attributes(self):
self.configure_valid_response(text="Yep, it works!")
tok = context_api.attach(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
GenerateContentResponseUsageMetadata,
MediaModality,
ModalityTokenCount,
Part,
)

from opentelemetry import context as context_api
Expand Down Expand Up @@ -163,6 +164,21 @@ def test_merges_streamed_chunks_into_one_output_message(self):
'[{"role":"assistant","parts":[{"content":"Hello, world","type":"text"}],"finish_reason":"","name":null}]',
)

@patch.dict(
"os.environ",
{"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "SPAN_ONLY"},
)
def test_reports_streamed_thought_parts_as_reasoning(self):
self.configure_valid_response(part=Part(text="Let me ", thought=True))
self.configure_valid_response(part=Part(text="think.", thought=True))
self.configure_valid_response(text="It is sunny.")
self.generate_content(model="gemini-2.0-flash", contents="Hi")
span = self.otel.get_span_named("generate_content gemini-2.0-flash")
self.assertEqual(
span.attributes[gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES],
'[{"role":"assistant","parts":[{"content":"Let me think.","type":"reasoning"},{"content":"It is sunny.","type":"text"}],"finish_reason":"","name":null}]',
)

def test_includes_token_counts_in_span_not_aggregated_from_responses(self):
# Tokens should not be aggregated in streaming. Cumulative counts are returned on each response.
self.configure_valid_response(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from opentelemetry.util.genai.types import (
GenericPart,
ReasoningPart,
ServerToolCallPart,
ServerToolCallResponsePart,
TextPart,
Expand Down Expand Up @@ -48,11 +49,28 @@ def test_to_system_instructions_non_text_parts(self):
],
)

def test_to_system_instructions_thought_part(self):
content = genai_types.Content(
parts=[genai_types.Part(text="Let me think.", thought=True)]
)
instructions = to_system_instructions(content=content)
self.assertEqual(instructions, [GenericPart(type="reasoning")])
Comment on lines +52 to +57

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenericPart is deprecated and on its way out, so this test pins behavior we're about to remove. Could reasoning parts be dropped from system instructions instead?

Suggested change
def test_to_system_instructions_thought_part(self):
content = genai_types.Content(
parts=[genai_types.Part(text="Let me think.", thought=True)]
)
instructions = to_system_instructions(content=content)
self.assertEqual(instructions, [GenericPart(type="reasoning")])
def test_to_system_instructions_thought_part(self):
content = genai_types.Content(
parts=[genai_types.Part(text="Let me think.", thought=True)]
)
instructions = to_system_instructions(content=content)
self.assertEqual(instructions, [])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will address in #936


def test_to_system_instructions_empty_or_unknown_part(self):
content = genai_types.Content(parts=[genai_types.Part()])
instructions = to_system_instructions(content=content)
self.assertEqual(instructions, [])

def test_to_part_maps_thought_text_to_reasoning(self):
self.assertEqual(
_to_part(genai_types.Part(text="Let me think.", thought=True), 0),
ReasoningPart(content="Let me think."),
)
self.assertEqual(
_to_part(genai_types.Part(text="Sunny.", thought=False), 0),
TextPart(content="Sunny."),
)

def test_to_part_maps_code_execution(self):
call_id = (
{"id": "code-1"}
Expand Down
Loading