From 321ea308f1a16bccde5bb2b37593c58e6d7fa43f Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Thu, 19 Feb 2026 09:51:05 -0700 Subject: [PATCH 01/17] feat: update toolcall types to match semconv --- .../src/opentelemetry/util/genai/types.py | 48 ++++++- .../tests/test_toolcall.py | 125 ++++++++++++++++++ 2 files changed, 167 insertions(+), 6 deletions(-) create mode 100644 util/opentelemetry-util-genai/tests/test_toolcall.py 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 045a65b372..57ac697301 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -43,17 +43,53 @@ class ContentCapturingMode(Enum): @dataclass() class ToolCall: - """Represents a tool call requested by the model - - This model is specified as part of semconv in `GenAI messages Python models - ToolCallRequestPart - `__. + """Represents a tool call with dual usage: message part and execution tracking. + + This type serves two purposes as defined in OpenTelemetry semantic conventions: + + 1. Message Part (ToolCallRequestPart): + Represents a tool call requested by the model as part of a message. + Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/non-normative/models.ipynb + + 2. Tool Execution (execute_tool spans): + Represents the actual execution of a tool call, tracked via spans and metrics. + Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md#execute-tool-span + + The execution-only fields (tool_type, tool_description, tool_result, error_type) + are used when tracking tool execution via spans, but are typically not present when + this type is used as a message part in InputMessage or OutputMessage. + + Semantic convention attributes for execute_tool spans: + - gen_ai.operation.name: "execute_tool" (Required) + - gen_ai.tool.name: Name of the tool (Recommended) + - gen_ai.tool.call.id: Tool call identifier (Recommended if available) + - gen_ai.tool.type: Type classification - "function", "extension", or "datastore" (Recommended if available) + - gen_ai.tool.description: Tool description (Recommended if available) + - gen_ai.tool.call.arguments: Parameters passed to tool (Opt-In, may contain sensitive data) + - gen_ai.tool.call.result: Result returned by tool (Opt-In, may contain sensitive data) + - error.type: Error type if operation failed (Conditionally Required) """ - arguments: Any + # Fields used for both message part and execution tracking: + # gen_ai.tool.name - Name of the tool name: str - id: str | None + # gen_ai.tool.call.arguments - Arguments passed to the tool (Opt-In, may contain sensitive data) + arguments: Any = None + # gen_ai.tool.call.id - Unique identifier for the tool call + id: str | None = None + # Message part type identifier type: Literal["tool_call"] = "tool_call" + # Execution-only fields (used for execute_tool spans, not typically in messages): + # gen_ai.tool.type - Tool type: "function", "extension", or "datastore" + tool_type: str | None = None + # gen_ai.tool.description - Description of what the tool does + tool_description: str | None = None + # gen_ai.tool.call.result - Result returned by the tool (Opt-In, may contain sensitive data) + tool_result: Any = None + # error.type - Error type if the tool call failed + error_type: str | None = None + @dataclass() class ToolCallResponse: diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py new file mode 100644 index 0000000000..d217c19485 --- /dev/null +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -0,0 +1,125 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for Enhanced ToolCall Type Definition""" + +import pytest + +from opentelemetry.util.genai.types import ( + InputMessage, + OutputMessage, + ToolCall, +) + + +class TestToolCallEnhancements: + """Test suite for Enhanced ToolCall Type Definition""" + + def test_toolcall_backward_compatibility(self): + """Test backward compatibility as message part""" + tc = ToolCall( + name="get_weather", + arguments={"location": "Paris"}, + id="call_123", + ) + assert tc.name == "get_weather" + assert tc.arguments == {"location": "Paris"} + assert tc.id == "call_123" + assert tc.type == "tool_call" + + def test_toolcall_in_message(self): + """Test ToolCall works as message part in InputMessage""" + tc = ToolCall(name="get_weather", arguments={"location": "Paris"}) + msg = InputMessage(role="user", parts=[tc]) + assert len(msg.parts) == 1 + assert msg.parts[0] == tc + + def test_toolcall_full_lifecycle(self): + """Test complete tool call lifecycle with all fields""" + # Start with tool call request + tc = ToolCall( + name="get_weather", + arguments={"location": "Paris", "units": "metric"}, + id="call_abc123", + tool_type="function", + tool_description="Retrieves current weather for a location", + ) + + # Simulate successful execution - set result + tc.tool_result = {"temperature": 15, "condition": "cloudy"} + + assert tc.name == "get_weather" + assert tc.tool_type == "function" + assert tc.tool_result is not None + assert tc.error_type is None + + # Simulate failed execution - set error + tc_failed = ToolCall( + name="get_weather", + arguments={"location": "Invalid"}, + id="call_xyz789", + tool_type="function", + ) + tc_failed.error_type = "InvalidLocationError" + + assert tc_failed.error_type == "InvalidLocationError" + assert tc_failed.tool_result is None + + def test_toolcall_with_output_message(self): + """Test ToolCall in OutputMessage (backward compatibility)""" + tc = ToolCall( + name="get_weather", + arguments={"location": "Paris"}, + id="call_123", + ) + msg = OutputMessage( + role="assistant", parts=[tc], finish_reason="tool_calls" + ) + + assert len(msg.parts) == 1 + assert msg.parts[0].name == "get_weather" + assert msg.finish_reason == "tool_calls" + + def test_toolcall_field_values(self): + """Test that ToolCall fields can be set and retrieved correctly""" + tc = ToolCall( + name="get_weather", + id="call_123", + tool_type="function", + tool_description="Weather tool", + arguments={"location": "Paris"}, + tool_result={"temp": 20}, + ) + + # Verify all field values are set correctly + assert tc.name == "get_weather" + assert tc.id == "call_123" + assert tc.tool_type == "function" + assert tc.tool_description == "Weather tool" + assert tc.arguments == {"location": "Paris"} + assert tc.tool_result == {"temp": 20} + assert tc.error_type is None + + # Verify these fields map to semantic convention attributes: + # - name -> gen_ai.tool.name + # - id -> gen_ai.tool.call.id + # - tool_type -> gen_ai.tool.type + # - tool_description -> gen_ai.tool.description + # - arguments -> gen_ai.tool.call.arguments (Opt-In) + # - tool_result -> gen_ai.tool.call.result (Opt-In) + # - error_type -> error.type + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 3de0e8d86b914efa345b66dd74d9c708a1de0ac8 Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Thu, 19 Feb 2026 09:57:11 -0700 Subject: [PATCH 02/17] update changelog --- util/opentelemetry-util-genai/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/util/opentelemetry-util-genai/CHANGELOG.md b/util/opentelemetry-util-genai/CHANGELOG.md index 0e7c521bf6..aea25a1fee 100644 --- a/util/opentelemetry-util-genai/CHANGELOG.md +++ b/util/opentelemetry-util-genai/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Enrich ToolCall type ([#4218](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4218)) - Add `gen_ai.tool_definitions` to completion hook ([#4181](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4181)) - Add support for emitting inference events and enrich message types. ([#3994](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3994)) - Add support for `server.address`, `server.port` on all signals and additional metric-only attributes From b277b721cff7d0e168742c43933764fc841b1f29 Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Thu, 19 Feb 2026 10:05:18 -0700 Subject: [PATCH 03/17] lint updates --- .../tests/test_toolcall.py | 193 +++++++++--------- 1 file changed, 97 insertions(+), 96 deletions(-) diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index d217c19485..06b5f0f0cc 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -23,102 +23,103 @@ ) -class TestToolCallEnhancements: - """Test suite for Enhanced ToolCall Type Definition""" - - def test_toolcall_backward_compatibility(self): - """Test backward compatibility as message part""" - tc = ToolCall( - name="get_weather", - arguments={"location": "Paris"}, - id="call_123", - ) - assert tc.name == "get_weather" - assert tc.arguments == {"location": "Paris"} - assert tc.id == "call_123" - assert tc.type == "tool_call" - - def test_toolcall_in_message(self): - """Test ToolCall works as message part in InputMessage""" - tc = ToolCall(name="get_weather", arguments={"location": "Paris"}) - msg = InputMessage(role="user", parts=[tc]) - assert len(msg.parts) == 1 - assert msg.parts[0] == tc - - def test_toolcall_full_lifecycle(self): - """Test complete tool call lifecycle with all fields""" - # Start with tool call request - tc = ToolCall( - name="get_weather", - arguments={"location": "Paris", "units": "metric"}, - id="call_abc123", - tool_type="function", - tool_description="Retrieves current weather for a location", - ) - - # Simulate successful execution - set result - tc.tool_result = {"temperature": 15, "condition": "cloudy"} - - assert tc.name == "get_weather" - assert tc.tool_type == "function" - assert tc.tool_result is not None - assert tc.error_type is None - - # Simulate failed execution - set error - tc_failed = ToolCall( - name="get_weather", - arguments={"location": "Invalid"}, - id="call_xyz789", - tool_type="function", - ) - tc_failed.error_type = "InvalidLocationError" - - assert tc_failed.error_type == "InvalidLocationError" - assert tc_failed.tool_result is None - - def test_toolcall_with_output_message(self): - """Test ToolCall in OutputMessage (backward compatibility)""" - tc = ToolCall( - name="get_weather", - arguments={"location": "Paris"}, - id="call_123", - ) - msg = OutputMessage( - role="assistant", parts=[tc], finish_reason="tool_calls" - ) - - assert len(msg.parts) == 1 - assert msg.parts[0].name == "get_weather" - assert msg.finish_reason == "tool_calls" - - def test_toolcall_field_values(self): - """Test that ToolCall fields can be set and retrieved correctly""" - tc = ToolCall( - name="get_weather", - id="call_123", - tool_type="function", - tool_description="Weather tool", - arguments={"location": "Paris"}, - tool_result={"temp": 20}, - ) - - # Verify all field values are set correctly - assert tc.name == "get_weather" - assert tc.id == "call_123" - assert tc.tool_type == "function" - assert tc.tool_description == "Weather tool" - assert tc.arguments == {"location": "Paris"} - assert tc.tool_result == {"temp": 20} - assert tc.error_type is None - - # Verify these fields map to semantic convention attributes: - # - name -> gen_ai.tool.name - # - id -> gen_ai.tool.call.id - # - tool_type -> gen_ai.tool.type - # - tool_description -> gen_ai.tool.description - # - arguments -> gen_ai.tool.call.arguments (Opt-In) - # - tool_result -> gen_ai.tool.call.result (Opt-In) - # - error_type -> error.type +def test_toolcall_backward_compatibility(): + """Test backward compatibility as message part""" + tc = ToolCall( + name="get_weather", + arguments={"location": "Paris"}, + id="call_123", + ) + assert tc.name == "get_weather" + assert tc.arguments == {"location": "Paris"} + assert tc.id == "call_123" + assert tc.type == "tool_call" + + +def test_toolcall_in_message(): + """Test ToolCall works as message part in InputMessage""" + tc = ToolCall(name="get_weather", arguments={"location": "Paris"}) + msg = InputMessage(role="user", parts=[tc]) + assert len(msg.parts) == 1 + assert msg.parts[0] == tc + + +def test_toolcall_full_lifecycle(): + """Test complete tool call lifecycle with all fields""" + # Start with tool call request + tc = ToolCall( + name="get_weather", + arguments={"location": "Paris", "units": "metric"}, + id="call_abc123", + tool_type="function", + tool_description="Retrieves current weather for a location", + ) + + # Simulate successful execution - set result + tc.tool_result = {"temperature": 15, "condition": "cloudy"} + + assert tc.name == "get_weather" + assert tc.tool_type == "function" + assert tc.tool_result is not None + assert tc.error_type is None + + # Simulate failed execution - set error + tc_failed = ToolCall( + name="get_weather", + arguments={"location": "Invalid"}, + id="call_xyz789", + tool_type="function", + ) + tc_failed.error_type = "InvalidLocationError" + + assert tc_failed.error_type == "InvalidLocationError" + assert tc_failed.tool_result is None + + +def test_toolcall_with_output_message(): + """Test ToolCall in OutputMessage (backward compatibility)""" + tc = ToolCall( + name="get_weather", + arguments={"location": "Paris"}, + id="call_123", + ) + msg = OutputMessage( + role="assistant", parts=[tc], finish_reason="tool_calls" + ) + + assert len(msg.parts) == 1 + assert msg.parts[0].name == "get_weather" + assert msg.finish_reason == "tool_calls" + + +def test_toolcall_field_values(): + """Test that ToolCall fields can be set and retrieved correctly""" + tc = ToolCall( + name="get_weather", + id="call_123", + tool_type="function", + tool_description="Weather tool", + arguments={"location": "Paris"}, + tool_result={"temp": 20}, + ) + + # Verify all field values are set correctly + assert tc.name == "get_weather" + assert tc.id == "call_123" + assert tc.tool_type == "function" + assert tc.tool_description == "Weather tool" + assert tc.arguments == {"location": "Paris"} + assert tc.tool_result == {"temp": 20} + assert tc.error_type is None + + # Verify these fields map to semantic convention attributes: + # - name -> gen_ai.tool.name + # - id -> gen_ai.tool.call.id + # - tool_type -> gen_ai.tool.type + # - tool_description -> gen_ai.tool.description + # - arguments -> gen_ai.tool.call.arguments (Opt-In) + # - tool_result -> gen_ai.tool.call.result (Opt-In) + # - error_type -> error.type if __name__ == "__main__": From 9b3f2a661834296489a57064d36a24860ea7f07a Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Thu, 19 Feb 2026 14:38:01 -0700 Subject: [PATCH 04/17] feat: refactor ToolCall to ToolCallRequest and enhance type definitions --- .../instrumentation/vertexai/utils.py | 4 +- .../src/opentelemetry/util/genai/types.py | 54 +++--- .../tests/test_toolcall.py | 169 +++++++++++------- 3 files changed, 136 insertions(+), 91 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py index 3221cdf9bc..6b280d2bc4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py @@ -56,7 +56,7 @@ FinishReason, MessagePart, Text, - ToolCall, + ToolCallRequest, ToolCallResponse, ) from opentelemetry.util.genai.utils import get_content_capturing_mode @@ -341,7 +341,7 @@ def convert_content_to_message_parts( elif "function_call" in part: part = part.function_call parts.append( - ToolCall( + ToolCallRequest( id=f"{part.name}_{idx}", name=part.name, arguments=json_format.MessageToDict( 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 57ac697301..e37d0bdf6a 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -42,22 +42,30 @@ class ContentCapturingMode(Enum): @dataclass() -class ToolCall: - """Represents a tool call with dual usage: message part and execution tracking. +class ToolCallRequest: + """Represents a tool call requested by the model - This type serves two purposes as defined in OpenTelemetry semantic conventions: + This model is specified as part of semconv in `GenAI messages Python models - ToolCallRequestPart + `__. + """ + + arguments: Any + name: str + id: str | None + type: Literal["tool_call"] = "tool_call" - 1. Message Part (ToolCallRequestPart): - Represents a tool call requested by the model as part of a message. - Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/non-normative/models.ipynb - 2. Tool Execution (execute_tool spans): - Represents the actual execution of a tool call, tracked via spans and metrics. - Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md#execute-tool-span +@dataclass() +class ToolCall(ToolCallRequest): + """Represents a tool call for execution tracking with spans and metrics. + + This type extends ToolCallRequest with additional fields for tracking tool execution + per the execute_tool span semantic conventions. + + Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md#execute-tool-span - The execution-only fields (tool_type, tool_description, tool_result, error_type) - are used when tracking tool execution via spans, but are typically not present when - this type is used as a message part in InputMessage or OutputMessage. + For simple message parts (tool calls requested by the model), consider using + ToolCallRequest instead to avoid unnecessary execution-tracking fields. Semantic convention attributes for execute_tool spans: - gen_ai.operation.name: "execute_tool" (Required) @@ -70,17 +78,7 @@ class ToolCall: - error.type: Error type if operation failed (Conditionally Required) """ - # Fields used for both message part and execution tracking: - # gen_ai.tool.name - Name of the tool - name: str - # gen_ai.tool.call.arguments - Arguments passed to the tool (Opt-In, may contain sensitive data) - arguments: Any = None - # gen_ai.tool.call.id - Unique identifier for the tool call - id: str | None = None - # Message part type identifier - type: Literal["tool_call"] = "tool_call" - - # Execution-only fields (used for execute_tool spans, not typically in messages): + # Execution-only fields (used for execute_tool spans): # gen_ai.tool.type - Tool type: "function", "extension", or "datastore" tool_type: str | None = None # gen_ai.tool.description - Description of what the tool does @@ -194,7 +192,15 @@ class GenericToolDefinition: ToolDefinition = Union[FunctionToolDefinition, GenericToolDefinition] MessagePart = Union[ - Text, ToolCall, ToolCallResponse, Blob, File, Uri, Reasoning, Any + Text, + ToolCallRequest, + ToolCall, + ToolCallResponse, + Blob, + File, + Uri, + Reasoning, + Any, ] diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index 06b5f0f0cc..a06f39efdd 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for Enhanced ToolCall Type Definition""" +"""Tests for ToolCallRequest and ToolCall types""" import pytest @@ -20,106 +20,145 @@ InputMessage, OutputMessage, ToolCall, + ToolCallRequest, ) -def test_toolcall_backward_compatibility(): - """Test backward compatibility as message part""" - tc = ToolCall( +def test_toolcallrequest_basic(): + """Test basic ToolCallRequest instantiation""" + tcr = ToolCallRequest(arguments=None, name="get_weather", id=None) + assert tcr.name == "get_weather" + assert tcr.type == "tool_call" + assert tcr.arguments is None + assert tcr.id is None + + +def test_toolcallrequest_with_all_fields(): + """Test ToolCallRequest with all fields""" + tcr = ToolCallRequest( name="get_weather", arguments={"location": "Paris"}, id="call_123", ) - assert tc.name == "get_weather" - assert tc.arguments == {"location": "Paris"} - assert tc.id == "call_123" - assert tc.type == "tool_call" + assert tcr.name == "get_weather" + assert tcr.arguments == {"location": "Paris"} + assert tcr.id == "call_123" + assert tcr.type == "tool_call" -def test_toolcall_in_message(): - """Test ToolCall works as message part in InputMessage""" - tc = ToolCall(name="get_weather", arguments={"location": "Paris"}) - msg = InputMessage(role="user", parts=[tc]) +def test_toolcallrequest_in_message(): + """Test ToolCallRequest works as message part""" + tcr = ToolCallRequest( + arguments={"location": "Paris"}, name="get_weather", id=None + ) + msg = InputMessage(role="user", parts=[tcr]) assert len(msg.parts) == 1 - assert msg.parts[0] == tc + assert msg.parts[0] == tcr + + +def test_toolcall_inherits_from_toolcallrequest(): + """Test that ToolCall inherits from ToolCallRequest""" + tc = ToolCall(arguments=None, name="get_weather", id=None) + assert isinstance(tc, ToolCallRequest) + assert isinstance(tc, ToolCall) + +def test_toolcall_has_execution_fields(): + """Test ToolCall has execution-only fields""" + tc = ToolCall(arguments=None, name="get_weather", id=None) + assert hasattr(tc, "tool_type") + assert hasattr(tc, "tool_description") + assert hasattr(tc, "tool_result") + assert hasattr(tc, "error_type") -def test_toolcall_full_lifecycle(): - """Test complete tool call lifecycle with all fields""" - # Start with tool call request + +def test_toolcall_execution_fields_default_none(): + """Test ToolCall execution fields default to None""" + tc = ToolCall(arguments=None, name="get_weather", id=None) + assert tc.tool_type is None + assert tc.tool_description is None + assert tc.tool_result is None + assert tc.error_type is None + + +def test_toolcall_with_execution_fields(): + """Test ToolCall with execution fields set""" tc = ToolCall( name="get_weather", - arguments={"location": "Paris", "units": "metric"}, - id="call_abc123", + arguments={"location": "Paris"}, + id="call_123", tool_type="function", - tool_description="Retrieves current weather for a location", + tool_description="Get current weather", + tool_result={"temp": 20, "condition": "sunny"}, ) - - # Simulate successful execution - set result - tc.tool_result = {"temperature": 15, "condition": "cloudy"} - assert tc.name == "get_weather" assert tc.tool_type == "function" - assert tc.tool_result is not None - assert tc.error_type is None + assert tc.tool_description == "Get current weather" + assert tc.tool_result == {"temp": 20, "condition": "sunny"} - # Simulate failed execution - set error - tc_failed = ToolCall( - name="get_weather", + +def test_toolcall_with_error(): + """Test ToolCall with error_type set""" + tc = ToolCall( arguments={"location": "Invalid"}, - id="call_xyz789", - tool_type="function", + name="get_weather", + id=None, + error_type="InvalidLocationError", ) - tc_failed.error_type = "InvalidLocationError" - - assert tc_failed.error_type == "InvalidLocationError" - assert tc_failed.tool_result is None + assert tc.error_type == "InvalidLocationError" + assert tc.tool_result is None -def test_toolcall_with_output_message(): - """Test ToolCall in OutputMessage (backward compatibility)""" +def test_toolcall_backward_compatibility(): + """Test ToolCall still works as message part (backward compatibility)""" tc = ToolCall( name="get_weather", arguments={"location": "Paris"}, id="call_123", ) - msg = OutputMessage( + # Should work in messages + msg = InputMessage(role="user", parts=[tc]) + assert len(msg.parts) == 1 + + # Should work in output messages + out_msg = OutputMessage( role="assistant", parts=[tc], finish_reason="tool_calls" ) + assert len(out_msg.parts) == 1 - assert len(msg.parts) == 1 - assert msg.parts[0].name == "get_weather" - assert msg.finish_reason == "tool_calls" +def test_toolcallrequest_no_execution_fields(): + """Test that ToolCallRequest doesn't have execution fields""" + tcr = ToolCallRequest(arguments=None, name="get_weather", id=None) + # ToolCallRequest should only have message part fields + assert not hasattr(tcr, "tool_type") + assert not hasattr(tcr, "tool_description") + assert not hasattr(tcr, "tool_result") + assert not hasattr(tcr, "error_type") -def test_toolcall_field_values(): - """Test that ToolCall fields can be set and retrieved correctly""" + +def test_mixed_types_in_message(): + """Test using both ToolCallRequest and ToolCall in messages""" + tcr = ToolCallRequest(arguments=None, name="simple_tool", id=None) tc = ToolCall( - name="get_weather", - id="call_123", - tool_type="function", - tool_description="Weather tool", - arguments={"location": "Paris"}, - tool_result={"temp": 20}, + arguments=None, name="complex_tool", id=None, tool_type="function" ) - # Verify all field values are set correctly - assert tc.name == "get_weather" - assert tc.id == "call_123" - assert tc.tool_type == "function" - assert tc.tool_description == "Weather tool" - assert tc.arguments == {"location": "Paris"} - assert tc.tool_result == {"temp": 20} - assert tc.error_type is None - - # Verify these fields map to semantic convention attributes: - # - name -> gen_ai.tool.name - # - id -> gen_ai.tool.call.id - # - tool_type -> gen_ai.tool.type - # - tool_description -> gen_ai.tool.description - # - arguments -> gen_ai.tool.call.arguments (Opt-In) - # - tool_result -> gen_ai.tool.call.result (Opt-In) - # - error_type -> error.type + msg = InputMessage(role="user", parts=[tcr, tc]) + assert len(msg.parts) == 2 + assert isinstance(msg.parts[0], ToolCallRequest) + assert isinstance(msg.parts[1], ToolCall) + # ToolCall is also a ToolCallRequest + assert isinstance(msg.parts[1], ToolCallRequest) + + +def test_toolcall_tool_type_values(): + """Test valid tool_type values""" + for tool_type in ["function", "extension", "datastore"]: + tc = ToolCall( + arguments=None, name="test", id=None, tool_type=tool_type + ) + assert tc.tool_type == tool_type if __name__ == "__main__": From 0f28588fb97ada15f1165b9b3e84c6945bbfdef7 Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Mon, 23 Feb 2026 10:21:10 -0700 Subject: [PATCH 05/17] test: point vertexai at current version of genai utils --- .../tests/requirements.oldest.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.oldest.txt b/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.oldest.txt index 7bfd62ff5f..74b2738541 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.oldest.txt +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.oldest.txt @@ -69,7 +69,8 @@ opentelemetry-api==1.37 opentelemetry-sdk==1.37 opentelemetry-semantic-conventions==0.58b0 opentelemetry-instrumentation==0.58b0 -opentelemetry-util-genai[upload]==0.2b0 +# opentelemetry-util-genai[upload]==0.2b0 +-e util/opentelemetry-util-genai[upload] fsspec==2025.9.0 -e instrumentation-genai/opentelemetry-instrumentation-vertexai[instruments] From 2f009f947b7ea116e10297e658b168a430afb53b Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Mon, 23 Feb 2026 10:38:35 -0700 Subject: [PATCH 06/17] test: fix fileupload test to use ToolCallRequest --- util/opentelemetry-util-genai/tests/test_upload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/opentelemetry-util-genai/tests/test_upload.py b/util/opentelemetry-util-genai/tests/test_upload.py index dd87b971e0..aa5fcb5b4a 100644 --- a/util/opentelemetry-util-genai/tests/test_upload.py +++ b/util/opentelemetry-util-genai/tests/test_upload.py @@ -44,7 +44,7 @@ types.InputMessage( role="assistant", parts=[ - types.ToolCall( + types.ToolCallRequest( id="get_capital_0", name="get_capital", arguments={"city": "Paris"}, From 1287d96db78cefd9c9caf189bf4ff86d03b4beca Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Mon, 2 Mar 2026 11:00:49 -0700 Subject: [PATCH 07/17] refactor(types): update inheritance structure --- .../instrumentation/google_genai/message.py | 4 +- .../src/opentelemetry/util/genai/types.py | 88 ++++++----- .../tests/test_toolcall.py | 146 +++--------------- 3 files changed, 74 insertions(+), 164 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py index 29ef112a6f..e402645644 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py @@ -27,7 +27,7 @@ MessagePart, OutputMessage, Text, - ToolCall, + ToolCallRequest, ToolCallResponse, ) @@ -130,7 +130,7 @@ def tool_call_id(name: str | None) -> str: ) if call := part.function_call: - return ToolCall( + return ToolCallRequest( id=call.id or tool_call_id(call.name), name=call.name or "", arguments=call.args, 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 e37d0bdf6a..709b9f5096 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -43,7 +43,10 @@ class ContentCapturingMode(Enum): @dataclass() class ToolCallRequest: - """Represents a tool call requested by the model + """Represents a tool call requested by the model (message part only). + + Use this for tool calls in message history. For execution tracking with spans + and metrics, use ToolCall instead. This model is specified as part of semconv in `GenAI messages Python models - ToolCallRequestPart `__. @@ -55,40 +58,6 @@ class ToolCallRequest: type: Literal["tool_call"] = "tool_call" -@dataclass() -class ToolCall(ToolCallRequest): - """Represents a tool call for execution tracking with spans and metrics. - - This type extends ToolCallRequest with additional fields for tracking tool execution - per the execute_tool span semantic conventions. - - Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md#execute-tool-span - - For simple message parts (tool calls requested by the model), consider using - ToolCallRequest instead to avoid unnecessary execution-tracking fields. - - Semantic convention attributes for execute_tool spans: - - gen_ai.operation.name: "execute_tool" (Required) - - gen_ai.tool.name: Name of the tool (Recommended) - - gen_ai.tool.call.id: Tool call identifier (Recommended if available) - - gen_ai.tool.type: Type classification - "function", "extension", or "datastore" (Recommended if available) - - gen_ai.tool.description: Tool description (Recommended if available) - - gen_ai.tool.call.arguments: Parameters passed to tool (Opt-In, may contain sensitive data) - - gen_ai.tool.call.result: Result returned by tool (Opt-In, may contain sensitive data) - - error.type: Error type if operation failed (Conditionally Required) - """ - - # Execution-only fields (used for execute_tool spans): - # gen_ai.tool.type - Tool type: "function", "extension", or "datastore" - tool_type: str | None = None - # gen_ai.tool.description - Description of what the tool does - tool_description: str | None = None - # gen_ai.tool.call.result - Result returned by the tool (Opt-In, may contain sensitive data) - tool_result: Any = None - # error.type - Error type if the tool call failed - error_type: str | None = None - - @dataclass() class ToolCallResponse: """Represents a tool call result sent to the model or a built-in tool call outcome and details @@ -194,7 +163,7 @@ class GenericToolDefinition: MessagePart = Union[ Text, ToolCallRequest, - ToolCall, + "ToolCall", ToolCallResponse, Blob, File, @@ -298,6 +267,53 @@ class LLMInvocation(GenAIInvocation): monotonic_start_s: float | None = None +@dataclass() +class ToolCall(GenAIInvocation): + """Represents a tool call for execution tracking with spans and metrics. + + This type extends GenAIInvocation (like LLMInvocation) for consistent lifecycle + management across all invocation types. It is NOT used as a MessagePart directly - + use ToolCallRequest for that purpose. + + Inherits from GenAIInvocation: + - context_token: Context tracking for span lifecycle + - span: Active span reference + - attributes: Custom attributes dict for extensibility + + Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md#execute-tool-span + + Semantic convention attributes for execute_tool spans: + - gen_ai.operation.name: "execute_tool" (Required) + - gen_ai.tool.name: Name of the tool (Recommended) + - gen_ai.tool.call.id: Tool call identifier (Recommended if available) + - gen_ai.tool.type: Type classification - "function", "extension", or "datastore" (Recommended if available) + - gen_ai.tool.description: Tool description (Recommended if available) + - gen_ai.tool.call.arguments: Parameters passed to tool (Opt-In, may contain sensitive data) + - gen_ai.tool.call.result: Result returned by tool (Opt-In, may contain sensitive data) + - error.type: Error type if operation failed (Conditionally Required) + """ + + # Message identification fields (same as ToolCallRequest) + # Note: These are required fields but must have defaults due to dataclass inheritance + name: str = "" + arguments: Any = None + id: str | None = None + type: Literal["tool_call"] = "tool_call" + + # Execution tracking fields (used for execute_tool spans): + # gen_ai.tool.type - Tool type: "function", "extension", or "datastore" + tool_type: str | None = None + # gen_ai.tool.description - Description of what the tool does + tool_description: str | None = None + # gen_ai.tool.call.result - Result returned by the tool (Opt-In, may contain sensitive data) + tool_result: Any = None + # error.type - Error type if the tool call failed + error_type: str | None = None + + # Timing field (not inherited from GenAIInvocation, matches LLMInvocation pattern) + monotonic_start_s: float | None = None + + @dataclass class Error: message: str diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index a06f39efdd..2e1148b132 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -12,153 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for ToolCallRequest and ToolCall types""" +"""Tests for ToolCallRequest and ToolCall inheritance structure""" import pytest from opentelemetry.util.genai.types import ( + GenAIInvocation, InputMessage, - OutputMessage, ToolCall, ToolCallRequest, ) -def test_toolcallrequest_basic(): - """Test basic ToolCallRequest instantiation""" - tcr = ToolCallRequest(arguments=None, name="get_weather", id=None) - assert tcr.name == "get_weather" - assert tcr.type == "tool_call" - assert tcr.arguments is None - assert tcr.id is None - - -def test_toolcallrequest_with_all_fields(): - """Test ToolCallRequest with all fields""" +def test_toolcallrequest_is_message_part(): + """ToolCallRequest is for message parts only""" tcr = ToolCallRequest( - name="get_weather", - arguments={"location": "Paris"}, - id="call_123", - ) - assert tcr.name == "get_weather" - assert tcr.arguments == {"location": "Paris"} - assert tcr.id == "call_123" - assert tcr.type == "tool_call" - - -def test_toolcallrequest_in_message(): - """Test ToolCallRequest works as message part""" - tcr = ToolCallRequest( - arguments={"location": "Paris"}, name="get_weather", id=None + arguments={"location": "Paris"}, name="get_weather", id="call_123" ) msg = InputMessage(role="user", parts=[tcr]) assert len(msg.parts) == 1 - assert msg.parts[0] == tcr - -def test_toolcall_inherits_from_toolcallrequest(): - """Test that ToolCall inherits from ToolCallRequest""" - tc = ToolCall(arguments=None, name="get_weather", id=None) - assert isinstance(tc, ToolCallRequest) - assert isinstance(tc, ToolCall) +def test_toolcall_inherits_from_genaiinvocation(): + """ToolCall inherits from GenAIInvocation for lifecycle management""" + tc = ToolCall(name="get_weather", arguments={"city": "Paris"}) + assert isinstance(tc, GenAIInvocation) + assert not isinstance(tc, ToolCallRequest) -def test_toolcall_has_execution_fields(): - """Test ToolCall has execution-only fields""" - tc = ToolCall(arguments=None, name="get_weather", id=None) - assert hasattr(tc, "tool_type") - assert hasattr(tc, "tool_description") - assert hasattr(tc, "tool_result") - assert hasattr(tc, "error_type") +def test_toolcall_has_attributes_dict(): + """ToolCall inherits attributes dict from GenAIInvocation""" + tc = ToolCall(name="test") + tc.attributes["custom.key"] = "value" + assert tc.attributes["custom.key"] == "value" -def test_toolcall_execution_fields_default_none(): - """Test ToolCall execution fields default to None""" - tc = ToolCall(arguments=None, name="get_weather", id=None) - assert tc.tool_type is None - assert tc.tool_description is None - assert tc.tool_result is None - assert tc.error_type is None - - -def test_toolcall_with_execution_fields(): - """Test ToolCall with execution fields set""" - tc = ToolCall( - name="get_weather", - arguments={"location": "Paris"}, - id="call_123", - tool_type="function", - tool_description="Get current weather", - tool_result={"temp": 20, "condition": "sunny"}, - ) - assert tc.name == "get_weather" - assert tc.tool_type == "function" - assert tc.tool_description == "Get current weather" - assert tc.tool_result == {"temp": 20, "condition": "sunny"} - - -def test_toolcall_with_error(): - """Test ToolCall with error_type set""" - tc = ToolCall( - arguments={"location": "Invalid"}, - name="get_weather", - id=None, - error_type="InvalidLocationError", - ) - assert tc.error_type == "InvalidLocationError" - assert tc.tool_result is None - -def test_toolcall_backward_compatibility(): - """Test ToolCall still works as message part (backward compatibility)""" - tc = ToolCall( - name="get_weather", - arguments={"location": "Paris"}, - id="call_123", - ) - # Should work in messages - msg = InputMessage(role="user", parts=[tc]) +def test_toolcall_in_message_part_union(): + """ToolCall can be used in messages despite not inheriting from ToolCallRequest""" + tc = ToolCall(name="get_weather", arguments={"city": "Paris"}) + msg = InputMessage(role="assistant", parts=[tc]) assert len(msg.parts) == 1 - - # Should work in output messages - out_msg = OutputMessage( - role="assistant", parts=[tc], finish_reason="tool_calls" - ) - assert len(out_msg.parts) == 1 - - -def test_toolcallrequest_no_execution_fields(): - """Test that ToolCallRequest doesn't have execution fields""" - tcr = ToolCallRequest(arguments=None, name="get_weather", id=None) - # ToolCallRequest should only have message part fields - assert not hasattr(tcr, "tool_type") - assert not hasattr(tcr, "tool_description") - assert not hasattr(tcr, "tool_result") - assert not hasattr(tcr, "error_type") - - -def test_mixed_types_in_message(): - """Test using both ToolCallRequest and ToolCall in messages""" - tcr = ToolCallRequest(arguments=None, name="simple_tool", id=None) - tc = ToolCall( - arguments=None, name="complex_tool", id=None, tool_type="function" - ) - - msg = InputMessage(role="user", parts=[tcr, tc]) - assert len(msg.parts) == 2 - assert isinstance(msg.parts[0], ToolCallRequest) - assert isinstance(msg.parts[1], ToolCall) - # ToolCall is also a ToolCallRequest - assert isinstance(msg.parts[1], ToolCallRequest) - - -def test_toolcall_tool_type_values(): - """Test valid tool_type values""" - for tool_type in ["function", "extension", "datastore"]: - tc = ToolCall( - arguments=None, name="test", id=None, tool_type=tool_type - ) - assert tc.tool_type == tool_type + assert isinstance(msg.parts[0], GenAIInvocation) if __name__ == "__main__": From 1bdd5c479dca89e91d69d0a7840a49813d95f1c6 Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Mon, 2 Mar 2026 15:37:26 -0700 Subject: [PATCH 08/17] fix: update opentelemetry-util-genai version constraints and introduce GenericPart for custom message types --- .../pyproject.toml | 2 +- .../instrumentation/google_genai/message.py | 11 ++++++++--- .../tests/requirements.oldest.txt | 2 +- .../pyproject.toml | 2 +- .../opentelemetry/instrumentation/vertexai/utils.py | 13 ++++++++++--- .../src/opentelemetry/util/genai/types.py | 10 ++++++++-- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml index a63b28d8b4..7d552d8ed4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "opentelemetry-api ~=1.37", "opentelemetry-instrumentation >=0.58b0, <2", "opentelemetry-semantic-conventions >=0.58b0, <2", - "opentelemetry-util-genai >= 0.3b0, <0.4b0", + "opentelemetry-util-genai >= 0.4b0, <0.5b0", ] [project.optional-dependencies] diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py index e402645644..0c01846784 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py @@ -23,6 +23,7 @@ from opentelemetry.util.genai.types import ( FinishReason, + GenericPart, InputMessage, MessagePart, OutputMessage, @@ -122,11 +123,15 @@ def tool_call_id(name: str | None) -> str: return Text(content=text) if data := part.inline_data: - return BlobPart(mime_type=data.mime_type or "", data=data.data or b"") + return GenericPart( + BlobPart(mime_type=data.mime_type or "", data=data.data or b"") + ) if data := part.file_data: - return FileDataPart( - mime_type=data.mime_type or "", uri=data.file_uri or "" + return GenericPart( + FileDataPart( + mime_type=data.mime_type or "", uri=data.file_uri or "" + ) ) if call := part.function_call: diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt index 357bbeccb3..63a90519d1 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt @@ -25,7 +25,7 @@ opentelemetry-api==1.37.0 opentelemetry-sdk==1.37.0 opentelemetry-semantic-conventions==0.58b0 opentelemetry-instrumentation==0.58b0 -opentelemetry-util-genai[upload]==0.3b0 +-e util/opentelemetry-util-genai[upload] fsspec==2025.9.0 diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml index 3d715a2a87..d33f16a592 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "opentelemetry-api ~= 1.37", "opentelemetry-instrumentation ~= 0.58b0", "opentelemetry-semantic-conventions ~= 0.58b0", - "opentelemetry-util-genai >= 0.2b0, <0.4b0", + "opentelemetry-util-genai >= 0.4b0, <0.5b0", ] [project.optional-dependencies] diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py index 6b280d2bc4..66b3c99550 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py @@ -54,6 +54,7 @@ from opentelemetry.util.genai.types import ( ContentCapturingMode, FinishReason, + GenericPart, MessagePart, Text, ToolCallRequest, @@ -354,13 +355,19 @@ def convert_content_to_message_parts( elif "inline_data" in part: part = part.inline_data parts.append( - BlobPart(mime_type=part.mime_type or "", data=part.data or b"") + GenericPart( + BlobPart( + mime_type=part.mime_type or "", data=part.data or b"" + ) + ) ) elif "file_data" in part: part = part.file_data parts.append( - FileDataPart( - mime_type=part.mime_type or "", uri=part.file_uri or "" + GenericPart( + FileDataPart( + mime_type=part.mime_type or "", uri=part.file_uri or "" + ) ) ) else: 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 709b9f5096..d5ebec3539 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -17,7 +17,7 @@ from contextvars import Token from dataclasses import dataclass, field from enum import Enum -from typing import Any, Literal, Type, Union +from typing import Any, Literal, NewType, Type, Union from typing_extensions import TypeAlias @@ -29,6 +29,12 @@ ContextToken: TypeAlias = Token[Context] +# GenericPart is used for provider-specific message part types that don't match +# the standard MessagePart types defined in semantic conventions. Wrap custom +# types with GenericPart(value) to explicitly opt-in to non-standard types. +# This will be removed in a future version when all instrumentations use core types. +GenericPart = NewType("GenericPart", object) + class ContentCapturingMode(Enum): # Do not capture content (default). @@ -169,7 +175,7 @@ class GenericToolDefinition: File, Uri, Reasoning, - Any, + GenericPart, # For provider-specific types; prefer standard types above ] From 57898bf5eb7133a1bc6b30a1c699983d06ed1594 Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Mon, 2 Mar 2026 15:47:05 -0700 Subject: [PATCH 09/17] fix version mismatch --- .../opentelemetry-instrumentation-google-genai/pyproject.toml | 2 +- .../opentelemetry-instrumentation-vertexai/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml index 7d552d8ed4..16f0dcea8a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "opentelemetry-api ~=1.37", "opentelemetry-instrumentation >=0.58b0, <2", "opentelemetry-semantic-conventions >=0.58b0, <2", - "opentelemetry-util-genai >= 0.4b0, <0.5b0", + "opentelemetry-util-genai >= 0.4b0.dev, <0.5b0", ] [project.optional-dependencies] diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml index d33f16a592..32dccd92bf 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "opentelemetry-api ~= 1.37", "opentelemetry-instrumentation ~= 0.58b0", "opentelemetry-semantic-conventions ~= 0.58b0", - "opentelemetry-util-genai >= 0.4b0, <0.5b0", + "opentelemetry-util-genai >= 0.4b0.dev, <0.5b0", ] [project.optional-dependencies] From b11de5c316f0ed65156f6119e0eefbd78de6df55 Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Wed, 4 Mar 2026 10:10:24 -0700 Subject: [PATCH 10/17] Convert GenericPart to a dataclass --- .../langchain/callback_handler.py | 11 ++++++++--- .../src/opentelemetry/util/genai/types.py | 17 +++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py b/instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py index 8f642567ca..d694857da4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py +++ b/instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py @@ -14,7 +14,7 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any, Optional, cast from uuid import UUID from langchain_core.callbacks import BaseCallbackHandler @@ -29,6 +29,7 @@ Error, InputMessage, LLMInvocation, + MessagePart, OutputMessage, Text, ) @@ -133,7 +134,11 @@ def on_chat_model_start( Text(content=text_value, type="text") ) - input_messages.append(InputMessage(parts=parts, role=role)) + input_messages.append( + InputMessage( + parts=cast(list[MessagePart], parts), role=role + ) + ) llm_invocation = LLMInvocation( request_model=request_model, @@ -206,7 +211,7 @@ def on_llm_end( role = chat_generation.message.type output_message = OutputMessage( role=role, - parts=parts, + parts=cast(list[MessagePart], parts), finish_reason=finish_reason, ) output_messages.append(output_message) 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 d5ebec3539..f1e7b106cb 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -17,7 +17,7 @@ from contextvars import Token from dataclasses import dataclass, field from enum import Enum -from typing import Any, Literal, NewType, Type, Union +from typing import Any, Literal, Type, Union from typing_extensions import TypeAlias @@ -29,11 +29,16 @@ ContextToken: TypeAlias = Token[Context] -# GenericPart is used for provider-specific message part types that don't match -# the standard MessagePart types defined in semantic conventions. Wrap custom -# types with GenericPart(value) to explicitly opt-in to non-standard types. -# This will be removed in a future version when all instrumentations use core types. -GenericPart = NewType("GenericPart", object) + +@dataclass() +class GenericPart: + """Used for provider-specific message part types that don't match + the standard MessagePart types defined in semantic conventions. Wrap custom + types with GenericPart(value=...) to explicitly opt-in to non-standard types. + This will be removed in a future version when all instrumentations use core types.""" + + value: Any + type: Literal["generic"] = "generic" class ContentCapturingMode(Enum): From 9bd35dc7f850c269fbda2f31126ca12b86ced748 Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Wed, 4 Mar 2026 10:41:44 -0700 Subject: [PATCH 11/17] patch openai to use toolcallrequest --- .../src/opentelemetry/instrumentation/openai_v2/patch.py | 4 ++-- .../src/opentelemetry/instrumentation/openai_v2/utils.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py index 3bc42f103c..0453eb26d4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py @@ -39,7 +39,7 @@ LLMInvocation, OutputMessage, Text, - ToolCall, + ToolCallRequest, ) from .instruments import Instruments @@ -910,7 +910,7 @@ def _set_output_messages(self): arguments = json.loads(arguments_str) except json.JSONDecodeError: arguments = arguments_str - tool_call_part = ToolCall( + tool_call_part = ToolCallRequest( name=tool_call.function_name, id=tool_call.tool_call_id, arguments=arguments, diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py index 7e5d2307ca..6afca130b6 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py @@ -41,7 +41,7 @@ LLMInvocation, OutputMessage, Text, - ToolCall, + ToolCallRequest, ToolCallResponse, ) @@ -452,7 +452,7 @@ def _prepare_input_messages(messages) -> List[InputMessage]: return chat_messages -def extract_tool_calls_new(tool_calls) -> list[ToolCall]: +def extract_tool_calls_new(tool_calls) -> list[ToolCallRequest]: parts = [] for tool_call in tool_calls: call_id = get_property_value(tool_call, "id") @@ -470,7 +470,9 @@ def extract_tool_calls_new(tool_calls) -> list[ToolCall]: arguments = arguments_str # TODO: support custom - parts.append(ToolCall(id=call_id, name=func_name, arguments=arguments)) + parts.append( + ToolCallRequest(id=call_id, name=func_name, arguments=arguments) + ) return parts From dc2b1b9dbf9b5ab01c0c7ef6f3b9e6639ae1a6e0 Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Wed, 4 Mar 2026 11:16:45 -0700 Subject: [PATCH 12/17] Refactor message part handling to use Blob and Uri types --- .../instrumentation/vertexai/utils.py | 45 +++++++++---------- .../test_chat_completions_experimental.py | 8 ++-- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py index 66b3c99550..9686d0dd7a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py @@ -52,13 +52,14 @@ ) from opentelemetry.semconv.attributes import server_attributes from opentelemetry.util.genai.types import ( + Blob, ContentCapturingMode, FinishReason, - GenericPart, MessagePart, Text, ToolCallRequest, ToolCallResponse, + Uri, ) from opentelemetry.util.genai.utils import get_content_capturing_mode from opentelemetry.util.types import AnyValue, AttributeValue @@ -309,21 +310,15 @@ def request_to_events( yield user_event(role=content.role, content=request_content) -@dataclass -class BlobPart: - data: bytes - mime_type: str - type: Literal["blob"] = "blob" - - -@dataclass -class FileDataPart: - mime_type: str - uri: str - type: Literal["file_data"] = "file_data" - - class Config: - extra = "allow" +def _modality_from_mime_type(mime_type: str) -> str: + """Infer modality from MIME type prefix.""" + if mime_type.startswith("image/"): + return "image" + if mime_type.startswith("video/"): + return "video" + if mime_type.startswith("audio/"): + return "audio" + return mime_type def convert_content_to_message_parts( @@ -354,20 +349,22 @@ def convert_content_to_message_parts( parts.append(Text(content=part.text)) elif "inline_data" in part: part = part.inline_data + mime_type = part.mime_type or "" parts.append( - GenericPart( - BlobPart( - mime_type=part.mime_type or "", data=part.data or b"" - ) + Blob( + mime_type=mime_type, + modality=_modality_from_mime_type(mime_type), + content=part.data or b"", ) ) elif "file_data" in part: part = part.file_data + mime_type = part.mime_type or "" parts.append( - GenericPart( - FileDataPart( - mime_type=part.mime_type or "", uri=part.file_uri or "" - ) + Uri( + mime_type=mime_type, + modality=_modality_from_mime_type(mime_type), + uri=part.file_uri or "", ) ) else: diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/test_chat_completions_experimental.py b/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/test_chat_completions_experimental.py index 5613f551a6..617d9a81c9 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/test_chat_completions_experimental.py +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/test_chat_completions_experimental.py @@ -74,7 +74,7 @@ def test_generate_content_with_files( "gen_ai.usage.output_tokens": 5, "server.address": "us-central1-aiplatform.googleapis.com", "server.port": 443, - "gen_ai.input.messages": '[{"role":"user","parts":[{"content":"Say this is a test","type":"text"},{"mime_type":"image/jpeg","uri":"https://images.pdimagearchive.org/collections/microscopic-delights/1lede-0021.jpg","type":"file_data"},{"data":"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==","mime_type":"image/jpeg","type":"blob"}]}]', + "gen_ai.input.messages": '[{"role":"user","parts":[{"content":"Say this is a test","type":"text"},{"mime_type":"image/jpeg","modality":"image","uri":"https://images.pdimagearchive.org/collections/microscopic-delights/1lede-0021.jpg","type":"uri"},{"mime_type":"image/jpeg","modality":"image","content":"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==","type":"blob"}]}]', "gen_ai.output.messages": '[{"role":"model","parts":[{"content":"This is a test.","type":"text"}],"finish_reason":"stop"}]', } @@ -97,12 +97,14 @@ def test_generate_content_with_files( {"content": "Say this is a test", "type": "text"}, { "mime_type": "image/jpeg", + "modality": "image", "uri": "https://images.pdimagearchive.org/collections/microscopic-delights/1lede-0021.jpg", - "type": "file_data", + "type": "uri", }, { - "data": b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x05\x00\x00\x00\x05\x08\x06\x00\x00\x00\x8do&\xe5\x00\x00\x00\x1cIDAT\x08\xd7c\xf8\xff\xff?\xc3\x7f\x06 \x05\xc3 \x12\x84\xd01\xf1\x82X\xcd\x04\x00\x0e\xf55\xcb\xd1\x8e\x0e\x1f\x00\x00\x00\x00IEND\xaeB`\x82", "mime_type": "image/jpeg", + "modality": "image", + "content": b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x05\x00\x00\x00\x05\x08\x06\x00\x00\x00\x8do&\xe5\x00\x00\x00\x1cIDAT\x08\xd7c\xf8\xff\xff?\xc3\x7f\x06 \x05\xc3 \x12\x84\xd01\xf1\x82X\xcd\x04\x00\x0e\xf55\xcb\xd1\x8e\x0e\x1f\x00\x00\x00\x00IEND\xaeB`\x82", "type": "blob", }, ), From 73b83694a23419108fec801ea64ddccdc385bac2 Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Mon, 9 Mar 2026 16:05:26 -0600 Subject: [PATCH 13/17] Add ServerToolCall and ServerToolCallResponse models with tests --- .../src/opentelemetry/util/genai/types.py | 37 ++++++++++++ .../tests/test_toolcall.py | 59 +++++++++++++++++++ 2 files changed, 96 insertions(+) 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 f1e7b106cb..746b7723ad 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -82,6 +82,41 @@ class ToolCallResponse: type: Literal["tool_call_response"] = "tool_call_response" +@dataclass() +class ServerToolCall: + """Represents a server-side tool call invocation. + + Server tool calls are executed by the model provider on the server side rather + than by the client application. Provider-specific tools (e.g., code_interpreter, + web_search) can have well-defined schemas defined by the respective providers. + + This model is specified as part of semconv in `GenAI messages Python models - ServerToolCallPart + `__. + """ + + name: str + server_tool_call: Any + id: str | None = None + type: Literal["server_tool_call"] = "server_tool_call" + + +@dataclass() +class ServerToolCallResponse: + """Represents a server-side tool call response. + + Contains the outcome and details of a server tool execution. Provider-specific + tools (e.g., code_interpreter, web_search) can have well-defined response schemas + defined by the respective providers. + + This model is specified as part of semconv in `GenAI messages Python models - ServerToolCallResponsePart + `__. + """ + + server_tool_call_response: Any + id: str | None = None + type: Literal["server_tool_call_response"] = "server_tool_call_response" + + @dataclass() class Text: """Represents text content sent to or received from the model @@ -176,6 +211,8 @@ class GenericToolDefinition: ToolCallRequest, "ToolCall", ToolCallResponse, + ServerToolCall, + ServerToolCallResponse, Blob, File, Uri, diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index 2e1148b132..d076f5e72b 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -19,6 +19,8 @@ from opentelemetry.util.genai.types import ( GenAIInvocation, InputMessage, + ServerToolCall, + ServerToolCallResponse, ToolCall, ToolCallRequest, ) @@ -55,5 +57,62 @@ def test_toolcall_in_message_part_union(): assert isinstance(msg.parts[0], GenAIInvocation) +def test_server_tool_call_basic(): + """ServerToolCall can be created with required fields""" + stc = ServerToolCall( + name="code_interpreter", + server_tool_call={"type": "code_interpreter", "code": "print(1)"}, + ) + assert stc.name == "code_interpreter" + assert stc.server_tool_call == { + "type": "code_interpreter", + "code": "print(1)", + } + assert stc.id is None + assert stc.type == "server_tool_call" + + +def test_server_tool_call_with_id(): + """ServerToolCall can have an optional id""" + stc = ServerToolCall( + name="web_search", + server_tool_call={"type": "web_search", "query": "weather"}, + id="stc_001", + ) + assert stc.id == "stc_001" + + +def test_server_tool_call_response_basic(): + """ServerToolCallResponse can be created with required fields""" + stcr = ServerToolCallResponse( + server_tool_call_response={ + "type": "code_interpreter", + "output": "1\n", + }, + ) + assert stcr.server_tool_call_response == { + "type": "code_interpreter", + "output": "1\n", + } + assert stcr.id is None + assert stcr.type == "server_tool_call_response" + + +def test_server_tool_call_in_message(): + """ServerToolCall and ServerToolCallResponse work as MessageParts""" + stc = ServerToolCall( + name="code_interpreter", + server_tool_call={"type": "code_interpreter", "code": "x = 1"}, + ) + 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], ServerToolCall) + assert isinstance(msg.parts[1], ServerToolCallResponse) + + if __name__ == "__main__": pytest.main([__file__, "-v"]) From dc4f7c24a8b1d0d29b07d3c349fe4736848ed0b0 Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Tue, 10 Mar 2026 10:12:13 -0600 Subject: [PATCH 14/17] refactor: streamline Blob and Uri handling in message processing --- .../instrumentation/google_genai/message.py | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py index 0c01846784..a7349f0c83 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py @@ -15,21 +15,20 @@ from __future__ import annotations import logging -from dataclasses import dataclass from enum import Enum -from typing import Literal from google.genai import types as genai_types from opentelemetry.util.genai.types import ( + Blob, FinishReason, - GenericPart, InputMessage, MessagePart, OutputMessage, Text, ToolCallRequest, ToolCallResponse, + Uri, ) @@ -40,23 +39,6 @@ class Role(str, Enum): TOOL = "tool" -@dataclass -class BlobPart: - data: bytes - mime_type: str - type: Literal["blob"] = "blob" - - -@dataclass -class FileDataPart: - mime_type: str - uri: str - type: Literal["file_data"] = "file_data" - - class Config: - extra = "allow" - - _logger = logging.getLogger(__name__) @@ -122,16 +104,22 @@ def tool_call_id(name: str | None) -> str: if (text := part.text) is not None: return Text(content=text) - if data := part.inline_data: - return GenericPart( - BlobPart(mime_type=data.mime_type or "", data=data.data or b"") + if inline_data := part.inline_data: + mime_type = inline_data.mime_type or "" + modality = mime_type.split("/")[0] if mime_type else "" + return Blob( + mime_type=mime_type, + modality=modality, + content=inline_data.data or b"", ) - if data := part.file_data: - return GenericPart( - FileDataPart( - mime_type=data.mime_type or "", uri=data.file_uri or "" - ) + if file_data := part.file_data: + mime_type = file_data.mime_type or "" + modality = mime_type.split("/")[0] if mime_type else "" + return Uri( + mime_type=mime_type, + modality=modality, + uri=file_data.file_uri or "", ) if call := part.function_call: From e1fdd08312bf9226ee2e79e6b3dfffac9e07ea6a Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Wed, 11 Mar 2026 10:18:50 -0600 Subject: [PATCH 15/17] convert anthropic to toolcallrequest and remove toolcall from messageparts --- .../opentelemetry/instrumentation/anthropic/utils.py | 10 ++++++---- .../src/opentelemetry/util/genai/types.py | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/utils.py b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/utils.py index 9914a55a22..7399b8831b 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/utils.py @@ -38,7 +38,7 @@ MessagePart, Reasoning, Text, - ToolCall, + ToolCallRequest, ToolCallResponse, ) @@ -114,7 +114,7 @@ def _convert_dict_block_to_part( if block_type == "tool_use": inp = block.get("input") - return ToolCall( + return ToolCallRequest( arguments=inp if isinstance(inp, dict) else None, name=str(block.get("name", "")), id=str(block.get("id", "")), @@ -144,7 +144,9 @@ def _convert_content_block_to_part( return Text(content=block.text) if isinstance(block, (ToolUseBlock, ServerToolUseBlock)): - return ToolCall(arguments=block.input, name=block.name, id=block.id) + return ToolCallRequest( + arguments=block.input, name=block.name, id=block.id + ) if isinstance(block, (ThinkingBlock, RedactedThinkingBlock)): content = ( @@ -229,7 +231,7 @@ def stream_block_state_to_part(state: StreamBlockState) -> MessagePart | None: arguments = json.loads(state.input_json) except ValueError: arguments = state.input_json - return ToolCall( + return ToolCallRequest( arguments=arguments, name=state.tool_name, id=state.tool_id, 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 746b7723ad..88debc9e90 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -209,7 +209,6 @@ class GenericToolDefinition: MessagePart = Union[ Text, ToolCallRequest, - "ToolCall", ToolCallResponse, ServerToolCall, ServerToolCallResponse, From e7f07f655508be3451dc654bf204823bc885129d Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Tue, 17 Mar 2026 09:17:59 -0600 Subject: [PATCH 16/17] move error.type to base genai class --- .../src/opentelemetry/util/genai/types.py | 3 +-- 1 file changed, 1 insertion(+), 2 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 c9c1ce107e..afad08e2f4 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py @@ -259,6 +259,7 @@ class GenAIInvocation: context_token: ContextToken | None = None span: Span | None = None attributes: dict[str, Any] = field(default_factory=_new_str_any_dict) + error_type: str | None = None @dataclass @@ -371,8 +372,6 @@ class ToolCall(GenAIInvocation): tool_description: str | None = None # gen_ai.tool.call.result - Result returned by the tool (Opt-In, may contain sensitive data) tool_result: Any = None - # error.type - Error type if the tool call failed - error_type: str | None = None # Timing field (not inherited from GenAIInvocation, matches LLMInvocation pattern) monotonic_start_s: float | None = None From 01ab8ae1cc666b8c53f99b67bb700d9dd2ec77bd Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Fri, 27 Mar 2026 10:14:21 -0600 Subject: [PATCH 17/17] update version requirements --- .../pyproject.toml | 6 +++--- .../tests/requirements.oldest.txt | 8 ++++---- .../opentelemetry-instrumentation-vertexai/pyproject.toml | 6 +++--- .../tests/requirements.oldest.txt | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml index 16f0dcea8a..87b30343e3 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml @@ -39,9 +39,9 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "opentelemetry-api ~=1.37", - "opentelemetry-instrumentation >=0.58b0, <2", - "opentelemetry-semantic-conventions >=0.58b0, <2", + "opentelemetry-api ~=1.39", + "opentelemetry-instrumentation >=0.60b0, <2", + "opentelemetry-semantic-conventions >=0.60b0, <2", "opentelemetry-util-genai >= 0.4b0.dev, <0.5b0", ] diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt index 63a90519d1..092a723ec4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt @@ -21,10 +21,10 @@ pytest-vcr==1.0.2 google-auth==2.15.0 google-genai==1.32.0 -opentelemetry-api==1.37.0 -opentelemetry-sdk==1.37.0 -opentelemetry-semantic-conventions==0.58b0 -opentelemetry-instrumentation==0.58b0 +opentelemetry-api==1.39.0 +opentelemetry-sdk==1.39.0 +opentelemetry-semantic-conventions==0.60b0 +opentelemetry-instrumentation==0.60b0 -e util/opentelemetry-util-genai[upload] fsspec==2025.9.0 diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml index 32dccd92bf..e6090642f6 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml @@ -26,9 +26,9 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "opentelemetry-api ~= 1.37", - "opentelemetry-instrumentation ~= 0.58b0", - "opentelemetry-semantic-conventions ~= 0.58b0", + "opentelemetry-api ~= 1.39", + "opentelemetry-instrumentation ~= 0.60b0", + "opentelemetry-semantic-conventions ~= 0.60b0", "opentelemetry-util-genai >= 0.4b0.dev, <0.5b0", ] diff --git a/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.oldest.txt b/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.oldest.txt index 910781a4e5..d02e3dc8e4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.oldest.txt +++ b/instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/requirements.oldest.txt @@ -65,10 +65,10 @@ grpcio>=1.75.1 ; python_version >= "3.14" shapely==2.0.6 ; python_version < "3.10" shapely==2.1.2 ; python_version >= "3.10" # when updating, also update in pyproject.toml -opentelemetry-api==1.37 -opentelemetry-sdk==1.37 -opentelemetry-semantic-conventions==0.58b0 -opentelemetry-instrumentation==0.58b0 +opentelemetry-api==1.39 +opentelemetry-sdk==1.39 +opentelemetry-semantic-conventions==0.60b0 +opentelemetry-instrumentation==0.60b0 # opentelemetry-util-genai[upload]==0.2b0 -e util/opentelemetry-util-genai[upload] fsspec==2025.9.0