From 321ea308f1a16bccde5bb2b37593c58e6d7fa43f Mon Sep 17 00:00:00 2001 From: Keith Decker Date: Thu, 19 Feb 2026 09:51:05 -0700 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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"},