From 32d3e05bc730d38be5805e8c17da253e8c681b24 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 7 Aug 2026 08:28:41 -0700 Subject: [PATCH 01/16] Align message-part class names with semconv (*Part suffix) --- .../instrumentation/genai/agno/patch.py | 6 +-- .../instrumentation/genai/anthropic/utils.py | 38 +++++++-------- .../genai/langchain/callback_handler.py | 10 ++-- .../instrumentation/genai/langchain/utils.py | 26 +++++----- .../tests/test_callback_handler.py | 18 +++---- .../genai/openai/chat_wrappers.py | 8 ++-- .../genai/openai/response_extractors.py | 48 +++++++++---------- .../instrumentation/genai/openai/utils.py | 18 +++---- .../tests/test_response_extractors.py | 2 +- .../instrumentation/genai/qwen_agent/utils.py | 20 ++++---- .../google_genai/interactions.py | 24 +++++----- .../instrumentation/google_genai/message.py | 20 ++++---- .../tests/interactions/test_parser.py | 26 +++++----- .../util/genai/_upload/completion_hook.py | 2 +- .../src/opentelemetry/util/genai/types.py | 36 +++++++------- .../tests/test_handler_agent.py | 16 +++---- .../tests/test_handler_completion_hook.py | 28 +++++------ .../tests/test_handler_workflow.py | 6 +-- .../tests/test_toolcall.py | 28 +++++------ .../tests/test_upload.py | 18 +++---- .../tests/test_utils.py | 8 ++-- .../tests/test_workflow_invocation.py | 10 ++-- 22 files changed, 208 insertions(+), 208 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py b/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py index 1c89fa155..3431fd3ad 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py @@ -24,7 +24,7 @@ from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, - Text, + TextPart, ) logger = logging.getLogger(__name__) @@ -151,7 +151,7 @@ def _set_invocation_input( if input_val is not None: content_str = _extract_input_content(input_val) invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content=content_str)]) + InputMessage(role="user", parts=[TextPart(content=content_str)]) ] @@ -165,7 +165,7 @@ def _set_invocation_output( invocation.output_messages = [ OutputMessage( role="assistant", - parts=[Text(content=output_str)], + parts=[TextPart(content=output_str)], finish_reason="stop", ) ] diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py b/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py index e405d262d..7b6fa6682 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py @@ -24,12 +24,12 @@ ) from opentelemetry.util.genai.types import ( - Blob, + BlobPart, MessagePart, - Reasoning, - Text, - ToolCallRequest, - ToolCallResponse, + ReasoningPart, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ) if TYPE_CHECKING: @@ -99,7 +99,7 @@ def _decode_base64(data: str) -> bytes | None: return None -def _extract_base64_blob(source: object, modality: str) -> Blob | None: +def _extract_base64_blob(source: object, modality: str) -> BlobPart | None: """Extract a Blob from a base64-encoded source dict.""" if not isinstance(source, dict): return None @@ -112,7 +112,7 @@ def _extract_base64_blob(source: object, modality: str) -> Blob | None: if decoded is None: return None media_type: object = source.get("media_type") # type: ignore[reportUnknownMemberType] - return Blob( + return BlobPart( mime_type=media_type if isinstance(media_type, str) else None, modality=modality, content=decoded, @@ -127,25 +127,25 @@ def _convert_dict_block_to_part( if block_type == "text": text = block.get("text") - return Text(content=str(text) if text is not None else "") + return TextPart(content=str(text) if text is not None else "") if block_type == "tool_use": inp = block.get("input") - return ToolCallRequest( + return ToolCallRequestPart( arguments=inp if isinstance(inp, dict) else None, name=str(block.get("name", "")), id=str(block.get("id", "")), ) if block_type == "tool_result": - return ToolCallResponse( + return ToolCallResponsePart( response=block.get("content"), id=str(block.get("tool_use_id", "")), ) if block_type in ("thinking", "redacted_thinking"): thinking = block.get("thinking") or block.get("data") - return Reasoning(content=str(thinking) if thinking is not None else "") + return ReasoningPart(content=str(thinking) if thinking is not None else "") if block_type in ("image", "audio", "video", "document", "file"): return _extract_base64_blob(block.get("source"), str(block_type)) @@ -158,10 +158,10 @@ def _convert_content_block_to_part( ) -> MessagePart | None: """Convert an Anthropic content block to a MessagePart.""" if isinstance(block, TextBlock): - return Text(content=block.text) + return TextPart(content=block.text) if isinstance(block, (ToolUseBlock, ServerToolUseBlock)): - return ToolCallRequest( + return ToolCallRequestPart( arguments=block.input, name=block.name, id=block.id ) @@ -169,10 +169,10 @@ def _convert_content_block_to_part( content = ( block.thinking if isinstance(block, ThinkingBlock) else block.data ) - return Reasoning(content=content) + return ReasoningPart(content=content) if isinstance(block, WebSearchToolResultBlock): - return ToolCallResponse( + return ToolCallResponsePart( response=block.model_dump().get("content"), id=block.tool_use_id, ) @@ -188,7 +188,7 @@ def convert_content_to_parts( if content is None: return [] if isinstance(content, str): - return [Text(content=content)] + return [TextPart(content=content)] parts: list[MessagePart] = [] for item in content: part = _convert_content_block_to_part(item) @@ -236,7 +236,7 @@ def update_stream_block_state( def stream_block_state_to_part(state: StreamBlockState) -> MessagePart | None: if state.type == "text": - return Text(content=state.text) + return TextPart(content=state.text) if state.type == "tool_use": arguments: str | dict[str, object] | None = state.tool_input @@ -245,13 +245,13 @@ def stream_block_state_to_part(state: StreamBlockState) -> MessagePart | None: arguments = json.loads(state.input_json) except ValueError: arguments = state.input_json - return ToolCallRequest( + return ToolCallRequestPart( arguments=arguments, name=state.tool_name, id=state.tool_id, ) if state.type in ("thinking", "redacted_thinking"): - return Reasoning(content=state.thinking) + return ReasoningPart(content=state.thinking) return None diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py index c41f3a386..b205c0f6d 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py @@ -42,8 +42,8 @@ from opentelemetry.util.genai.types import ( MessagePart, OutputMessage, - Text, - ToolCallRequest, + TextPart, + ToolCallRequestPart, ) SUPPORTED_RAPI_RESPONSE_HEADERS = ("x-ms-served-model",) @@ -374,9 +374,9 @@ def on_llm_end( ) if finish_reason in ("tool_calls", "tool_use"): - tool_calls: list[ToolCallRequest] = [] + tool_calls: list[ToolCallRequestPart] = [] for tool_call in chat_generation.message.tool_calls: - tool_call_request = ToolCallRequest( + tool_call_request = ToolCallRequestPart( name=tool_call["name"], id=tool_call["id"], arguments=tool_call["args"], @@ -402,7 +402,7 @@ def on_llm_end( ) else: parts = [ - Text( + TextPart( content=chat_generation.message.content, type="text", ) diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py index ef726f1ac..96116d0e1 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py @@ -23,10 +23,10 @@ InputMessage, MessagePart, OutputMessage, - Reasoning, - Text, - ToolCallRequest, - ToolCallResponse, + ReasoningPart, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ToolDefinition, ) @@ -89,18 +89,18 @@ def _content_to_parts( parts: list[MessagePart] = [] if isinstance(content, str): if content: - parts.append(Text(content=content)) + parts.append(TextPart(content=content)) return parts for item in content: if isinstance(item, str): if item: - parts.append(Text(content=item)) + parts.append(TextPart(content=item)) continue block_type = item.get("type") if block_type == "text": text_value = item.get("text") if isinstance(text_value, str) and text_value: - parts.append(Text(content=text_value)) + parts.append(TextPart(content=text_value)) elif block_type in ("thinking", "reasoning"): reasoning_value = ( item.get("thinking") @@ -108,13 +108,13 @@ def _content_to_parts( or item.get("text") ) if isinstance(reasoning_value, str) and reasoning_value: - parts.append(Reasoning(content=reasoning_value)) + parts.append(ReasoningPart(content=reasoning_value)) return parts def _legacy_function_call_request( message: AIMessage, -) -> ToolCallRequest | None: +) -> ToolCallRequestPart | None: """Extract a legacy OpenAI ``function_call`` as a :class:`ToolCallRequest`. Pre-tools OpenAI models return a single call under @@ -136,7 +136,7 @@ def _legacy_function_call_request( arguments = json.loads(raw_arguments) except (json.JSONDecodeError, ValueError): arguments = raw_arguments - return ToolCallRequest(arguments=arguments, name=name, id=None) + return ToolCallRequestPart(arguments=arguments, name=name, id=None) def _ai_message_parts(message: AIMessage) -> list[MessagePart]: @@ -152,7 +152,7 @@ def _ai_message_parts(message: AIMessage) -> list[MessagePart]: if not name: continue parts.append( - ToolCallRequest( + ToolCallRequestPart( arguments=call["args"], name=name, id=call["id"], @@ -169,7 +169,7 @@ def _tool_message_parts(message: ToolMessage) -> list[MessagePart]: """Build :class:`MessagePart` s for a :class:`ToolMessage` (tool result).""" tool_call_id = getattr(message, "tool_call_id", None) return [ - ToolCallResponse( + ToolCallResponsePart( response=message.content, id=tool_call_id if isinstance(tool_call_id, str) else None, ) @@ -318,7 +318,7 @@ def make_input_message(data: Any) -> list[InputMessage]: if input_data: serialized = serialize(input_data) if serialized: - return [InputMessage(role="user", parts=[Text(serialized)])] + return [InputMessage(role="user", parts=[TextPart(serialized)])] return [] diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py index 1632c21f3..0d0840384 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py @@ -38,8 +38,8 @@ from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, - Text, - ToolCallRequest, + TextPart, + ToolCallRequestPart, ) # --------------------------------------------------------------------------- @@ -645,7 +645,7 @@ def test_messages_key_with_human_message(self): assert isinstance(result[0], InputMessage) assert result[0].role == "user" assert len(result[0].parts) == 1 - assert isinstance(result[0].parts[0], Text) + assert isinstance(result[0].parts[0], TextPart) assert result[0].parts[0].content == "Hello" def test_messages_key_skips_empty_content(self): @@ -1192,7 +1192,7 @@ def test_openai_tool_calls_finish_reason_produces_tool_call_request(self): assert assigned[0].finish_reason == "tool_calls" assert len(assigned[0].parts) == 1 part = assigned[0].parts[0] - assert isinstance(part, ToolCallRequest) + assert isinstance(part, ToolCallRequestPart) assert part.name == "get_weather" assert part.id == "call_123" assert part.arguments == {"location": "Paris"} @@ -1223,7 +1223,7 @@ def test_bedrock_tool_use_finish_reason_produces_tool_call_request(self): assert assigned[0].finish_reason == "tool_use" assert len(assigned[0].parts) == 1 part = assigned[0].parts[0] - assert isinstance(part, ToolCallRequest) + assert isinstance(part, ToolCallRequestPart) assert part.name == "get_weather" assert part.id == "tooluse_abc" assert part.arguments == {"location": "London"} @@ -1586,7 +1586,7 @@ def test_legacy_function_call_finish_reason_produces_tool_call_request( assert len(assigned) == 1 assert len(assigned[0].parts) == 1 part = assigned[0].parts[0] - assert isinstance(part, ToolCallRequest) + assert isinstance(part, ToolCallRequestPart) assert part.name == "get_weather" assert part.arguments == {"city": "Paris"} @@ -1607,7 +1607,7 @@ def test_legacy_function_call_dict_arguments(): }, ) call = _legacy_function_call_request(message) - assert isinstance(call, ToolCallRequest) + assert isinstance(call, ToolCallRequestPart) assert call.name == "get_weather" assert call.arguments == {"city": "New York"} @@ -1623,7 +1623,7 @@ def test_legacy_function_call_string_arguments_parsed(): }, ) call = _legacy_function_call_request(message) - assert isinstance(call, ToolCallRequest) + assert isinstance(call, ToolCallRequestPart) assert call.arguments == {"city": "New York"} @@ -1641,7 +1641,7 @@ def test_to_input_messages_includes_legacy_function_call(): messages = to_input_messages([message]) assert len(messages) == 1 assert any( - isinstance(p, ToolCallRequest) and p.name == "f" + isinstance(p, ToolCallRequestPart) and p.name == "f" for p in messages[0].parts ) diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/chat_wrappers.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/chat_wrappers.py index 31954d0c7..872977389 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/chat_wrappers.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/chat_wrappers.py @@ -19,8 +19,8 @@ ) from opentelemetry.util.genai.types import ( OutputMessage, - Text, - ToolCallRequest, + TextPart, + ToolCallRequestPart, ) from .chat_buffers import ChoiceBuffer @@ -123,7 +123,7 @@ def _set_output_messages(self) -> None: ) if choice.text_content: message.parts.append( - Text(content="".join(choice.text_content)) + TextPart(content="".join(choice.text_content)) ) if choice.tool_calls_buffers: tool_calls = [] @@ -135,7 +135,7 @@ def _set_output_messages(self) -> None: arguments = json.loads(arguments_str) except json.JSONDecodeError: arguments = arguments_str - tool_call_part = ToolCallRequest( + tool_call_part = ToolCallRequestPart( name=tool_call.function_name, id=tool_call.tool_call_id, arguments=arguments, diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py index 7a495b4dd..6a9029319 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py @@ -31,7 +31,7 @@ Error, InputMessage, OutputMessage, - Text, + TextPart, ToolDefinition, ) @@ -67,11 +67,11 @@ GenericToolDefinition, InputMessage, OutputMessage, - Reasoning, - Text, + ReasoningPart, + TextPart, ) from opentelemetry.util.genai.types import ( - ToolCallRequest as ToolCall, + ToolCallRequestPart as ToolCall, ) except ImportError: Error = None @@ -79,8 +79,8 @@ GenericToolDefinition = None InputMessage = None OutputMessage = None - Reasoning = None - Text = None + ReasoningPart = None + TextPart = None ToolCall = None @@ -180,20 +180,20 @@ def extract_params( ) -def get_system_instruction(instructions: str | None) -> list[Text]: - if Text is None or instructions is None: +def get_system_instruction(instructions: str | None) -> list[TextPart]: + if TextPart is None or instructions is None: return [] - return [Text(content=instructions)] + return [TextPart(content=instructions)] def get_input_messages( input_value: str | Sequence[object] | None, ) -> list[InputMessage]: - if InputMessage is None or Text is None: + if InputMessage is None or TextPart is None: return [] if isinstance(input_value, str): - return [InputMessage(role="user", parts=[Text(content=input_value)])] + return [InputMessage(role="user", parts=[TextPart(content=input_value)])] messages: list[InputMessage] = [] for item in _get_sequence(input_value): @@ -204,7 +204,7 @@ def get_input_messages( content = _get_field(item, "content") if isinstance(content, str): messages.append( - InputMessage(role=role, parts=[Text(content=content)]) + InputMessage(role=role, parts=[TextPart(content=content)]) ) continue @@ -212,27 +212,27 @@ def get_input_messages( for part in _get_sequence(content): text = _get_field(part, "text") if isinstance(text, str): - parts.append(Text(content=text)) + parts.append(TextPart(content=text)) if parts: messages.append(InputMessage(role=role, parts=parts)) return messages -def _extract_output_parts(content_blocks: Sequence[object]) -> list[Text]: +def _extract_output_parts(content_blocks: Sequence[object]) -> list[TextPart]: if ( - Text is None + TextPart is None or ResponseOutputText is None or ResponseOutputRefusal is None ): return [] - parts: list[Text] = [] + parts: list[TextPart] = [] for block in content_blocks: if isinstance(block, ResponseOutputText): - parts.append(Text(content=block.text)) + parts.append(TextPart(content=block.text)) elif isinstance(block, ResponseOutputRefusal): - parts.append(Text(content=block.refusal)) + parts.append(TextPart(content=block.refusal)) return parts @@ -248,19 +248,19 @@ def _parse_tool_call_arguments(arguments: str | None) -> object: def _extract_reasoning_parts( item: ResponseReasoningItem, -) -> list[Reasoning]: - if Reasoning is None: +) -> list[ReasoningPart]: + if ReasoningPart is None: return [] - parts: list[Reasoning] = [] + parts: list[ReasoningPart] = [] for block in item.summary: if isinstance(block.text, str): - parts.append(Reasoning(content=block.text)) + parts.append(ReasoningPart(content=block.text)) for block in item.content or []: if getattr(block, "type", None) == "reasoning_text" and isinstance( getattr(block, "text", None), str ): - parts.append(Reasoning(content=block.text)) + parts.append(ReasoningPart(content=block.text)) return parts @@ -352,7 +352,7 @@ def get_output_messages_from_response( not _response_types_available() or not isinstance(response, Response) or OutputMessage is None - or Text is None + or TextPart is None ): return [] diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py index 76b3ae603..bce02b97c 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py @@ -25,9 +25,9 @@ FunctionToolDefinition, InputMessage, OutputMessage, - Text, - ToolCallRequest, - ToolCallResponse, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ToolDefinition, ) @@ -205,22 +205,22 @@ def _prepare_input_messages(messages) -> list[InputMessage]: if tool_calls: chat_message.parts += extract_tool_calls_new(tool_calls) if _is_text_part(content): - chat_message.parts.append(Text(content=str(content))) + chat_message.parts.append(TextPart(content=str(content))) elif role == "tool": tool_call_id = get_property_value(message, "tool_call_id") chat_message.parts.append( - ToolCallResponse(id=tool_call_id, response=content) + ToolCallResponsePart(id=tool_call_id, response=content) ) else: # system, developer, user, fallback if _is_text_part(content): - chat_message.parts.append(Text(content=str(content))) + chat_message.parts.append(TextPart(content=str(content))) return chat_messages -def extract_tool_calls_new(tool_calls) -> list[ToolCallRequest]: +def extract_tool_calls_new(tool_calls) -> list[ToolCallRequestPart]: parts = [] for tool_call in tool_calls: call_id = get_property_value(tool_call, "id") @@ -239,7 +239,7 @@ def extract_tool_calls_new(tool_calls) -> list[ToolCallRequest]: # TODO: support custom parts.append( - ToolCallRequest(id=call_id, name=func_name, arguments=arguments) + ToolCallRequestPart(id=call_id, name=func_name, arguments=arguments) ) return parts @@ -274,7 +274,7 @@ def _prepare_output_messages(choices) -> list[OutputMessage]: parts += extract_tool_calls_new(tool_calls) content = get_property_value(choice.message, "content") if _is_text_part(content): - parts.append(Text(content=str(content))) + parts.append(TextPart(content=str(content))) message = OutputMessage( finish_reason=choice.finish_reason or "error", diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_extractors.py b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_extractors.py index 9faebeeda..3549fc7b1 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_extractors.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_extractors.py @@ -323,7 +323,7 @@ def test_extract_output_type_handles_text_format_mapping(loaded_module): def test_extractors_handle_missing_genai_types_import(loaded_module): with ( - mock.patch.object(loaded_module, "Text", None), + mock.patch.object(loaded_module, "TextPart", None), mock.patch.object(loaded_module, "InputMessage", None), mock.patch.object(loaded_module, "OutputMessage", None), ): diff --git a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/src/opentelemetry/instrumentation/genai/qwen_agent/utils.py b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/src/opentelemetry/instrumentation/genai/qwen_agent/utils.py index aad938d3a..6936e8071 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/src/opentelemetry/instrumentation/genai/qwen_agent/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/src/opentelemetry/instrumentation/genai/qwen_agent/utils.py @@ -16,9 +16,9 @@ InputMessage, MessagePart, OutputMessage, - Text, - ToolCallRequest, - ToolCallResponse, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ) if TYPE_CHECKING: @@ -116,7 +116,7 @@ def find_tool_call_id( return None -def _function_call_part(function_call: Any) -> ToolCallRequest: +def _function_call_part(function_call: Any) -> ToolCallRequestPart: name = _field_value(function_call, "name") or "" arguments = _field_value(function_call, "arguments") or "{}" if isinstance(arguments, str): @@ -124,7 +124,7 @@ def _function_call_part(function_call: Any) -> ToolCallRequest: arguments = json.loads(arguments) except (json.JSONDecodeError, ValueError): pass - return ToolCallRequest(name=name, arguments=arguments, id=None) + return ToolCallRequestPart(name=name, arguments=arguments, id=None) def _tool_call_response_id(msg: Any) -> str: @@ -165,7 +165,7 @@ def convert_to_input_messages( # API converts it to role="tool"; handle both. if role in ("function", "tool") and content: parts.append( - ToolCallResponse( + ToolCallResponsePart( id=_tool_call_response_id(msg), response=_extract_content_text(content), ) @@ -173,7 +173,7 @@ def convert_to_input_messages( elif content: text = _extract_content_text(content) if text: - parts.append(Text(content=text)) + parts.append(TextPart(content=text)) if parts: input_messages.append(InputMessage(role=role, parts=parts)) @@ -210,10 +210,10 @@ def convert_to_output_messages( if content: text = _extract_content_text(content) if text: - parts.append(Text(content=text)) + parts.append(TextPart(content=text)) if not parts: - parts.append(Text(content="")) + parts.append(TextPart(content="")) output_messages.append( OutputMessage( @@ -293,6 +293,6 @@ def create_agent_invocation( # qwen-agent prepends to the LLM messages on every run. system_message = getattr(agent_instance, "system_message", None) if system_message: - invocation.system_instruction = [Text(content=system_message)] + invocation.system_instruction = [TextPart(content=system_message)] return invocation diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py index 17c93cbe8..ba6b8cf2e 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py @@ -99,11 +99,11 @@ class Stream: GenericToolDefinition, InputMessage, OutputMessage, - Text, - ToolCallRequest, - ToolCallResponse, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, ToolDefinition, - Uri, + UriPart, ) @@ -174,7 +174,7 @@ def _interactions_input_to_messages( if input_data is None: return [] if isinstance(input_data, str): - return [InputMessage(role="user", parts=[Text(content=input_data)])] + return [InputMessage(role="user", parts=[TextPart(content=input_data)])] if not isinstance(input_data, Sequence): input_data = [input_data] @@ -186,22 +186,22 @@ def _interactions_input_to_messages( call_id = _get_field(item, "id") name = _get_field(item, "name") arguments = _get_field(item, "arguments") - part = ToolCallRequest( + part = ToolCallRequestPart( id=call_id, name=name or "", arguments=arguments ) parts.append(part) elif item_type == "function_result": call_id = _get_field(item, "call_id") result = _get_field(item, "result") - part = ToolCallResponse(id=call_id, response=result) + part = ToolCallResponsePart(id=call_id, response=result) parts.append(part) elif isinstance(item, str): - parts.append(Text(content=item)) + parts.append(TextPart(content=item)) elif item_type == "text": - part = Text(content=_get_field(item, "text") or "") + part = TextPart(content=_get_field(item, "text") or "") parts.append(part) elif item_type == "document": - part = Uri( + part = UriPart( mime_type=_get_field(item, "mime_type"), modality="document", uri=_get_field(item, "uri") or "", @@ -244,7 +244,7 @@ def _interactions_response_to_messages( return [ OutputMessage( role="assistant", - parts=[Text(content=output_text)], + parts=[TextPart(content=output_text)], finish_reason="stop", ) ] @@ -406,7 +406,7 @@ def _start_interactions_invocation( kwargs.get("input") ) if system_instruction := kwargs.get("system_instruction"): - invocation.system_instruction = [Text(content=system_instruction)] + invocation.system_instruction = [TextPart(content=system_instruction)] return invocation diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py index 865215474..001b466db 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py @@ -9,15 +9,15 @@ from google.genai import types as genai_types from opentelemetry.util.genai.types import ( - Blob, + BlobPart, FinishReason, InputMessage, MessagePart, OutputMessage, - Text, - ToolCallRequest, - ToolCallResponse, - Uri, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, + UriPart, ) @@ -91,12 +91,12 @@ def tool_call_id(name: str | None) -> str: return f"{idx}" if (text := part.text) is not None: - return Text(content=text) + return TextPart(content=text) if inline_data := part.inline_data: mime_type = inline_data.mime_type or "" modality = mime_type.split("/")[0] if mime_type else "" - return Blob( + return BlobPart( mime_type=mime_type, modality=modality, content=inline_data.data or b"", @@ -105,21 +105,21 @@ def tool_call_id(name: str | None) -> str: if file_data := part.file_data: mime_type = file_data.mime_type or "" modality = mime_type.split("/")[0] if mime_type else "" - return Uri( + return UriPart( mime_type=mime_type, modality=modality, uri=file_data.file_uri or "", ) if call := part.function_call: - return ToolCallRequest( + return ToolCallRequestPart( id=call.id or tool_call_id(call.name), name=call.name or "", arguments=call.args, ) if response := part.function_response: - return ToolCallResponse( + return ToolCallResponsePart( id=response.id or tool_call_id(response.name), response=response.response, ) diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py b/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py index 1625a2ea2..0587fc290 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/interactions/test_parser.py @@ -13,10 +13,10 @@ ) from opentelemetry.util.genai.types import ( GenericPart, - Text, - ToolCallRequest, - ToolCallResponse, - Uri, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, + UriPart, ) @@ -32,22 +32,22 @@ def test_input_to_messages_str(self) -> None: messages = _interactions_input_to_messages("Hello world") self.assertEqual(messages[0].role, "user") self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], Text) + self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Hello world") def test_input_to_messages_list_of_strings(self) -> None: messages = _interactions_input_to_messages(["Hello", "world"]) self.assertEqual(len(messages[0].parts), 2) - self.assertIsInstance(messages[0].parts[0], Text) + self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Hello") - self.assertIsInstance(messages[0].parts[1], Text) + self.assertIsInstance(messages[0].parts[1], TextPart) self.assertEqual(messages[0].parts[1].content, "world") def test_input_to_messages_text_step(self) -> None: steps = [{"type": "text", "text": "Hello text step"}] messages = _interactions_input_to_messages(steps) self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], Text) + self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Hello text step") def test_input_to_messages_document_step(self) -> None: @@ -60,7 +60,7 @@ def test_input_to_messages_document_step(self) -> None: ] messages = _interactions_input_to_messages(steps) self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], Uri) + self.assertIsInstance(messages[0].parts[0], UriPart) self.assertEqual(messages[0].parts[0].mime_type, "application/pdf") self.assertEqual(messages[0].parts[0].modality, "document") self.assertEqual( @@ -78,7 +78,7 @@ def test_input_to_messages_tool_call_step(self) -> None: ] messages = _interactions_input_to_messages(steps) self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], ToolCallRequest) + self.assertIsInstance(messages[0].parts[0], ToolCallRequestPart) self.assertEqual(messages[0].parts[0].id, "call-123") self.assertEqual(messages[0].parts[0].name, "calc") self.assertEqual(messages[0].parts[0].arguments, {"x": 5}) @@ -93,7 +93,7 @@ def test_input_to_messages_tool_result_step(self) -> None: ] messages = _interactions_input_to_messages(steps) self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], ToolCallResponse) + self.assertIsInstance(messages[0].parts[0], ToolCallResponsePart) self.assertEqual(messages[0].parts[0].id, "call-123") self.assertEqual(messages[0].parts[0].response, {"val": 10}) @@ -109,7 +109,7 @@ def test_input_to_messages_single_non_sequence_step(self) -> None: step = {"type": "text", "text": "Hello single step"} messages = _interactions_input_to_messages(step) self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], Text) + self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Hello single step") def test_input_to_messages_none_type_fall_through(self) -> None: @@ -126,5 +126,5 @@ def test_response_to_messages(self) -> None: self.assertEqual(messages[0].role, "assistant") self.assertEqual(messages[0].finish_reason, "stop") self.assertEqual(len(messages[0].parts), 1) - self.assertIsInstance(messages[0].parts[0], Text) + self.assertIsInstance(messages[0].parts[0], TextPart) self.assertEqual(messages[0].parts[0].content, "Model response text") diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py index 3bde5f899..9900cf13d 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py @@ -83,7 +83,7 @@ def is_message_part_list_hashable( message_parts: list[types.MessagePart] | None, ) -> bool: return bool(message_parts) and all( - isinstance(x, types.Text) for x in message_parts + isinstance(x, types.TextPart) for x in message_parts ) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py index d3b751cbb..64e855ddc 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -50,7 +50,7 @@ class GenericPart: @dataclass() -class ToolCallRequest: +class ToolCallRequestPart: """Represents a tool call requested by the model (message part only). Use this for tool calls in message history. For execution tracking with spans @@ -67,7 +67,7 @@ class ToolCallRequest: @dataclass() -class ToolCallResponse: +class ToolCallResponsePart: """Represents a tool call result sent to the model or a built-in tool call outcome and details This model is specified as part of semconv in `GenAI messages Python models - ToolCallResponsePart @@ -80,7 +80,7 @@ class ToolCallResponse: @dataclass() -class ServerToolCall: +class ServerToolCallPart: """Represents a server-side tool call. Server tool calls are executed by the model provider on the server side rather @@ -98,7 +98,7 @@ class ServerToolCall: @dataclass() -class ServerToolCallResponse: +class ServerToolCallResponsePart: """Represents a server-side tool call response. Contains the outcome and details of a server tool execution. Provider-specific @@ -115,7 +115,7 @@ class ServerToolCallResponse: @dataclass() -class Text: +class TextPart: """Represents text content sent to or received from the model This model is specified as part of semconv in `GenAI messages Python models - TextPart @@ -127,7 +127,7 @@ class Text: @dataclass() -class Reasoning: +class ReasoningPart: """Represents reasoning/thinking content received from the model This model is specified as part of semconv in `GenAI messages Python models - ReasoningPart @@ -161,7 +161,7 @@ class CompactionPart: @dataclass() -class Blob: +class BlobPart: """Represents blob binary data sent inline to the model This model is specified as part of semconv in `GenAI messages Python models - BlobPart @@ -175,7 +175,7 @@ class Blob: @dataclass() -class File: +class FilePart: """Represents an external referenced file sent to the model by file id This model is specified as part of semconv in `GenAI messages Python models - FilePart @@ -189,7 +189,7 @@ class File: @dataclass() -class Uri: +class UriPart: """Represents an external referenced file sent to the model by URI This model is specified as part of semconv in `GenAI messages Python models - UriPart @@ -223,15 +223,15 @@ class GenericToolDefinition: ToolDefinition = Union[FunctionToolDefinition, GenericToolDefinition] MessagePart = Union[ - Text, - ToolCallRequest, - ToolCallResponse, - ServerToolCall, - ServerToolCallResponse, - Blob, - File, - Uri, - Reasoning, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, + ServerToolCallPart, + ServerToolCallResponsePart, + BlobPart, + FilePart, + UriPart, + ReasoningPart, CompactionPart, GenericPart, # For provider-specific types; prefer standard types above ] diff --git a/util/opentelemetry-util-genai/tests/test_handler_agent.py b/util/opentelemetry-util-genai/tests/test_handler_agent.py index 49b221e2c..42d882b39 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_agent.py +++ b/util/opentelemetry-util-genai/tests/test_handler_agent.py @@ -25,7 +25,7 @@ FunctionToolDefinition, InputMessage, OutputMessage, - Text, + TextPart, ) @@ -196,12 +196,12 @@ def test_default_values(self): def test_with_messages(self): invocation = self.handler.invoke_local_agent() invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content="Hello")]) + InputMessage(role="user", parts=[TextPart(content="Hello")]) ] invocation.output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="Hi there!")], + parts=[TextPart(content="Hi there!")], finish_reason="stop", ) ] @@ -331,7 +331,7 @@ def setUp(self): def test_system_instruction_on_span(self, _mock_cap): invocation = self.handler.invoke_local_agent() invocation.system_instruction = [ - Text(content="You are a helpful assistant."), + TextPart(content="You are a helpful assistant."), ] invocation.stop() @@ -362,12 +362,12 @@ def test_tool_definitions_on_span(self, _mock_cap): def test_messages_on_span(self, _mock_cap): invocation = self.handler.invoke_local_agent() invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content="Hello")]) + InputMessage(role="user", parts=[TextPart(content="Hello")]) ] invocation.output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="Hi!")], + parts=[TextPart(content="Hi!")], finish_reason="stop", ) ] @@ -380,10 +380,10 @@ def test_messages_on_span(self, _mock_cap): def test_content_not_on_span_by_default(self): invocation = self.handler.invoke_local_agent() invocation.system_instruction = [ - Text(content="You are a helpful assistant."), + TextPart(content="You are a helpful assistant."), ] invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content="Hello")]) + InputMessage(role="user", parts=[TextPart(content="Hello")]) ] invocation.stop() diff --git a/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py b/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py index bc4766c0c..dc276d56e 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py +++ b/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py @@ -20,7 +20,7 @@ FunctionToolDefinition, InputMessage, OutputMessage, - Text, + TextPart, ) _CAPTURE_EVENT_ENV = { @@ -48,16 +48,16 @@ def test_hook_called_on_stop(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[Text(content="hello")]) + InputMessage(role="user", parts=[TextPart(content="hello")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="hi")], + parts=[TextPart(content="hi")], finish_reason="stop", ) ] - system_instruction = [Text(content="be helpful")] + system_instruction = [TextPart(content="be helpful")] tool_definitions = [ FunctionToolDefinition( name="get_weather", @@ -86,7 +86,7 @@ def test_hook_called_on_fail(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[Text(content="hello")]) + InputMessage(role="user", parts=[TextPart(content="hello")]) ] invocation = handler.inference("openai", request_model="gpt-4o") @@ -158,12 +158,12 @@ def test_workflow_hook_called_on_stop_with_messages(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[Text(content="what is 2+2?")]) + InputMessage(role="user", parts=[TextPart(content="what is 2+2?")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="4")], + parts=[TextPart(content="4")], finish_reason="stop", ) ] @@ -189,7 +189,7 @@ def test_workflow_hook_called_on_fail(self): invocation = handler.workflow(name="my-workflow") invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content="hello")]) + InputMessage(role="user", parts=[TextPart(content="hello")]) ] invocation.fail(RuntimeError("workflow failed")) @@ -213,16 +213,16 @@ def test_local_agent_hook_called_on_stop_with_messages(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[Text(content="what is 2+2?")]) + InputMessage(role="user", parts=[TextPart(content="what is 2+2?")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="4")], + parts=[TextPart(content="4")], finish_reason="stop", ) ] - system_instruction = [Text(content="be helpful")] + system_instruction = [TextPart(content="be helpful")] tool_definitions = [ FunctionToolDefinition( name="get_weather", @@ -255,7 +255,7 @@ def test_local_agent_hook_called_on_fail(self): invocation = handler.invoke_local_agent(request_model="gpt-4") invocation.input_messages = [ - InputMessage(role="user", parts=[Text(content="hello")]) + InputMessage(role="user", parts=[TextPart(content="hello")]) ] invocation.fail(RuntimeError("agent failed")) @@ -268,12 +268,12 @@ def test_remote_agent_hook_called_on_stop_with_messages(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[Text(content="hi")]) + InputMessage(role="user", parts=[TextPart(content="hi")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="hello")], + parts=[TextPart(content="hello")], finish_reason="stop", ) ] diff --git a/util/opentelemetry-util-genai/tests/test_handler_workflow.py b/util/opentelemetry-util-genai/tests/test_handler_workflow.py index cf45f90cb..4fa96f4a3 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_workflow.py +++ b/util/opentelemetry-util-genai/tests/test_handler_workflow.py @@ -25,7 +25,7 @@ Error, InputMessage, OutputMessage, - Text, + TextPart, ) @@ -273,9 +273,9 @@ def test_workflow_context_manager_success_has_unset_status(self) -> None: self.assertEqual(spans[0].status.status_code, StatusCode.UNSET) def test_workflow_context_manager_with_messages(self) -> None: - inp = InputMessage(role="user", parts=[Text(content="hello")]) + inp = InputMessage(role="user", parts=[TextPart(content="hello")]) out = OutputMessage( - role="assistant", parts=[Text(content="hi")], finish_reason="stop" + role="assistant", parts=[TextPart(content="hi")], finish_reason="stop" ) with self.handler.workflow("msg_wf") as inv: inv.input_messages = [inp] diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index 47b3c6de3..5d880c749 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -26,9 +26,9 @@ from opentelemetry.util.genai.types import ( CompactionPart, InputMessage, - ServerToolCall, - ServerToolCallResponse, - ToolCallRequest, + ServerToolCallPart, + ServerToolCallResponsePart, + ToolCallRequestPart, ) from opentelemetry.util.genai.utils import gen_ai_json_dumps @@ -39,7 +39,7 @@ def _make_handler() -> TelemetryHandler: def test_toolcallrequest_is_message_part(): """ToolCallRequest is for message parts only""" - tcr = ToolCallRequest( + tcr = ToolCallRequestPart( arguments={"location": "Paris"}, name="get_weather", id="call_123" ) msg = InputMessage(role="user", parts=[tcr]) @@ -52,7 +52,7 @@ def test_toolcall_inherits_from_genaiinvocation(): tc = handler.tool("get_weather") tc.arguments = {"city": "Paris"} assert isinstance(tc, GenAIInvocation) - assert not isinstance(tc, ToolCallRequest) + assert not isinstance(tc, ToolCallRequestPart) tc.stop() @@ -67,12 +67,12 @@ def test_toolcall_has_attributes_dict(): def test_toolcallrequest_in_message_part_union(): """ToolCallRequest (not ToolInvocation) is the correct type for message parts""" - tc = ToolCallRequest( + tc = ToolCallRequestPart( name="get_weather", arguments={"city": "Paris"}, id="call_123" ) msg = InputMessage(role="assistant", parts=[tc]) assert len(msg.parts) == 1 - assert isinstance(msg.parts[0], ToolCallRequest) + assert isinstance(msg.parts[0], ToolCallRequestPart) assert not isinstance(msg.parts[0], GenAIInvocation) @@ -86,7 +86,7 @@ def test_toolcall_operation_name(): def test_server_tool_call_basic(): """ServerToolCall can be created with required fields""" - stc = ServerToolCall( + stc = ServerToolCallPart( name="code_interpreter", server_tool_call={"type": "code_interpreter", "code": "print(1)"}, ) @@ -101,7 +101,7 @@ def test_server_tool_call_basic(): def test_server_tool_call_with_id(): """ServerToolCall can have an optional id""" - stc = ServerToolCall( + stc = ServerToolCallPart( name="web_search", server_tool_call={"type": "web_search", "query": "weather"}, id="stc_001", @@ -111,7 +111,7 @@ def test_server_tool_call_with_id(): def test_server_tool_call_response_basic(): """ServerToolCallResponse can be created with required fields""" - stcr = ServerToolCallResponse( + stcr = ServerToolCallResponsePart( server_tool_call_response={ "type": "code_interpreter", "output": "1\n", @@ -127,18 +127,18 @@ def test_server_tool_call_response_basic(): def test_server_tool_call_in_message(): """ServerToolCall and ServerToolCallResponse work as MessageParts""" - stc = ServerToolCall( + stc = ServerToolCallPart( name="code_interpreter", server_tool_call={"type": "code_interpreter", "code": "x = 1"}, ) - stcr = ServerToolCallResponse( + stcr = ServerToolCallResponsePart( server_tool_call_response={"type": "code_interpreter", "output": ""}, id="stc_001", ) msg = InputMessage(role="assistant", parts=[stc, stcr]) assert len(msg.parts) == 2 - assert isinstance(msg.parts[0], ServerToolCall) - assert isinstance(msg.parts[1], ServerToolCallResponse) + assert isinstance(msg.parts[0], ServerToolCallPart) + assert isinstance(msg.parts[1], ServerToolCallResponsePart) def test_compactionpart_is_message_part(): diff --git a/util/opentelemetry-util-genai/tests/test_upload.py b/util/opentelemetry-util-genai/tests/test_upload.py index 10474645e..2234db4da 100644 --- a/util/opentelemetry-util-genai/tests/test_upload.py +++ b/util/opentelemetry-util-genai/tests/test_upload.py @@ -28,12 +28,12 @@ FAKE_INPUTS = [ types.InputMessage( role="user", - parts=[types.Text(content="What is the capital of France?")], + parts=[types.TextPart(content="What is the capital of France?")], ), types.InputMessage( role="assistant", parts=[ - types.ToolCallRequest( + types.ToolCallRequestPart( id="get_capital_0", name="get_capital", arguments={"city": "Paris"}, @@ -43,7 +43,7 @@ types.InputMessage( role="user", parts=[ - types.ToolCallResponse( + types.ToolCallResponsePart( id="get_capital_0", response={"capital": "Paris"} ) ], @@ -52,11 +52,11 @@ FAKE_OUTPUTS = [ types.OutputMessage( role="assistant", - parts=[types.Text(content="Paris")], + parts=[types.TextPart(content="Paris")], finish_reason="stop", ), ] -FAKE_SYSTEM_INSTRUCTION = [types.Text(content="You are a helpful assistant.")] +FAKE_SYSTEM_INSTRUCTION = [types.TextPart(content="You are a helpful assistant.")] FAKE_TOOL_DEFINITIONS: list[types.ToolDefinition] = [ types.FunctionToolDefinition( @@ -174,7 +174,7 @@ def test_lru_cache_works(self): self.hook.on_completion( inputs=[], outputs=[], - system_instruction=[types.Text(content=str(iteration))], + system_instruction=[types.TextPart(content=str(iteration))], tool_definitions=[], ) self.hook.shutdown() @@ -351,8 +351,8 @@ def test_system_insruction_is_hashed_to_avoid_reupload(self): # FIle should exist. self.assertTrue(self.hook._file_exists(expected_file_name)) system_instructions = [ - types.Text(content="You are a helpful assistant."), - types.Text(content="You will do your best."), + types.TextPart(content="You are a helpful assistant."), + types.TextPart(content="You will do your best."), ] record = LogRecord() self.hook.on_completion( @@ -480,7 +480,7 @@ def test_upload_bytes(self) -> None: types.InputMessage( role="user", parts=[ - types.Text(content="What is the capital of France?"), + types.TextPart(content="What is the capital of France?"), {"type": "generic_bytes", "bytes": b"hello"}, ], ) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index 127e9abae..92dd35068 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -38,7 +38,7 @@ InputMessage, MessagePart, OutputMessage, - Text, + TextPart, ) from opentelemetry.util.genai.utils import ( get_content_capturing_mode, @@ -50,21 +50,21 @@ def _create_input_message( content: str = "hello world", role: str = "Human" ) -> InputMessage: - return InputMessage(role=role, parts=[Text(content=content)]) + return InputMessage(role=role, parts=[TextPart(content=content)]) def _create_output_message( content: str = "hello back", finish_reason: str = "stop", role: str = "AI" ) -> OutputMessage: return OutputMessage( - role=role, parts=[Text(content=content)], finish_reason=finish_reason + role=role, parts=[TextPart(content=content)], finish_reason=finish_reason ) def _create_system_instruction( content: str = "You are a helpful assistant.", ) -> list[MessagePart]: - return [Text(content=content)] + return [TextPart(content=content)] def _get_single_span(span_exporter: InMemorySpanExporter) -> ReadableSpan: diff --git a/util/opentelemetry-util-genai/tests/test_workflow_invocation.py b/util/opentelemetry-util-genai/tests/test_workflow_invocation.py index 6ddfd30a6..b0d564b79 100644 --- a/util/opentelemetry-util-genai/tests/test_workflow_invocation.py +++ b/util/opentelemetry-util-genai/tests/test_workflow_invocation.py @@ -13,7 +13,7 @@ from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, - Text, + TextPart, ) @@ -42,7 +42,7 @@ def test_custom_name(self): assert invocation._name == "customer_support_pipeline" def test_with_input_messages(self): - msg = InputMessage(role="user", parts=[Text(content="hello")]) + msg = InputMessage(role="user", parts=[TextPart(content="hello")]) invocation = self.handler.workflow(name="test") invocation.input_messages = [msg] invocation.stop() @@ -51,7 +51,7 @@ def test_with_input_messages(self): def test_with_output_messages(self): msg = OutputMessage( - role="assistant", parts=[Text(content="hi")], finish_reason="stop" + role="assistant", parts=[TextPart(content="hi")], finish_reason="stop" ) invocation = self.handler.workflow(name="test") invocation.output_messages = [msg] @@ -85,10 +85,10 @@ def test_default_attributes_are_independent(self): inv2.stop() def test_full_construction(self): - inp = InputMessage(role="user", parts=[Text(content="query")]) + inp = InputMessage(role="user", parts=[TextPart(content="query")]) out = OutputMessage( role="assistant", - parts=[Text(content="answer")], + parts=[TextPart(content="answer")], finish_reason="stop", ) invocation = self.handler.workflow(name="my_workflow") From 3982e0b35f431cfb321a880fa1154fd9e7236c52 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 7 Aug 2026 08:39:15 -0700 Subject: [PATCH 02/16] Fix documentation files --- .../migrate-from-openinference/SKILL.md | 24 +++++++++---------- .../skills/write-conformance-tests/SKILL.md | 14 +++++------ .../instrumentation/genai/anthropic/utils.py | 2 +- .../instrumentation/genai/langchain/utils.py | 10 ++++---- .../tests/test_callback_handler.py | 6 ++--- util/opentelemetry-util-genai/README.rst | 2 +- .../util/genai/_tool_invocation.py | 2 +- .../tests/test_toolcall.py | 14 +++++------ 8 files changed, 37 insertions(+), 37 deletions(-) diff --git a/.github/skills/migrate-from-openinference/SKILL.md b/.github/skills/migrate-from-openinference/SKILL.md index 9703a0bed..a015e82d7 100644 --- a/.github/skills/migrate-from-openinference/SKILL.md +++ b/.github/skills/migrate-from-openinference/SKILL.md @@ -338,9 +338,9 @@ Drop every match. The mappings: `EmbeddingInvocation`, `ToolInvocation`, `WorkflowInvocation`, `AgentInvocation`, `Error`, `GenAIInvocation` - `opentelemetry.util.genai.types` — `InputMessage`, `OutputMessage`, - `Text`, `ToolCallRequest`, `ToolCallResponse`, `Reasoning`, - `ServerToolCall`, `ServerToolCallResponse`, `GenericPart`, `Blob`, - `File`, `Uri`, `Modality` + `TextPart`, `ToolCallRequestPart`, `ToolCallResponsePart`, `ReasoningPart`, + `ServerToolCallPart`, `ServerToolCallResponsePart`, `GenericPart`, `BlobPart`, + `FilePart`, `UriPart`, `Modality` - `opentelemetry.util.genai.completion_hook` - `opentelemetry.util.genai.environment_variables` @@ -401,15 +401,15 @@ right (all types from `opentelemetry.util.genai.types` unless noted): | Source request item | OTel construct | |---|---| -| User / assistant / system text message | `Input/OutputMessage(role=…, parts=[Text(content=…)])` | -| Assistant message containing a tool/function call | `Message(role="assistant", parts=[ToolCallRequest(name=…, id=…, arguments=…)])` | -| Tool/function result message | `Message(role="tool", parts=[ToolCallResponse(id=…, response=…)])` | -| Reasoning / thinking item | `Message(role="assistant", parts=[Reasoning(content=…)])` | -| Server-side tool call (web_search, file_search, code_interpreter, …) | `Message(parts=[ServerToolCall(name=…, server_tool_call=…, id=…)])` | -| Server-side tool call result | `Message(parts=[ServerToolCallResponse(server_tool_call_response=…, id=…)])` | -| Inline image / audio / video bytes | `Blob(mime_type=…, modality="image"\|"audio"\|"video", content=b"…")` | -| External media URL | `Uri(mime_type=…, modality=…, uri="…")` | -| File reference (e.g. OpenAI `file_id`) | `File(mime_type=…, modality=…, file_id="file-…")` | +| User / assistant / system text message | `Input/OutputMessage(role=…, parts=[TextPart(content=…)])` | +| Assistant message containing a tool/function call | `Message(role="assistant", parts=[ToolCallRequestPart(name=…, id=…, arguments=…)])` | +| Tool/function result message | `Message(role="tool", parts=[ToolCallResponsePart(id=…, response=…)])` | +| Reasoning / thinking item | `Message(role="assistant", parts=[ReasoningPart(content=…)])` | +| Server-side tool call (web_search, file_search, code_interpreter, …) | `Message(parts=[ServerToolCallPart(name=…, server_tool_call=…, id=…)])` | +| Server-side tool call result | `Message(parts=[ServerToolCallResponsePart(server_tool_call_response=…, id=…)])` | +| Inline image / audio / video bytes | `BlobPart(mime_type=…, modality="image"\|"audio"\|"video", content=b"…")` | +| External media URL | `UriPart(mime_type=…, modality=…, uri="…")` | +| File reference (e.g. OpenAI `file_id`) | `FilePart(mime_type=…, modality=…, file_id="file-…")` | | Provider-specific item with no semconv mapping | `GenericPart(value=…)` — never silently drop. Flag those in the review report. | Output messages mirror the input mapping — `OutputMessage` serializes with diff --git a/.github/skills/write-conformance-tests/SKILL.md b/.github/skills/write-conformance-tests/SKILL.md index 1c8360d7b..a6317daba 100644 --- a/.github/skills/write-conformance-tests/SKILL.md +++ b/.github/skills/write-conformance-tests/SKILL.md @@ -58,13 +58,13 @@ instruments: walk its wrappers (the step-6 mapping for a port) for which | Part `type` | util-genai type | Emitted when the library accepts… | |---|---|---| -| `text` | `Text` | plain text (always) | -| `tool_call` / `tool_call_response` | `ToolCallRequest` / `ToolCallResponse` | function/tool calling — covered by `tool_calling.py` | -| `server_tool_call` / `server_tool_call_response` | `ServerToolCall` / `ServerToolCallResponse` | vendor server-side tools (web_search, code_interpreter, …) | -| `reasoning` | `Reasoning` | reasoning / thinking items | -| `blob` | `Blob` | inline image/audio/video **bytes** (`modality` distinguishes them) | -| `uri` | `Uri` | an external media **URL** (`modality`) | -| `file` | `File` | a **file reference** / id (`modality`) | +| `text` | `TextPart` | plain text (always) | +| `tool_call` / `tool_call_response` | `ToolCallRequestPart` / `ToolCallResponsePart` | function/tool calling — covered by `tool_calling.py` | +| `server_tool_call` / `server_tool_call_response` | `ServerToolCallPart` / `ServerToolCallResponsePart` | vendor server-side tools (web_search, code_interpreter, …) | +| `reasoning` | `ReasoningPart` | reasoning / thinking items | +| `blob` | `BlobPart` | inline image/audio/video **bytes** (`modality` distinguishes them) | +| `uri` | `UriPart` | an external media **URL** (`modality`) | +| `file` | `FilePart` | a **file reference** / id (`modality`) | | `generic` | `GenericPart` | a provider item with no semconv mapping — flag, don't drop | Group by shared turn/cassette — typically one `multimodal.py` for the diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py b/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py index 7b6fa6682..6267b61d1 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py @@ -100,7 +100,7 @@ def _decode_base64(data: str) -> bytes | None: def _extract_base64_blob(source: object, modality: str) -> BlobPart | None: - """Extract a Blob from a base64-encoded source dict.""" + """Extract a BlobPart from a base64-encoded source dict.""" if not isinstance(source, dict): return None # source is a TypedDict (e.g. Base64ImageSourceParam) narrowed to dict; diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py index 96116d0e1..9a2ff5e7b 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py @@ -81,8 +81,8 @@ def _content_to_parts( """Convert a LangChain message ``content`` payload into ``MessagePart`` s. Content may be a plain string or a list of provider-specific block dicts - (e.g. Anthropic structured content). We extract :class:`Text` and - :class:`Reasoning` parts; ``tool_use`` blocks are intentionally ignored + (e.g. Anthropic structured content). We extract :class:`TextPart` and + :class:`ReasoningPart` parts; ``tool_use`` blocks are intentionally ignored here because LangChain consolidates them into ``message.tool_calls`` which is read separately. """ @@ -115,7 +115,7 @@ def _content_to_parts( def _legacy_function_call_request( message: AIMessage, ) -> ToolCallRequestPart | None: - """Extract a legacy OpenAI ``function_call`` as a :class:`ToolCallRequest`. + """Extract a legacy OpenAI ``function_call`` as a :class:`ToolCallRequestPart`. Pre-tools OpenAI models return a single call under ``additional_kwargs['function_call']`` (``{"name", "arguments"}``) rather @@ -143,7 +143,7 @@ def _ai_message_parts(message: AIMessage) -> list[MessagePart]: """Build :class:`MessagePart` s for an :class:`AIMessage`. Includes any text/reasoning content followed by a - :class:`ToolCallRequest` for each entry in ``message.tool_calls``, plus a + :class:`ToolCallRequestPart` for each entry in ``message.tool_calls``, plus a legacy ``additional_kwargs['function_call']`` when present. """ parts: list[MessagePart] = _content_to_parts(message.content) @@ -294,7 +294,7 @@ def make_input_message(data: Any) -> list[InputMessage]: When no ``messages`` key exists (common in LangGraph state dicts), the remaining state fields are serialized as JSON and emitted as a single - user-role :class:`Text` part. + user-role :class:`TextPart` part. """ if not isinstance(data, dict): return [] diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py index 0d0840384..8f753666b 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py @@ -1168,7 +1168,7 @@ def _make_handler_with_llm_invocation( class TestOnLlmEndToolCalls: def test_openai_tool_calls_finish_reason_produces_tool_call_request(self): - """finish_reason='tool_calls' (OpenAI) must produce ToolCallRequest parts.""" + """finish_reason='tool_calls' (OpenAI) must produce ToolCallRequestPart parts.""" run_id = _run_id() handler, _, llm_inv = _make_handler_with_llm_invocation(run_id) @@ -1198,7 +1198,7 @@ def test_openai_tool_calls_finish_reason_produces_tool_call_request(self): assert part.arguments == {"location": "Paris"} def test_bedrock_tool_use_finish_reason_produces_tool_call_request(self): - """finish_reason='tool_use' (Bedrock/Anthropic) must produce ToolCallRequest parts.""" + """finish_reason='tool_use' (Bedrock/Anthropic) must produce ToolCallRequestPart parts.""" run_id = _run_id() handler, _, llm_inv = _make_handler_with_llm_invocation(run_id) @@ -1561,7 +1561,7 @@ def test_extract_token_details_no_details_key(): def test_legacy_function_call_finish_reason_produces_tool_call_request( self, ): - """Pre-tools OpenAI ``function_call`` must surface as a ToolCallRequest.""" + """Pre-tools OpenAI ``function_call`` must surface as a ToolCallRequestPart.""" run_id = _run_id() handler, _, llm_inv = _make_handler_with_llm_invocation(run_id) diff --git a/util/opentelemetry-util-genai/README.rst b/util/opentelemetry-util-genai/README.rst index 0a9f8f971..850e6802d 100644 --- a/util/opentelemetry-util-genai/README.rst +++ b/util/opentelemetry-util-genai/README.rst @@ -10,7 +10,7 @@ Key Components -------------- - ``TelemetryHandler`` -- manages LLM invocation lifecycles (spans, metrics, events) -- ``InferenceInvocation`` and message types (``Text``, ``Reasoning``, ``Blob``, etc.) -- structured data model for GenAI interactions +- ``InferenceInvocation`` and message types (``TextPart``, ``ReasoningPart``, ``BlobPart``, etc.) -- structured data model for GenAI interactions - ``CompletionHook`` -- protocol for uploading content to external storage (built-in ``fsspec`` support) - Metrics -- ``gen_ai.client.operation.duration`` and ``gen_ai.client.token.usage`` histograms, plus the streaming timing histograms ``gen_ai.client.operation.time_to_first_chunk`` and diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py index b4f2a56c7..55e933348 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py @@ -33,7 +33,7 @@ def _any_value_to_attribute_value(value: AnyValue) -> AttributeValue | None: class ToolInvocation(GenAIInvocation): """Represents a tool call invocation for execute_tool span tracking. - Not used as a message part — use ToolCallRequest for that purpose. + Not used as a message part — use ToolCallRequestPart for that purpose. Use handler.tool(name) rather than constructing this directly. diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index 5d880c749..9dd32e5dd 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -1,7 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -"""Tests for ToolCallRequest and ToolInvocation inheritance structure""" +"""Tests for ToolCallRequestPart and ToolInvocation inheritance structure""" import os from unittest.mock import patch @@ -38,7 +38,7 @@ def _make_handler() -> TelemetryHandler: def test_toolcallrequest_is_message_part(): - """ToolCallRequest is for message parts only""" + """ToolCallRequestPart is for message parts only""" tcr = ToolCallRequestPart( arguments={"location": "Paris"}, name="get_weather", id="call_123" ) @@ -66,7 +66,7 @@ def test_toolcall_has_attributes_dict(): def test_toolcallrequest_in_message_part_union(): - """ToolCallRequest (not ToolInvocation) is the correct type for message parts""" + """ToolCallRequestPart (not ToolInvocation) is the correct type for message parts""" tc = ToolCallRequestPart( name="get_weather", arguments={"city": "Paris"}, id="call_123" ) @@ -85,7 +85,7 @@ def test_toolcall_operation_name(): def test_server_tool_call_basic(): - """ServerToolCall can be created with required fields""" + """ServerToolCallPart can be created with required fields""" stc = ServerToolCallPart( name="code_interpreter", server_tool_call={"type": "code_interpreter", "code": "print(1)"}, @@ -100,7 +100,7 @@ def test_server_tool_call_basic(): def test_server_tool_call_with_id(): - """ServerToolCall can have an optional id""" + """ServerToolCallPart can have an optional id""" stc = ServerToolCallPart( name="web_search", server_tool_call={"type": "web_search", "query": "weather"}, @@ -110,7 +110,7 @@ def test_server_tool_call_with_id(): def test_server_tool_call_response_basic(): - """ServerToolCallResponse can be created with required fields""" + """ServerToolCallResponsePart can be created with required fields""" stcr = ServerToolCallResponsePart( server_tool_call_response={ "type": "code_interpreter", @@ -126,7 +126,7 @@ def test_server_tool_call_response_basic(): def test_server_tool_call_in_message(): - """ServerToolCall and ServerToolCallResponse work as MessageParts""" + """ServerToolCallPart and ServerToolCallResponsePart work as MessageParts""" stc = ServerToolCallPart( name="code_interpreter", server_tool_call={"type": "code_interpreter", "code": "x = 1"}, From 4016a5ff7827f47c0275094e89493df2230857ea Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 7 Aug 2026 08:57:05 -0700 Subject: [PATCH 03/16] Fix formatting --- .../src/opentelemetry/instrumentation/genai/agno/patch.py | 4 +++- .../instrumentation/genai/anthropic/utils.py | 4 +++- .../instrumentation/genai/openai/response_extractors.py | 4 +++- .../opentelemetry/instrumentation/genai/openai/utils.py | 4 +++- .../instrumentation/google_genai/interactions.py | 8 ++++++-- .../tests/test_handler_workflow.py | 4 +++- util/opentelemetry-util-genai/tests/test_upload.py | 8 ++++++-- util/opentelemetry-util-genai/tests/test_utils.py | 4 +++- .../tests/test_workflow_invocation.py | 4 +++- 9 files changed, 33 insertions(+), 11 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py b/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py index 3431fd3ad..d5a02657c 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/src/opentelemetry/instrumentation/genai/agno/patch.py @@ -151,7 +151,9 @@ def _set_invocation_input( if input_val is not None: content_str = _extract_input_content(input_val) invocation.input_messages = [ - InputMessage(role="user", parts=[TextPart(content=content_str)]) + InputMessage( + role="user", parts=[TextPart(content=content_str)] + ) ] diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py b/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py index 6267b61d1..b4f9b3c23 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py @@ -145,7 +145,9 @@ def _convert_dict_block_to_part( if block_type in ("thinking", "redacted_thinking"): thinking = block.get("thinking") or block.get("data") - return ReasoningPart(content=str(thinking) if thinking is not None else "") + return ReasoningPart( + content=str(thinking) if thinking is not None else "" + ) if block_type in ("image", "audio", "video", "document", "file"): return _extract_base64_blob(block.get("source"), str(block_type)) diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py index 6a9029319..4fb5b870b 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_extractors.py @@ -193,7 +193,9 @@ def get_input_messages( return [] if isinstance(input_value, str): - return [InputMessage(role="user", parts=[TextPart(content=input_value)])] + return [ + InputMessage(role="user", parts=[TextPart(content=input_value)]) + ] messages: list[InputMessage] = [] for item in _get_sequence(input_value): diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py index bce02b97c..79997fd0c 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py @@ -239,7 +239,9 @@ def extract_tool_calls_new(tool_calls) -> list[ToolCallRequestPart]: # TODO: support custom parts.append( - ToolCallRequestPart(id=call_id, name=func_name, arguments=arguments) + ToolCallRequestPart( + id=call_id, name=func_name, arguments=arguments + ) ) return parts diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py index ba6b8cf2e..b5d9e5607 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/interactions.py @@ -174,7 +174,9 @@ def _interactions_input_to_messages( if input_data is None: return [] if isinstance(input_data, str): - return [InputMessage(role="user", parts=[TextPart(content=input_data)])] + return [ + InputMessage(role="user", parts=[TextPart(content=input_data)]) + ] if not isinstance(input_data, Sequence): input_data = [input_data] @@ -406,7 +408,9 @@ def _start_interactions_invocation( kwargs.get("input") ) if system_instruction := kwargs.get("system_instruction"): - invocation.system_instruction = [TextPart(content=system_instruction)] + invocation.system_instruction = [ + TextPart(content=system_instruction) + ] return invocation diff --git a/util/opentelemetry-util-genai/tests/test_handler_workflow.py b/util/opentelemetry-util-genai/tests/test_handler_workflow.py index 4fa96f4a3..7b701330d 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_workflow.py +++ b/util/opentelemetry-util-genai/tests/test_handler_workflow.py @@ -275,7 +275,9 @@ def test_workflow_context_manager_success_has_unset_status(self) -> None: def test_workflow_context_manager_with_messages(self) -> None: inp = InputMessage(role="user", parts=[TextPart(content="hello")]) out = OutputMessage( - role="assistant", parts=[TextPart(content="hi")], finish_reason="stop" + role="assistant", + parts=[TextPart(content="hi")], + finish_reason="stop", ) with self.handler.workflow("msg_wf") as inv: inv.input_messages = [inp] diff --git a/util/opentelemetry-util-genai/tests/test_upload.py b/util/opentelemetry-util-genai/tests/test_upload.py index 2234db4da..57f5d6b5c 100644 --- a/util/opentelemetry-util-genai/tests/test_upload.py +++ b/util/opentelemetry-util-genai/tests/test_upload.py @@ -56,7 +56,9 @@ finish_reason="stop", ), ] -FAKE_SYSTEM_INSTRUCTION = [types.TextPart(content="You are a helpful assistant.")] +FAKE_SYSTEM_INSTRUCTION = [ + types.TextPart(content="You are a helpful assistant.") +] FAKE_TOOL_DEFINITIONS: list[types.ToolDefinition] = [ types.FunctionToolDefinition( @@ -480,7 +482,9 @@ def test_upload_bytes(self) -> None: types.InputMessage( role="user", parts=[ - types.TextPart(content="What is the capital of France?"), + types.TextPart( + content="What is the capital of France?" + ), {"type": "generic_bytes", "bytes": b"hello"}, ], ) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index 92dd35068..a9f376fef 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -57,7 +57,9 @@ def _create_output_message( content: str = "hello back", finish_reason: str = "stop", role: str = "AI" ) -> OutputMessage: return OutputMessage( - role=role, parts=[TextPart(content=content)], finish_reason=finish_reason + role=role, + parts=[TextPart(content=content)], + finish_reason=finish_reason, ) diff --git a/util/opentelemetry-util-genai/tests/test_workflow_invocation.py b/util/opentelemetry-util-genai/tests/test_workflow_invocation.py index b0d564b79..bcfda5c38 100644 --- a/util/opentelemetry-util-genai/tests/test_workflow_invocation.py +++ b/util/opentelemetry-util-genai/tests/test_workflow_invocation.py @@ -51,7 +51,9 @@ def test_with_input_messages(self): def test_with_output_messages(self): msg = OutputMessage( - role="assistant", parts=[TextPart(content="hi")], finish_reason="stop" + role="assistant", + parts=[TextPart(content="hi")], + finish_reason="stop", ) invocation = self.handler.workflow(name="test") invocation.output_messages = [msg] From d1fc3c7fd4306d6a808edc30743748e69d7598e7 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 7 Aug 2026 09:05:32 -0700 Subject: [PATCH 04/16] Add CHANGELOG --- .../.changelog/365.changed | 1 + .../.changelog/365.changed | 1 + .../.changelog/365.changed | 1 + .../.changelog/365.changed | 1 + .../.changelog/365.changed | 1 + .../.changelog/365.changed | 1 + util/opentelemetry-util-genai/.changelog/365.changed | 1 + 7 files changed, 7 insertions(+) create mode 100644 instrumentation/opentelemetry-instrumentation-genai-agno/.changelog/365.changed create mode 100644 instrumentation/opentelemetry-instrumentation-genai-anthropic/.changelog/365.changed create mode 100644 instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/365.changed create mode 100644 instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/365.changed create mode 100644 instrumentation/opentelemetry-instrumentation-genai-qwen-agent/.changelog/365.changed create mode 100644 instrumentation/opentelemetry-instrumentation-google-genai/.changelog/365.changed create mode 100644 util/opentelemetry-util-genai/.changelog/365.changed diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-genai-agno/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-genai-anthropic/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/365.changed b/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/util/opentelemetry-util-genai/.changelog/365.changed b/util/opentelemetry-util-genai/.changelog/365.changed new file mode 100644 index 000000000..37887becb --- /dev/null +++ b/util/opentelemetry-util-genai/.changelog/365.changed @@ -0,0 +1 @@ +Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file From e9374439c2d4d4b85d6be79ec6fa33ec6919bfb2 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 7 Aug 2026 09:15:02 -0700 Subject: [PATCH 05/16] Fix tests --- .../opentelemetry-instrumentation-genai-agno/pyproject.toml | 2 +- .../tests/requirements.oldest.txt | 4 ++-- .../tests/test_handler_fetch_response.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/pyproject.toml b/instrumentation/opentelemetry-instrumentation-genai-agno/pyproject.toml index 059e13834..555ae7c71 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "opentelemetry-api ~= 1.43", "opentelemetry-instrumentation >= 0.64b0, <1", "opentelemetry-semantic-conventions >= 0.64b0, <1", - "opentelemetry-util-genai >= 1.0b0, <2", + "opentelemetry-util-genai >= 1.1b0.dev, <2", ] [project.optional-dependencies] diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt index 8eecc5b0c..5358b2194 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt @@ -21,5 +21,5 @@ # OpenTelemetry SDK and test utilities come transitively from opentelemetry-test-util-genai, which # every oldest env installs. Pin here only test-only deps that nothing else already provides. # -# There is currently nothing to pin: agno has no test-only dependency that isn't already -# provided by a declared bound or by the shared test fixtures. +# Drop this once opentelemetry-util-genai 1.1b0 is published. +-e util/opentelemetry-util-genai diff --git a/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py b/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py index e94facb66..7878912d3 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py +++ b/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py @@ -32,7 +32,7 @@ Error, FunctionToolDefinition, OutputMessage, - Text, + TextPart, ) # TODO: use the semconv constants once these attributes are released in @@ -274,12 +274,12 @@ def _fetch_with_content(self) -> None: invocation.output_messages = [ OutputMessage( role="assistant", - parts=[Text(content="This is a test.")], + parts=[TextPart(content="This is a test.")], finish_reason="stop", ) ] invocation.system_instruction = [ - Text(content="You are a helpful assistant.") + TextPart(content="You are a helpful assistant.") ] invocation.tool_definitions = [ FunctionToolDefinition( From b058de7f2fa8b3188edac893d781a250d7249996 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 17 Aug 2026 14:45:50 -0700 Subject: [PATCH 06/16] Add deprecation messages and backward compatible aliases --- .../.changelog/365.deprecated | 1 + .../src/opentelemetry/util/genai/types.py | 50 +++++++++++++++++++ .../tests/test_toolcall.py | 30 +++++++++++ .../tests/test_utils.py | 24 +++++++++ 4 files changed, 105 insertions(+) create mode 100644 util/opentelemetry-util-genai/.changelog/365.deprecated diff --git a/util/opentelemetry-util-genai/.changelog/365.deprecated b/util/opentelemetry-util-genai/.changelog/365.deprecated new file mode 100644 index 000000000..057509c72 --- /dev/null +++ b/util/opentelemetry-util-genai/.changelog/365.deprecated @@ -0,0 +1 @@ +Deprecate the message part classes `Text`, `Reasoning`, `Blob`, `File`, `Uri`, `ToolCallRequest`, `ToolCallResponse`, `ServerToolCall` and `ServerToolCallResponse`; they remain available as backward compatible aliases of their `*Part` replacements. diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py index 64e855ddc..9ec981020 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -202,6 +202,56 @@ class UriPart: type: Literal["uri"] = "uri" +# Deprecated aliases for the message part classes, keeping for backward compatability +# for users who import these classes directly. Delete +# this block once the deprecation period ends. + +Text = TextPart +""".. deprecated:: 1.0b0 + Use ``TextPart`` instead. +""" + +Reasoning = ReasoningPart +""".. deprecated:: 1.0b0 + Use ``ReasoningPart`` instead. +""" + +Blob = BlobPart +""".. deprecated:: 1.0b0 + Use ``BlobPart`` instead. +""" + +File = FilePart +""".. deprecated:: 1.0b0 + Use ``FilePart`` instead. +""" + +Uri = UriPart +""".. deprecated:: 1.0b0 + Use ``UriPart`` instead. +""" + +ToolCallRequest = ToolCallRequestPart +""".. deprecated:: 1.0b0 + Use ``ToolCallRequestPart`` instead. +""" + +ToolCallResponse = ToolCallResponsePart +""".. deprecated:: 1.0b0 + Use ``ToolCallResponsePart`` instead. +""" + +ServerToolCall = ServerToolCallPart +""".. deprecated:: 1.0b0 + Use ``ServerToolCallPart`` instead. +""" + +ServerToolCallResponse = ServerToolCallResponsePart +""".. deprecated:: 1.0b0 + Use ``ServerToolCallResponsePart`` instead. +""" + + @dataclass() class FunctionToolDefinition: """Represents a function tool definition sent to the model""" diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index 9dd32e5dd..4141d336b 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -26,9 +26,14 @@ from opentelemetry.util.genai.types import ( CompactionPart, InputMessage, + ServerToolCall, ServerToolCallPart, + ServerToolCallResponse, ServerToolCallResponsePart, + ToolCallRequest, ToolCallRequestPart, + ToolCallResponse, + ToolCallResponsePart, ) from opentelemetry.util.genai.utils import gen_ai_json_dumps @@ -155,6 +160,31 @@ def test_compactionpart_is_message_part(): assert isinstance(msg.parts[0], CompactionPart) +def test_deprecated_tool_call_aliases_resolve_to_part_classes(): + """The pre-*Part tool call names still resolve to their replacements.""" + assert ToolCallRequest is ToolCallRequestPart + assert ToolCallResponse is ToolCallResponsePart + assert ServerToolCall is ServerToolCallPart + assert ServerToolCallResponse is ServerToolCallResponsePart + + +def test_deprecated_tool_call_aliases_build_message_parts(): + """Parts built through the deprecated aliases are still valid MessageParts.""" + tcr = ToolCallRequest( + arguments={"location": "Paris"}, name="get_weather", id="call_123" + ) + stc = ServerToolCall( + name="code_interpreter", + server_tool_call={"type": "code_interpreter", "code": "x = 1"}, + ) + msg = InputMessage(role="assistant", parts=[tcr, stc]) + + assert isinstance(msg.parts[0], ToolCallRequestPart) + assert msg.parts[0].type == "tool_call" + assert isinstance(msg.parts[1], ServerToolCallPart) + assert msg.parts[1].type == "server_tool_call" + + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index a9f376fef..b7f86d898 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -34,11 +34,20 @@ get_telemetry_handler, ) from opentelemetry.util.genai.types import ( + Blob, + BlobPart, ContentCapturingMode, + File, + FilePart, InputMessage, MessagePart, OutputMessage, + Reasoning, + ReasoningPart, + Text, TextPart, + Uri, + UriPart, ) from opentelemetry.util.genai.utils import ( get_content_capturing_mode, @@ -138,6 +147,21 @@ def _normalize_to_dict(value: Any) -> dict[str, Any]: return dict(value) if isinstance(value, tuple) else value +class TestDeprecatedMessagePartAliases(unittest.TestCase): + def test_aliases_resolve_to_part_classes(self): + self.assertIs(Text, TextPart) + self.assertIs(Reasoning, ReasoningPart) + self.assertIs(Blob, BlobPart) + self.assertIs(File, FilePart) + self.assertIs(Uri, UriPart) + + def test_alias_builds_message_part(self): + message = InputMessage(role="user", parts=[Text(content="hello")]) + + self.assertIsInstance(message.parts[0], TextPart) + self.assertEqual(message.parts[0].type, "text") + + class TestShouldEmitEvent(unittest.TestCase): def test_should_emit_event_against_various_env_var_combinations( self, From 0c4cec078f80170ddabb498f72ae00d058552296 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 17 Aug 2026 14:48:00 -0700 Subject: [PATCH 07/16] Fix spelling --- .../src/opentelemetry/util/genai/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py index 9ec981020..1d58a9014 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -202,7 +202,7 @@ class UriPart: type: Literal["uri"] = "uri" -# Deprecated aliases for the message part classes, keeping for backward compatability +# Deprecated aliases for the message part classes, keeping for backward compatibility # for users who import these classes directly. Delete # this block once the deprecation period ends. From 5baf9ce9ba57eaf3bbb18ce352de002246912c8f Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 17 Aug 2026 14:54:19 -0700 Subject: [PATCH 08/16] Fix docs error --- .../src/opentelemetry/util/genai/types.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py index 1d58a9014..03f6eff0d 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -207,47 +207,56 @@ class UriPart: # this block once the deprecation period ends. Text = TextPart -""".. deprecated:: 1.0b0 +""" +.. deprecated:: 1.0b0 Use ``TextPart`` instead. """ Reasoning = ReasoningPart -""".. deprecated:: 1.0b0 +""" +.. deprecated:: 1.0b0 Use ``ReasoningPart`` instead. """ Blob = BlobPart -""".. deprecated:: 1.0b0 +""" +.. deprecated:: 1.0b0 Use ``BlobPart`` instead. """ File = FilePart -""".. deprecated:: 1.0b0 +""" +.. deprecated:: 1.0b0 Use ``FilePart`` instead. """ Uri = UriPart -""".. deprecated:: 1.0b0 +""" +.. deprecated:: 1.0b0 Use ``UriPart`` instead. """ ToolCallRequest = ToolCallRequestPart -""".. deprecated:: 1.0b0 +""" +.. deprecated:: 1.0b0 Use ``ToolCallRequestPart`` instead. """ ToolCallResponse = ToolCallResponsePart -""".. deprecated:: 1.0b0 +""" +.. deprecated:: 1.0b0 Use ``ToolCallResponsePart`` instead. """ ServerToolCall = ServerToolCallPart -""".. deprecated:: 1.0b0 +""" +.. deprecated:: 1.0b0 Use ``ServerToolCallPart`` instead. """ ServerToolCallResponse = ServerToolCallResponsePart -""".. deprecated:: 1.0b0 +""" +.. deprecated:: 1.0b0 Use ``ServerToolCallResponsePart`` instead. """ From 74e6389d015942ca06531c1b7ccbda96f0b556b4 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 21 Aug 2026 08:01:31 -0700 Subject: [PATCH 09/16] Remove the editable version --- .../tests/requirements.oldest.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt index 5358b2194..8eecc5b0c 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt @@ -21,5 +21,5 @@ # OpenTelemetry SDK and test utilities come transitively from opentelemetry-test-util-genai, which # every oldest env installs. Pin here only test-only deps that nothing else already provides. # -# Drop this once opentelemetry-util-genai 1.1b0 is published. --e util/opentelemetry-util-genai +# There is currently nothing to pin: agno has no test-only dependency that isn't already +# provided by a declared bound or by the shared test fixtures. From 754c8b3acff2e1955589788b842bd0820f705f91 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 21 Aug 2026 08:37:30 -0700 Subject: [PATCH 10/16] Add the editable version of utils to the oldest requirements --- .../tests/requirements.oldest.txt | 1 + .../tests/requirements.oldest.txt | 4 +++- .../tests/requirements.oldest.txt | 2 ++ .../tests/requirements.oldest.txt | 2 ++ .../tests/requirements.oldest.txt | 2 ++ 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt index 8880e70bf..71b48366b 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt @@ -22,3 +22,4 @@ # every oldest env installs. Pin here only test-only deps that nothing else already provides. # +-e util/opentelemetry-util-genai \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt index fc4c7d834..59100cce4 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt @@ -26,4 +26,6 @@ langchain-openai==0.2.0 langchain-aws==0.2.2 langchain-google-genai==2.0.0 langchain-anthropic==0.3.0 -boto3==1.37.0 \ No newline at end of file +boto3==1.37.0 + +-e util/opentelemetry-util-genai \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt index 1d7d06929..5693abad5 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt @@ -30,3 +30,5 @@ httpx==0.27.2 Deprecated==1.2.14 importlib-metadata==6.11.0 packaging==24.0 + +-e util/opentelemetry-util-genai \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt index 3fda1cd74..87c6bf820 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt @@ -32,3 +32,5 @@ tqdm==4.66.1 # Exercise the oldest supported wrapt major (1.17 is the floor inherited # from opentelemetry-util-genai). wrapt==1.17.0 + +-e util/opentelemetry-util-genai \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt index 34517fcdc..c53932041 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt @@ -24,3 +24,5 @@ fsspec==2025.9.0 + +-e util/opentelemetry-util-genai[upload] \ No newline at end of file From 2642bb353831f7374a7b04748e1e254ec9821431 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 21 Aug 2026 09:10:12 -0700 Subject: [PATCH 11/16] Fix agno tests --- .../tests/requirements.oldest.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt index 8eecc5b0c..b6c7d0cce 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt @@ -23,3 +23,5 @@ # # There is currently nothing to pin: agno has no test-only dependency that isn't already # provided by a declared bound or by the shared test fixtures. + +-e util/opentelemetry-util-genai \ No newline at end of file From e86600b9b8cf96d93abd937a359c0a16b9bfdfd6 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 21 Aug 2026 09:20:54 -0700 Subject: [PATCH 12/16] Fix requirements --- .../tests/requirements.oldest.txt | 3 ++- .../tests/requirements.oldest.txt | 3 ++- .../tests/requirements.oldest.txt | 3 ++- .../tests/requirements.oldest.txt | 3 ++- .../tests/requirements.oldest.txt | 3 ++- .../tests/requirements.oldest.txt | 1 + 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt index b6c7d0cce..77fa4af14 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt @@ -24,4 +24,5 @@ # There is currently nothing to pin: agno has no test-only dependency that isn't already # provided by a declared bound or by the shared test fixtures. --e util/opentelemetry-util-genai \ No newline at end of file +-e util/opentelemetry-util-genai +-e instrumentation/opentelemetry-instrumentation-genai-agno \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt index 71b48366b..5c161b832 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt @@ -22,4 +22,5 @@ # every oldest env installs. Pin here only test-only deps that nothing else already provides. # --e util/opentelemetry-util-genai \ No newline at end of file +-e util/opentelemetry-util-genai +-e instrumentation/opentelemetry-instrumentation-genai-anthropic \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt index 59100cce4..e4f219e40 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt @@ -28,4 +28,5 @@ langchain-google-genai==2.0.0 langchain-anthropic==0.3.0 boto3==1.37.0 --e util/opentelemetry-util-genai \ No newline at end of file +-e util/opentelemetry-util-genai +-e instrumentation/opentelemetry-instrumentation-genai-langchain[instruments] \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt index 5693abad5..647c83f70 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt @@ -31,4 +31,5 @@ Deprecated==1.2.14 importlib-metadata==6.11.0 packaging==24.0 --e util/opentelemetry-util-genai \ No newline at end of file +-e util/opentelemetry-util-genai +-e instrumentation/opentelemetry-instrumentation-genai-openai \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt index 87c6bf820..42eb6f7ff 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt @@ -33,4 +33,5 @@ tqdm==4.66.1 # from opentelemetry-util-genai). wrapt==1.17.0 --e util/opentelemetry-util-genai \ No newline at end of file +-e util/opentelemetry-util-genai +-e instrumentation/opentelemetry-instrumentation-genai-qwen-agent \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt index c53932041..495720a3f 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt @@ -25,4 +25,5 @@ fsspec==2025.9.0 +-e instrumentation/opentelemetry-instrumentation-google-genai -e util/opentelemetry-util-genai[upload] \ No newline at end of file From d3c698197970e1a63e60c852ff7c287bdabce83a Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 21 Aug 2026 09:39:16 -0700 Subject: [PATCH 13/16] Align with the conventions --- .../tests/requirements.oldest.txt | 3 --- .../tests/requirements.oldest.txt | 3 --- .../tests/requirements.oldest.txt | 3 --- .../tests/requirements.oldest.txt | 3 --- .../tests/requirements.oldest.txt | 3 --- .../tests/requirements.oldest.txt | 3 --- 6 files changed, 18 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt index 77fa4af14..8eecc5b0c 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-agno/tests/requirements.oldest.txt @@ -23,6 +23,3 @@ # # There is currently nothing to pin: agno has no test-only dependency that isn't already # provided by a declared bound or by the shared test fixtures. - --e util/opentelemetry-util-genai --e instrumentation/opentelemetry-instrumentation-genai-agno \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt index 5c161b832..71ed0e0c6 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt @@ -21,6 +21,3 @@ # OpenTelemetry SDK and test utilities come transitively from opentelemetry-test-util-genai, which # every oldest env installs. Pin here only test-only deps that nothing else already provides. # - --e util/opentelemetry-util-genai --e instrumentation/opentelemetry-instrumentation-genai-anthropic \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt index e4f219e40..697eb7644 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt @@ -27,6 +27,3 @@ langchain-aws==0.2.2 langchain-google-genai==2.0.0 langchain-anthropic==0.3.0 boto3==1.37.0 - --e util/opentelemetry-util-genai --e instrumentation/opentelemetry-instrumentation-genai-langchain[instruments] \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt index 647c83f70..1d7d06929 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.oldest.txt @@ -30,6 +30,3 @@ httpx==0.27.2 Deprecated==1.2.14 importlib-metadata==6.11.0 packaging==24.0 - --e util/opentelemetry-util-genai --e instrumentation/opentelemetry-instrumentation-genai-openai \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt index 42eb6f7ff..3fda1cd74 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-qwen-agent/tests/requirements.oldest.txt @@ -32,6 +32,3 @@ tqdm==4.66.1 # Exercise the oldest supported wrapt major (1.17 is the floor inherited # from opentelemetry-util-genai). wrapt==1.17.0 - --e util/opentelemetry-util-genai --e instrumentation/opentelemetry-instrumentation-genai-qwen-agent \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt index 495720a3f..34517fcdc 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt @@ -24,6 +24,3 @@ fsspec==2025.9.0 - --e instrumentation/opentelemetry-instrumentation-google-genai --e util/opentelemetry-util-genai[upload] \ No newline at end of file From 7734dd18c0c69162b598eeea799ab639a41f1da4 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 21 Aug 2026 09:42:18 -0700 Subject: [PATCH 14/16] Remove unused files --- .../tests/requirements.oldest.txt | 2 +- .../tests/requirements.oldest.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt index 71ed0e0c6..e2724ab2a 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.oldest.txt @@ -20,4 +20,4 @@ # factor, so they are NOT pinned here — pyproject.toml is the single source of truth. The # OpenTelemetry SDK and test utilities come transitively from opentelemetry-test-util-genai, which # every oldest env installs. Pin here only test-only deps that nothing else already provides. -# +# \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt index 697eb7644..48ed4db3c 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt @@ -26,4 +26,3 @@ langchain-openai==0.2.0 langchain-aws==0.2.2 langchain-google-genai==2.0.0 langchain-anthropic==0.3.0 -boto3==1.37.0 From 774bfe3fa44900d1a06a0586aaf1d413118e0453 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 21 Aug 2026 09:45:43 -0700 Subject: [PATCH 15/16] Fix file --- .../tests/requirements.oldest.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt index 48ed4db3c..fc4c7d834 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/tests/requirements.oldest.txt @@ -26,3 +26,4 @@ langchain-openai==0.2.0 langchain-aws==0.2.2 langchain-google-genai==2.0.0 langchain-anthropic==0.3.0 +boto3==1.37.0 \ No newline at end of file From bc5b61367c7318ddc6493b0a023fa14b772d85ab Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 21 Aug 2026 12:00:34 -0700 Subject: [PATCH 16/16] Remove util changes --- .../.changelog/365.changed | 1 - .../.changelog/365.deprecated | 1 - util/opentelemetry-util-genai/README.rst | 2 +- .../util/genai/_tool_invocation.py | 2 +- .../util/genai/_upload/completion_hook.py | 2 +- .../src/opentelemetry/util/genai/types.py | 95 ++++--------------- .../tests/test_handler_agent.py | 16 ++-- .../tests/test_handler_completion_hook.py | 28 +++--- .../tests/test_handler_fetch_response.py | 6 +- .../tests/test_handler_workflow.py | 8 +- .../tests/test_toolcall.py | 66 ++++--------- .../tests/test_upload.py | 22 ++--- .../tests/test_utils.py | 32 +------ .../tests/test_workflow_invocation.py | 12 +-- 14 files changed, 84 insertions(+), 209 deletions(-) delete mode 100644 util/opentelemetry-util-genai/.changelog/365.changed delete mode 100644 util/opentelemetry-util-genai/.changelog/365.deprecated diff --git a/util/opentelemetry-util-genai/.changelog/365.changed b/util/opentelemetry-util-genai/.changelog/365.changed deleted file mode 100644 index 37887becb..000000000 --- a/util/opentelemetry-util-genai/.changelog/365.changed +++ /dev/null @@ -1 +0,0 @@ -Align message-part class names with semconv (*Part suffix). Rename Text/Blob/File/Uri/Reasoning/ToolCallRequest/ToolCallResponse/ServerToolCall/ServerToolCallResponse to their *Part forms to match semantic-conventions-genai models. \ No newline at end of file diff --git a/util/opentelemetry-util-genai/.changelog/365.deprecated b/util/opentelemetry-util-genai/.changelog/365.deprecated deleted file mode 100644 index 057509c72..000000000 --- a/util/opentelemetry-util-genai/.changelog/365.deprecated +++ /dev/null @@ -1 +0,0 @@ -Deprecate the message part classes `Text`, `Reasoning`, `Blob`, `File`, `Uri`, `ToolCallRequest`, `ToolCallResponse`, `ServerToolCall` and `ServerToolCallResponse`; they remain available as backward compatible aliases of their `*Part` replacements. diff --git a/util/opentelemetry-util-genai/README.rst b/util/opentelemetry-util-genai/README.rst index 850e6802d..0a9f8f971 100644 --- a/util/opentelemetry-util-genai/README.rst +++ b/util/opentelemetry-util-genai/README.rst @@ -10,7 +10,7 @@ Key Components -------------- - ``TelemetryHandler`` -- manages LLM invocation lifecycles (spans, metrics, events) -- ``InferenceInvocation`` and message types (``TextPart``, ``ReasoningPart``, ``BlobPart``, etc.) -- structured data model for GenAI interactions +- ``InferenceInvocation`` and message types (``Text``, ``Reasoning``, ``Blob``, etc.) -- structured data model for GenAI interactions - ``CompletionHook`` -- protocol for uploading content to external storage (built-in ``fsspec`` support) - Metrics -- ``gen_ai.client.operation.duration`` and ``gen_ai.client.token.usage`` histograms, plus the streaming timing histograms ``gen_ai.client.operation.time_to_first_chunk`` and diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py index 55e933348..b4f2a56c7 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py @@ -33,7 +33,7 @@ def _any_value_to_attribute_value(value: AnyValue) -> AttributeValue | None: class ToolInvocation(GenAIInvocation): """Represents a tool call invocation for execute_tool span tracking. - Not used as a message part — use ToolCallRequestPart for that purpose. + Not used as a message part — use ToolCallRequest for that purpose. Use handler.tool(name) rather than constructing this directly. diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py index 9900cf13d..3bde5f899 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_upload/completion_hook.py @@ -83,7 +83,7 @@ def is_message_part_list_hashable( message_parts: list[types.MessagePart] | None, ) -> bool: return bool(message_parts) and all( - isinstance(x, types.TextPart) for x in message_parts + isinstance(x, types.Text) for x in message_parts ) diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py index 03f6eff0d..d3b751cbb 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -50,7 +50,7 @@ class GenericPart: @dataclass() -class ToolCallRequestPart: +class ToolCallRequest: """Represents a tool call requested by the model (message part only). Use this for tool calls in message history. For execution tracking with spans @@ -67,7 +67,7 @@ class ToolCallRequestPart: @dataclass() -class ToolCallResponsePart: +class ToolCallResponse: """Represents a tool call result sent to the model or a built-in tool call outcome and details This model is specified as part of semconv in `GenAI messages Python models - ToolCallResponsePart @@ -80,7 +80,7 @@ class ToolCallResponsePart: @dataclass() -class ServerToolCallPart: +class ServerToolCall: """Represents a server-side tool call. Server tool calls are executed by the model provider on the server side rather @@ -98,7 +98,7 @@ class ServerToolCallPart: @dataclass() -class ServerToolCallResponsePart: +class ServerToolCallResponse: """Represents a server-side tool call response. Contains the outcome and details of a server tool execution. Provider-specific @@ -115,7 +115,7 @@ class ServerToolCallResponsePart: @dataclass() -class TextPart: +class Text: """Represents text content sent to or received from the model This model is specified as part of semconv in `GenAI messages Python models - TextPart @@ -127,7 +127,7 @@ class TextPart: @dataclass() -class ReasoningPart: +class Reasoning: """Represents reasoning/thinking content received from the model This model is specified as part of semconv in `GenAI messages Python models - ReasoningPart @@ -161,7 +161,7 @@ class CompactionPart: @dataclass() -class BlobPart: +class Blob: """Represents blob binary data sent inline to the model This model is specified as part of semconv in `GenAI messages Python models - BlobPart @@ -175,7 +175,7 @@ class BlobPart: @dataclass() -class FilePart: +class File: """Represents an external referenced file sent to the model by file id This model is specified as part of semconv in `GenAI messages Python models - FilePart @@ -189,7 +189,7 @@ class FilePart: @dataclass() -class UriPart: +class Uri: """Represents an external referenced file sent to the model by URI This model is specified as part of semconv in `GenAI messages Python models - UriPart @@ -202,65 +202,6 @@ class UriPart: type: Literal["uri"] = "uri" -# Deprecated aliases for the message part classes, keeping for backward compatibility -# for users who import these classes directly. Delete -# this block once the deprecation period ends. - -Text = TextPart -""" -.. deprecated:: 1.0b0 - Use ``TextPart`` instead. -""" - -Reasoning = ReasoningPart -""" -.. deprecated:: 1.0b0 - Use ``ReasoningPart`` instead. -""" - -Blob = BlobPart -""" -.. deprecated:: 1.0b0 - Use ``BlobPart`` instead. -""" - -File = FilePart -""" -.. deprecated:: 1.0b0 - Use ``FilePart`` instead. -""" - -Uri = UriPart -""" -.. deprecated:: 1.0b0 - Use ``UriPart`` instead. -""" - -ToolCallRequest = ToolCallRequestPart -""" -.. deprecated:: 1.0b0 - Use ``ToolCallRequestPart`` instead. -""" - -ToolCallResponse = ToolCallResponsePart -""" -.. deprecated:: 1.0b0 - Use ``ToolCallResponsePart`` instead. -""" - -ServerToolCall = ServerToolCallPart -""" -.. deprecated:: 1.0b0 - Use ``ServerToolCallPart`` instead. -""" - -ServerToolCallResponse = ServerToolCallResponsePart -""" -.. deprecated:: 1.0b0 - Use ``ServerToolCallResponsePart`` instead. -""" - - @dataclass() class FunctionToolDefinition: """Represents a function tool definition sent to the model""" @@ -282,15 +223,15 @@ class GenericToolDefinition: ToolDefinition = Union[FunctionToolDefinition, GenericToolDefinition] MessagePart = Union[ - TextPart, - ToolCallRequestPart, - ToolCallResponsePart, - ServerToolCallPart, - ServerToolCallResponsePart, - BlobPart, - FilePart, - UriPart, - ReasoningPart, + Text, + ToolCallRequest, + ToolCallResponse, + ServerToolCall, + ServerToolCallResponse, + Blob, + File, + Uri, + Reasoning, CompactionPart, GenericPart, # For provider-specific types; prefer standard types above ] diff --git a/util/opentelemetry-util-genai/tests/test_handler_agent.py b/util/opentelemetry-util-genai/tests/test_handler_agent.py index 42d882b39..49b221e2c 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_agent.py +++ b/util/opentelemetry-util-genai/tests/test_handler_agent.py @@ -25,7 +25,7 @@ FunctionToolDefinition, InputMessage, OutputMessage, - TextPart, + Text, ) @@ -196,12 +196,12 @@ def test_default_values(self): def test_with_messages(self): invocation = self.handler.invoke_local_agent() invocation.input_messages = [ - InputMessage(role="user", parts=[TextPart(content="Hello")]) + InputMessage(role="user", parts=[Text(content="Hello")]) ] invocation.output_messages = [ OutputMessage( role="assistant", - parts=[TextPart(content="Hi there!")], + parts=[Text(content="Hi there!")], finish_reason="stop", ) ] @@ -331,7 +331,7 @@ def setUp(self): def test_system_instruction_on_span(self, _mock_cap): invocation = self.handler.invoke_local_agent() invocation.system_instruction = [ - TextPart(content="You are a helpful assistant."), + Text(content="You are a helpful assistant."), ] invocation.stop() @@ -362,12 +362,12 @@ def test_tool_definitions_on_span(self, _mock_cap): def test_messages_on_span(self, _mock_cap): invocation = self.handler.invoke_local_agent() invocation.input_messages = [ - InputMessage(role="user", parts=[TextPart(content="Hello")]) + InputMessage(role="user", parts=[Text(content="Hello")]) ] invocation.output_messages = [ OutputMessage( role="assistant", - parts=[TextPart(content="Hi!")], + parts=[Text(content="Hi!")], finish_reason="stop", ) ] @@ -380,10 +380,10 @@ def test_messages_on_span(self, _mock_cap): def test_content_not_on_span_by_default(self): invocation = self.handler.invoke_local_agent() invocation.system_instruction = [ - TextPart(content="You are a helpful assistant."), + Text(content="You are a helpful assistant."), ] invocation.input_messages = [ - InputMessage(role="user", parts=[TextPart(content="Hello")]) + InputMessage(role="user", parts=[Text(content="Hello")]) ] invocation.stop() diff --git a/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py b/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py index dc276d56e..bc4766c0c 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py +++ b/util/opentelemetry-util-genai/tests/test_handler_completion_hook.py @@ -20,7 +20,7 @@ FunctionToolDefinition, InputMessage, OutputMessage, - TextPart, + Text, ) _CAPTURE_EVENT_ENV = { @@ -48,16 +48,16 @@ def test_hook_called_on_stop(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[TextPart(content="hello")]) + InputMessage(role="user", parts=[Text(content="hello")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[TextPart(content="hi")], + parts=[Text(content="hi")], finish_reason="stop", ) ] - system_instruction = [TextPart(content="be helpful")] + system_instruction = [Text(content="be helpful")] tool_definitions = [ FunctionToolDefinition( name="get_weather", @@ -86,7 +86,7 @@ def test_hook_called_on_fail(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[TextPart(content="hello")]) + InputMessage(role="user", parts=[Text(content="hello")]) ] invocation = handler.inference("openai", request_model="gpt-4o") @@ -158,12 +158,12 @@ def test_workflow_hook_called_on_stop_with_messages(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[TextPart(content="what is 2+2?")]) + InputMessage(role="user", parts=[Text(content="what is 2+2?")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[TextPart(content="4")], + parts=[Text(content="4")], finish_reason="stop", ) ] @@ -189,7 +189,7 @@ def test_workflow_hook_called_on_fail(self): invocation = handler.workflow(name="my-workflow") invocation.input_messages = [ - InputMessage(role="user", parts=[TextPart(content="hello")]) + InputMessage(role="user", parts=[Text(content="hello")]) ] invocation.fail(RuntimeError("workflow failed")) @@ -213,16 +213,16 @@ def test_local_agent_hook_called_on_stop_with_messages(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[TextPart(content="what is 2+2?")]) + InputMessage(role="user", parts=[Text(content="what is 2+2?")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[TextPart(content="4")], + parts=[Text(content="4")], finish_reason="stop", ) ] - system_instruction = [TextPart(content="be helpful")] + system_instruction = [Text(content="be helpful")] tool_definitions = [ FunctionToolDefinition( name="get_weather", @@ -255,7 +255,7 @@ def test_local_agent_hook_called_on_fail(self): invocation = handler.invoke_local_agent(request_model="gpt-4") invocation.input_messages = [ - InputMessage(role="user", parts=[TextPart(content="hello")]) + InputMessage(role="user", parts=[Text(content="hello")]) ] invocation.fail(RuntimeError("agent failed")) @@ -268,12 +268,12 @@ def test_remote_agent_hook_called_on_stop_with_messages(self): handler = self._make_handler(hook) input_messages = [ - InputMessage(role="user", parts=[TextPart(content="hi")]) + InputMessage(role="user", parts=[Text(content="hi")]) ] output_messages = [ OutputMessage( role="assistant", - parts=[TextPart(content="hello")], + parts=[Text(content="hello")], finish_reason="stop", ) ] diff --git a/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py b/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py index 7878912d3..e94facb66 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py +++ b/util/opentelemetry-util-genai/tests/test_handler_fetch_response.py @@ -32,7 +32,7 @@ Error, FunctionToolDefinition, OutputMessage, - TextPart, + Text, ) # TODO: use the semconv constants once these attributes are released in @@ -274,12 +274,12 @@ def _fetch_with_content(self) -> None: invocation.output_messages = [ OutputMessage( role="assistant", - parts=[TextPart(content="This is a test.")], + parts=[Text(content="This is a test.")], finish_reason="stop", ) ] invocation.system_instruction = [ - TextPart(content="You are a helpful assistant.") + Text(content="You are a helpful assistant.") ] invocation.tool_definitions = [ FunctionToolDefinition( diff --git a/util/opentelemetry-util-genai/tests/test_handler_workflow.py b/util/opentelemetry-util-genai/tests/test_handler_workflow.py index 7b701330d..cf45f90cb 100644 --- a/util/opentelemetry-util-genai/tests/test_handler_workflow.py +++ b/util/opentelemetry-util-genai/tests/test_handler_workflow.py @@ -25,7 +25,7 @@ Error, InputMessage, OutputMessage, - TextPart, + Text, ) @@ -273,11 +273,9 @@ def test_workflow_context_manager_success_has_unset_status(self) -> None: self.assertEqual(spans[0].status.status_code, StatusCode.UNSET) def test_workflow_context_manager_with_messages(self) -> None: - inp = InputMessage(role="user", parts=[TextPart(content="hello")]) + inp = InputMessage(role="user", parts=[Text(content="hello")]) out = OutputMessage( - role="assistant", - parts=[TextPart(content="hi")], - finish_reason="stop", + role="assistant", parts=[Text(content="hi")], finish_reason="stop" ) with self.handler.workflow("msg_wf") as inv: inv.input_messages = [inp] diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index 4141d336b..47b3c6de3 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -1,7 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -"""Tests for ToolCallRequestPart and ToolInvocation inheritance structure""" +"""Tests for ToolCallRequest and ToolInvocation inheritance structure""" import os from unittest.mock import patch @@ -27,13 +27,8 @@ CompactionPart, InputMessage, ServerToolCall, - ServerToolCallPart, ServerToolCallResponse, - ServerToolCallResponsePart, ToolCallRequest, - ToolCallRequestPart, - ToolCallResponse, - ToolCallResponsePart, ) from opentelemetry.util.genai.utils import gen_ai_json_dumps @@ -43,8 +38,8 @@ def _make_handler() -> TelemetryHandler: def test_toolcallrequest_is_message_part(): - """ToolCallRequestPart is for message parts only""" - tcr = ToolCallRequestPart( + """ToolCallRequest is for message parts only""" + tcr = ToolCallRequest( arguments={"location": "Paris"}, name="get_weather", id="call_123" ) msg = InputMessage(role="user", parts=[tcr]) @@ -57,7 +52,7 @@ def test_toolcall_inherits_from_genaiinvocation(): tc = handler.tool("get_weather") tc.arguments = {"city": "Paris"} assert isinstance(tc, GenAIInvocation) - assert not isinstance(tc, ToolCallRequestPart) + assert not isinstance(tc, ToolCallRequest) tc.stop() @@ -71,13 +66,13 @@ def test_toolcall_has_attributes_dict(): def test_toolcallrequest_in_message_part_union(): - """ToolCallRequestPart (not ToolInvocation) is the correct type for message parts""" - tc = ToolCallRequestPart( + """ToolCallRequest (not ToolInvocation) is the correct type for message parts""" + tc = ToolCallRequest( name="get_weather", arguments={"city": "Paris"}, id="call_123" ) msg = InputMessage(role="assistant", parts=[tc]) assert len(msg.parts) == 1 - assert isinstance(msg.parts[0], ToolCallRequestPart) + assert isinstance(msg.parts[0], ToolCallRequest) assert not isinstance(msg.parts[0], GenAIInvocation) @@ -90,8 +85,8 @@ def test_toolcall_operation_name(): def test_server_tool_call_basic(): - """ServerToolCallPart can be created with required fields""" - stc = ServerToolCallPart( + """ServerToolCall can be created with required fields""" + stc = ServerToolCall( name="code_interpreter", server_tool_call={"type": "code_interpreter", "code": "print(1)"}, ) @@ -105,8 +100,8 @@ def test_server_tool_call_basic(): def test_server_tool_call_with_id(): - """ServerToolCallPart can have an optional id""" - stc = ServerToolCallPart( + """ServerToolCall can have an optional id""" + stc = ServerToolCall( name="web_search", server_tool_call={"type": "web_search", "query": "weather"}, id="stc_001", @@ -115,8 +110,8 @@ def test_server_tool_call_with_id(): def test_server_tool_call_response_basic(): - """ServerToolCallResponsePart can be created with required fields""" - stcr = ServerToolCallResponsePart( + """ServerToolCallResponse can be created with required fields""" + stcr = ServerToolCallResponse( server_tool_call_response={ "type": "code_interpreter", "output": "1\n", @@ -131,19 +126,19 @@ def test_server_tool_call_response_basic(): def test_server_tool_call_in_message(): - """ServerToolCallPart and ServerToolCallResponsePart work as MessageParts""" - stc = ServerToolCallPart( + """ServerToolCall and ServerToolCallResponse work as MessageParts""" + stc = ServerToolCall( name="code_interpreter", server_tool_call={"type": "code_interpreter", "code": "x = 1"}, ) - stcr = ServerToolCallResponsePart( + stcr = ServerToolCallResponse( server_tool_call_response={"type": "code_interpreter", "output": ""}, id="stc_001", ) msg = InputMessage(role="assistant", parts=[stc, stcr]) assert len(msg.parts) == 2 - assert isinstance(msg.parts[0], ServerToolCallPart) - assert isinstance(msg.parts[1], ServerToolCallResponsePart) + assert isinstance(msg.parts[0], ServerToolCall) + assert isinstance(msg.parts[1], ServerToolCallResponse) def test_compactionpart_is_message_part(): @@ -160,31 +155,6 @@ def test_compactionpart_is_message_part(): assert isinstance(msg.parts[0], CompactionPart) -def test_deprecated_tool_call_aliases_resolve_to_part_classes(): - """The pre-*Part tool call names still resolve to their replacements.""" - assert ToolCallRequest is ToolCallRequestPart - assert ToolCallResponse is ToolCallResponsePart - assert ServerToolCall is ServerToolCallPart - assert ServerToolCallResponse is ServerToolCallResponsePart - - -def test_deprecated_tool_call_aliases_build_message_parts(): - """Parts built through the deprecated aliases are still valid MessageParts.""" - tcr = ToolCallRequest( - arguments={"location": "Paris"}, name="get_weather", id="call_123" - ) - stc = ServerToolCall( - name="code_interpreter", - server_tool_call={"type": "code_interpreter", "code": "x = 1"}, - ) - msg = InputMessage(role="assistant", parts=[tcr, stc]) - - assert isinstance(msg.parts[0], ToolCallRequestPart) - assert msg.parts[0].type == "tool_call" - assert isinstance(msg.parts[1], ServerToolCallPart) - assert msg.parts[1].type == "server_tool_call" - - if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/util/opentelemetry-util-genai/tests/test_upload.py b/util/opentelemetry-util-genai/tests/test_upload.py index 57f5d6b5c..10474645e 100644 --- a/util/opentelemetry-util-genai/tests/test_upload.py +++ b/util/opentelemetry-util-genai/tests/test_upload.py @@ -28,12 +28,12 @@ FAKE_INPUTS = [ types.InputMessage( role="user", - parts=[types.TextPart(content="What is the capital of France?")], + parts=[types.Text(content="What is the capital of France?")], ), types.InputMessage( role="assistant", parts=[ - types.ToolCallRequestPart( + types.ToolCallRequest( id="get_capital_0", name="get_capital", arguments={"city": "Paris"}, @@ -43,7 +43,7 @@ types.InputMessage( role="user", parts=[ - types.ToolCallResponsePart( + types.ToolCallResponse( id="get_capital_0", response={"capital": "Paris"} ) ], @@ -52,13 +52,11 @@ FAKE_OUTPUTS = [ types.OutputMessage( role="assistant", - parts=[types.TextPart(content="Paris")], + parts=[types.Text(content="Paris")], finish_reason="stop", ), ] -FAKE_SYSTEM_INSTRUCTION = [ - types.TextPart(content="You are a helpful assistant.") -] +FAKE_SYSTEM_INSTRUCTION = [types.Text(content="You are a helpful assistant.")] FAKE_TOOL_DEFINITIONS: list[types.ToolDefinition] = [ types.FunctionToolDefinition( @@ -176,7 +174,7 @@ def test_lru_cache_works(self): self.hook.on_completion( inputs=[], outputs=[], - system_instruction=[types.TextPart(content=str(iteration))], + system_instruction=[types.Text(content=str(iteration))], tool_definitions=[], ) self.hook.shutdown() @@ -353,8 +351,8 @@ def test_system_insruction_is_hashed_to_avoid_reupload(self): # FIle should exist. self.assertTrue(self.hook._file_exists(expected_file_name)) system_instructions = [ - types.TextPart(content="You are a helpful assistant."), - types.TextPart(content="You will do your best."), + types.Text(content="You are a helpful assistant."), + types.Text(content="You will do your best."), ] record = LogRecord() self.hook.on_completion( @@ -482,9 +480,7 @@ def test_upload_bytes(self) -> None: types.InputMessage( role="user", parts=[ - types.TextPart( - content="What is the capital of France?" - ), + types.Text(content="What is the capital of France?"), {"type": "generic_bytes", "bytes": b"hello"}, ], ) diff --git a/util/opentelemetry-util-genai/tests/test_utils.py b/util/opentelemetry-util-genai/tests/test_utils.py index b7f86d898..127e9abae 100644 --- a/util/opentelemetry-util-genai/tests/test_utils.py +++ b/util/opentelemetry-util-genai/tests/test_utils.py @@ -34,20 +34,11 @@ get_telemetry_handler, ) from opentelemetry.util.genai.types import ( - Blob, - BlobPart, ContentCapturingMode, - File, - FilePart, InputMessage, MessagePart, OutputMessage, - Reasoning, - ReasoningPart, Text, - TextPart, - Uri, - UriPart, ) from opentelemetry.util.genai.utils import ( get_content_capturing_mode, @@ -59,23 +50,21 @@ def _create_input_message( content: str = "hello world", role: str = "Human" ) -> InputMessage: - return InputMessage(role=role, parts=[TextPart(content=content)]) + return InputMessage(role=role, parts=[Text(content=content)]) def _create_output_message( content: str = "hello back", finish_reason: str = "stop", role: str = "AI" ) -> OutputMessage: return OutputMessage( - role=role, - parts=[TextPart(content=content)], - finish_reason=finish_reason, + role=role, parts=[Text(content=content)], finish_reason=finish_reason ) def _create_system_instruction( content: str = "You are a helpful assistant.", ) -> list[MessagePart]: - return [TextPart(content=content)] + return [Text(content=content)] def _get_single_span(span_exporter: InMemorySpanExporter) -> ReadableSpan: @@ -147,21 +136,6 @@ def _normalize_to_dict(value: Any) -> dict[str, Any]: return dict(value) if isinstance(value, tuple) else value -class TestDeprecatedMessagePartAliases(unittest.TestCase): - def test_aliases_resolve_to_part_classes(self): - self.assertIs(Text, TextPart) - self.assertIs(Reasoning, ReasoningPart) - self.assertIs(Blob, BlobPart) - self.assertIs(File, FilePart) - self.assertIs(Uri, UriPart) - - def test_alias_builds_message_part(self): - message = InputMessage(role="user", parts=[Text(content="hello")]) - - self.assertIsInstance(message.parts[0], TextPart) - self.assertEqual(message.parts[0].type, "text") - - class TestShouldEmitEvent(unittest.TestCase): def test_should_emit_event_against_various_env_var_combinations( self, diff --git a/util/opentelemetry-util-genai/tests/test_workflow_invocation.py b/util/opentelemetry-util-genai/tests/test_workflow_invocation.py index bcfda5c38..6ddfd30a6 100644 --- a/util/opentelemetry-util-genai/tests/test_workflow_invocation.py +++ b/util/opentelemetry-util-genai/tests/test_workflow_invocation.py @@ -13,7 +13,7 @@ from opentelemetry.util.genai.types import ( InputMessage, OutputMessage, - TextPart, + Text, ) @@ -42,7 +42,7 @@ def test_custom_name(self): assert invocation._name == "customer_support_pipeline" def test_with_input_messages(self): - msg = InputMessage(role="user", parts=[TextPart(content="hello")]) + msg = InputMessage(role="user", parts=[Text(content="hello")]) invocation = self.handler.workflow(name="test") invocation.input_messages = [msg] invocation.stop() @@ -51,9 +51,7 @@ def test_with_input_messages(self): def test_with_output_messages(self): msg = OutputMessage( - role="assistant", - parts=[TextPart(content="hi")], - finish_reason="stop", + role="assistant", parts=[Text(content="hi")], finish_reason="stop" ) invocation = self.handler.workflow(name="test") invocation.output_messages = [msg] @@ -87,10 +85,10 @@ def test_default_attributes_are_independent(self): inv2.stop() def test_full_construction(self): - inp = InputMessage(role="user", parts=[TextPart(content="query")]) + inp = InputMessage(role="user", parts=[Text(content="query")]) out = OutputMessage( role="assistant", - parts=[TextPart(content="answer")], + parts=[Text(content="answer")], finish_reason="stop", ) invocation = self.handler.workflow(name="my_workflow")