From 4719a79fe8f0669db5c234306d82898dd7a41653 Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Fri, 13 Feb 2026 18:31:02 +1100 Subject: [PATCH 1/7] openai-agents: Handle various types of span_data.response.output parts --- .../openai_agents/span_processor.py | 474 +++++++++++++++-- .../tests/test_tracer.py | 497 +++++++++++++++++- 2 files changed, 920 insertions(+), 51 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py index d1dce8ec5e..620dd81c7a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py @@ -18,6 +18,7 @@ import importlib import logging +import json from dataclasses import dataclass from datetime import datetime, timezone from enum import Enum @@ -371,10 +372,20 @@ def safe_json_dumps(obj: Any) -> str: """Safely convert object to JSON string (fallback to str).""" try: return gen_ai_json_dumps(obj) - except (TypeError, ValueError): + except (TypeError, ValueError) as e: + logger.warning("Failed to serialize object to JSON: %s", e) return str(obj) +def safe_json_loads(s: str) -> Any: + """Safely parse JSON string (fallback to original string).""" + try: + return json.loads(s) + except json.JSONDecodeError as e: + logger.warning("Failed to parse JSON string: %s", e) + return s + + def _as_utc_nano(dt: datetime) -> int: """Convert datetime to UTC nanoseconds timestamp.""" return int(dt.astimezone(timezone.utc).timestamp() * 1_000_000_000) @@ -873,6 +884,373 @@ def _normalize_messages_to_role_parts( return normalized + def _normalize_response_output_part(self, item: Any) -> list[dict[str, Any]]: + part_type = getattr(item, "type", None) + if part_type == "message": # ResponseOutputMessage + parts = [] + content = getattr(item, "content", None) + if isinstance(content, Sequence): + for c in content: + content_type = getattr(c, "type", None) + if content_type == "output_text": + parts.append({ + "type": "text", + "annotations": ( # out of spec but useful + ["readacted"] + if not self.include_sensitive_data + else [ + a.to_dict() if hasattr(a, "to_dict") else str(a) + for a in getattr(c, "annotations", []) + ] + ), + "content": ( + "readacted" + if not self.include_sensitive_data + else getattr(c, "text", None) + ) + }) + elif content_type == "refusal": + parts.append({ + "type": "refusal", # custom type + "content": ( + "readacted" + if not self.include_sensitive_data + else getattr(c, "refusal", None) + ) + }) + else: + parts.append({ + "type": "text", + "content": ( + "readacted" + if not self.include_sensitive_data + else str(content) + ) + }) + return parts + if part_type == "file_search_call": # ResponseFileSearchToolCall + return [{ + "type": "tool_call", + "name": "file_search", + "id": getattr(item, "id", None), + "arguments": ({ + "queries": ["readacted"] + if not self.include_sensitive_data + else getattr(item, "queries", None) + }) + }] + elif part_type == "function_call": # ResponseFunctionToolCall + return [{ + "type": "tool_call", + "name": getattr(item, "name", None), + "id": getattr(item, "id", None), + "arguments": ( + "readacted" + if not self.include_sensitive_data + else safe_json_loads(getattr(item, "arguments", None)) + ) + }] + elif part_type == "web_search_call": # ResponseFunctionWebSearch + action = getattr(item, "action", None) + action_type = getattr(action, "type", None) + action_obj = ( + {"type": action_type} if not self.include_sensitive_data + else action.to_dict() if hasattr(action, "to_dict") else str(action) + ) + return [{ + "type": "tool_call", + "name": "web_search", + "id": getattr(item, "id", None), + "arguments": { + "action": action_obj, + } + }] + elif part_type == "computer_call": # ResponseComputerToolCall + action = getattr(item, "action", None) + action_type = getattr(action, "type", None) + action_obj = ( + {"type": action_type} if not self.include_sensitive_data + else action.to_dict() if hasattr(action, "to_dict") else str(action) + ) + return [{ + "type": "tool_call", + "name": "computer", + "id": getattr(item, "id", None), + "arguments": { + "action": action_obj, + } + }] + elif part_type == "reasoning": # ResponseReasoningItem + content = getattr(item, "content", None) + content_str = str.join("\n", [ + getattr(c, "text", "") if hasattr(c, "text") else str(c) + for c in content + ]) if isinstance(content, Sequence) else str(content) + return [{ + "type": "reasoning", + "content": ( + "readacted" + if not self.include_sensitive_data + else content_str + ) + }] + elif part_type == "compaction": # ResponseCompactionItem + return [{ + "type": "compaction", # custom type + "content": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "encrypted_content", None) + ) + }] + elif part_type == "image_generation_call": # ImageGenerationCall + return [{ + "type": "tool_call_response", + "name": "image_generation", + "id": getattr(item, "id", None), + "response": { + "result": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "result", None) + ) + } + }] + elif part_type == "code_interpreter_call": # ResponseCodeInterpreterToolCall + return [{ + "type": "tool_call", + "name": "code_interpreter", + "id": getattr(item, "id", None), + "arguments": { + "container_id": getattr(item, "container_id", None), + "code": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "code", None) + ) + } + }, { + "type": "tool_call_response", + "name": "code_interpreter", + "id": getattr(item, "id", None), + "response": ( + "readacted" + if not self.include_sensitive_data + else [ + output.to_dict() for output in getattr(item, "outputs", []) + ] + ) + }] + elif part_type == "local_shell_call": # LocalShellCall + action = getattr(item, "action", None) + return [{ + "type": "tool_call", + "name": "local_shell", + "id": getattr(item, "id", None), + "arguments": { + "type": getattr(action, "type", None), + "timeout_ms": getattr(action, "timeout_ms", None), + "command": ( + ["readacted"] + if not self.include_sensitive_data + else getattr(action, "command", None) + ), + "env": ( + "readacted" + if not self.include_sensitive_data + else getattr(action, "env", None) + ), + "user": ( + "readacted" + if not self.include_sensitive_data + else getattr(action, "user", None) + ), + "working_directory": ( + "readacted" + if not self.include_sensitive_data + else getattr(action, "working_directory", None) + ) + } + }] + elif part_type == "shell_call": # ResponseFunctionShellToolCall + action = getattr(item, "action", None) + return [{ + "type": "tool_call", + "name": "shell", + "id": getattr(item, "id", None), + "arguments": { + "created_by": getattr(action, "created_by", None), + "call_id": getattr(item, "call_id", None), + "environment": getattr(action, "environment", None), + "commands": ( + ["readacted"] + if not self.include_sensitive_data + else getattr(action, "commands", None) + ), + "max_output_length": getattr(action, "max_output_length", None), + "timeout_ms": getattr(action, "timeout_ms", None), + } + }] + elif part_type == "shell_call_output": # ResponseFunctionShellToolCallOutput + return [{ + "type": "tool_call_response", + "name": "shell", + "id": getattr(item, "id", None), + "response": { + "created_by": getattr(item, "created_by", None), + "call_id": getattr(item, "call_id", None), + "result": ( + ["readacted"] + if not self.include_sensitive_data + else [output.to_dict() for output in getattr(item, "output", [])] + ) + } + }] + elif part_type == "apply_patch_call": # ResponseApplyPatchToolCall + operation = getattr(item, "operation", None) + operation_type = getattr(operation, "type", None) + return [{ + "type": "tool_call", + "name": "apply_patch", + "id": getattr(item, "id", None), + "arguments": { + "created_by": getattr(item, "created_by", None), + "call_id": getattr(item, "call_id", None), + "operation": ( + { "type": operation_type } + if not self.include_sensitive_data + else operation.to_dict() if hasattr(operation, "to_dict") else safe_json_dumps(operation) + ), + } + }] + elif part_type == "apply_patch_call_output": # ResponseApplyPatchToolCallOutput + return [{ + "type": "tool_call_response", + "name": "apply_patch", + "id": getattr(item, "id", None), + "response": { + "created_by": getattr(item, "created_by", None), + "call_id": getattr(item, "call_id", None), + "status": getattr(item, "status", None), + "output": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "output", None) + ), + } + }] + elif part_type == "mcp_call": # McpCall + parts = [{ + "type": "tool_call", + "name": "mcp_call", + "id": getattr(item, "id", None), + "arguments": { + "server": getattr(item, "server_label", None), + "tool_name": getattr(item, "name", None), + "tool_args": ( + "readacted" + if not self.include_sensitive_data + else safe_json_loads(getattr(item, "arguments", None)) + ), + } + }] + if getattr(item, "output", None) or getattr(item, "error", None): + parts.append({ + "type": "tool_call_response", + "name": "mcp_call", + "id": getattr(item, "id", None), + "response": { + "output": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "output", None) + ), + "error": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "error", None) + ), + "status": getattr(item, "status", None), + } + }) + return parts + elif part_type == "mcp_list_tools": # McpListTools + parts = [{ + "type": "tool_call", + "name": "mcp_list_tools", + "id": getattr(item, "id", None), + "arguments": { + "server": getattr(item, "server_label", None), + } + }] + if getattr(item, "tools", None) or getattr(item, "error", None): + parts.append({ + "type": "tool_call_response", + "name": "mcp_list_tools", + "id": getattr(item, "id", None), + "response": { + "tools": ( + "readacted" + if not self.include_sensitive_data + else [tool.to_dict() for tool in getattr(item, "tools", [])] + ), + "error": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "error", None) + ), + } + }) + return parts + elif part_type == "mcp_approval_request": # McpApprovalRequest + return [{ + "type": "tool_call", + "name": "mcp_approval_request", + "id": getattr(item, "id", None), + "arguments": { + "server": getattr(item, "server_label", None), + "tool_name": getattr(item, "name", None), + "tool_args": ( + "readacted" + if not self.include_sensitive_data + else safe_json_loads(getattr(item, "arguments", None)) + ), + } + }] + elif part_type == "custom_tool_call": # ResponseCustomToolCall + return [{ + "type": "tool_call", + "name": getattr(item, "name", None), + "id": getattr(item, "id", None), + "arguments": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "input", None) + ) + }] + + # Fallback: content string attribute + txt = getattr(item, "content", None) + if isinstance(txt, str) and txt: + return [{ + "type": "text", + "content": ( + "readacted" + if not self.include_sensitive_data + else txt + ), + }] + else: + # Fallback: stringified + return [{ + "type": "text", + "content": ( + "readacted" + if not self.include_sensitive_data + else safe_json_dumps(item) + ), + }] + def _normalize_output_messages_to_role_parts( self, span_data: Any ) -> list[dict[str, Any]]: @@ -885,55 +1263,32 @@ def _normalize_output_messages_to_role_parts( parts: list[dict[str, Any]] = [] finish_reason: Optional[str] = None - # Response span: prefer consolidated output_text + # Response span: use span_data.response.output, or fall back to consolidated output_text response = getattr(span_data, "response", None) if response is not None: - # Collect text content - output_text = getattr(response, "output_text", None) - if isinstance(output_text, str) and output_text: - parts.append( - { - "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else output_text - ), - } - ) + output = getattr(response, "output", None) + if isinstance(output, Sequence): + for item in output: + parts.extend(self._normalize_response_output_part(item)) + + # Capture finish_reason from parts when present + fr = getattr(item, "finish_reason", None) + if isinstance(fr, str) and not finish_reason: + finish_reason = fr else: - output = getattr(response, "output", None) - if isinstance(output, Sequence): - for item in output: - # ResponseOutputMessage may have a string representation - txt = getattr(item, "content", None) - if isinstance(txt, str) and txt: - parts.append( - { - "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else txt - ), - } - ) - else: - # Fallback: stringified - parts.append( - { - "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else str(item) - ), - } - ) - # Capture finish_reason from parts when present - fr = getattr(item, "finish_reason", None) - if isinstance(fr, str) and not finish_reason: - finish_reason = fr + # Collect text content + output_text = getattr(response, "output_text", None) + if isinstance(output_text, str) and output_text: + parts.append( + { + "type": "text", + "content": ( + "readacted" + if not self.include_sensitive_data + else output_text + ), + } + ) # Generation span: use span_data.output if not parts: @@ -1056,10 +1411,16 @@ def _build_content_payload(self, span: Span[Any]) -> ContentPayload: elif _is_instance_of(span_data, ResponseSpanData): span_input = getattr(span_data, "input", None) + response_obj = getattr(span_data, "response", None) if capture_messages and span_input: payload.input_messages = ( self._normalize_messages_to_role_parts(span_input) ) + + if capture_system and response_obj and hasattr(response_obj, "instructions"): + payload.system_instructions = self._normalize_to_text_parts( + response_obj.instructions + ) if capture_system and span_input: sys_instr = self._collect_system_instructions(span_input) if sys_instr: @@ -2029,6 +2390,25 @@ def _get_attributes_from_response_span_data( if output_tokens is not None: yield GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens + # Tool definitions from response + if self._capture_tool_definitions and hasattr( + span_data.response, "tools" + ): + def _serialize_tool_value(value: Any) -> Optional[str]: + if value is None: + return None + return { + "name": getattr(value, "name", None), + "type": getattr(value, "type", None), + "description": getattr(value, "description", None), + "parameters": getattr(value, "parameters", None), + } + + yield ( + GEN_AI_TOOL_DEFINITIONS, + safe_json_dumps(list(map(_serialize_tool_value, span_data.response.tools))), + ) + # Input/output messages if ( self.include_sensitive_data diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py index 1f21ab25c0..57bc92335e 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py @@ -25,6 +25,54 @@ set_trace_processors, trace, ) +from openai.types.responses import ( # noqa: E402 + ResponseOutputMessage, + ResponseOutputText, + ResponseOutputRefusal, + ResponseFileSearchToolCall, + ResponseFunctionToolCall, + ResponseFunctionWebSearch, + ResponseComputerToolCall, + ResponseReasoningItem, + ResponseCompactionItem, + ResponseCodeInterpreterToolCall, + ResponseFunctionShellToolCall, + ResponseFunctionShellToolCallOutput, + ResponseApplyPatchToolCall, + ResponseApplyPatchToolCallOutput, + ResponseCustomToolCall, +) +from openai.types.responses.response_output_item import ( # noqa: E402 + ImageGenerationCall, + LocalShellCall, + LocalShellCallAction, + McpCall, + McpListTools, + McpApprovalRequest, + McpListToolsTool, +) +from openai.types.responses.response_apply_patch_tool_call import ( # noqa: E402 + OperationCreateFile, +) +from openai.types.responses.response_reasoning_item import ( # noqa: E402 + Content, +) +from openai.types.responses.response_code_interpreter_tool_call import ( # noqa: E402 + OutputLogs, +) +from openai.types.responses.response_function_shell_tool_call_output import ( # noqa: E402 + Output, + OutputOutcomeExit, +) +from openai.types.responses.response_function_web_search import ( # noqa: E402 + ActionSearch as ActionWebSearch, +) +from openai.types.responses.response_computer_tool_call import ( # noqa: E402 + ActionClick, +) +from openai.types.responses.response_function_shell_tool_call import ( # noqa: E402 + Action as ActionShellCall, +) from opentelemetry.instrumentation.openai_agents import ( # noqa: E402 OpenAIAgentsInstrumentor, @@ -83,6 +131,7 @@ def test_generation_span_creates_client_span(): with trace("workflow"): with generation_span( input=[{"role": "user", "content": "hi"}], + output=[{"role": "assistant", "content": "hello"}], model="gpt-4o-mini", model_config={ "temperature": 0.2, @@ -489,7 +538,237 @@ def __init__(self) -> None: self.id = "resp-123" self.model = "gpt-4o-mini" self.usage = _Usage(42, 9) - self.output = [{"finish_reason": "stop"}] + self.output = [ + # message type with output_text + ResponseOutputMessage( + id="msg-1", + role="assistant", + type="message", + status="completed", + content=[ + ResponseOutputText( + type="output_text", text="Hello!", annotations=[] + ) + ], + ), + # message type with refusal + ResponseOutputMessage( + id="msg-2", + role="assistant", + type="message", + status="completed", + content=[ + ResponseOutputRefusal( + type="refusal", refusal="I cannot do that" + ) + ], + ), + # reasoning + ResponseReasoningItem( + id="reason-1", + type="reasoning", + summary=[], + content=[ + Content(type="reasoning_text", text="Step 1: Think"), + Content(type="reasoning_text", text="Step 2: Act"), + ], + ), + # compaction + ResponseCompactionItem( + id="compact-1", + type="compaction", + encrypted_content="encrypted_data", + ), + # file_search_call + ResponseFileSearchToolCall( + type="file_search_call", + id="fs-123", + status="completed", + queries=["search query"], + ), + # function_call + ResponseFunctionToolCall( + name="get_weather", + id="fc-123", + type="function_call", + call_id="call-fc-123", + arguments='{"city": "Paris"}', + ), + # web_search_call + ResponseFunctionWebSearch( + id="ws-123", + type="web_search_call", + status="completed", + action=ActionWebSearch(type="search", query="test"), + ), + # computer_call + ResponseComputerToolCall( + id="cc-123", + type="computer_call", + call_id="call-cc-123", + status="completed", + pending_safety_checks=[], + action=ActionClick( + type="click", x=100, y=200, button="left" + ), + ), + # image_generation_call + ImageGenerationCall( + id="ig-123", + status="completed", + type="image_generation_call", + result="image_url", + ), + # code_interpreter_call + ResponseCodeInterpreterToolCall( + id="ci-123", + type="code_interpreter_call", + status="completed", + container_id="cont-123", + code="print('hello')", + outputs=[OutputLogs(type="logs", logs="hello")], + ), + # local_shell_call + LocalShellCall( + id="ls-123", + call_id="call-ls-123", + type="local_shell_call", + status="completed", + action=LocalShellCallAction( + type="exec", + timeout_ms=5000, + command=["ls", "-la"], + env={"PATH": "/usr/bin"}, + user="root", + working_directory="/tmp", + ), + ), + # shell_call + ResponseFunctionShellToolCall( + id="sh-123", + type="shell_call", + status="completed", + call_id="call-123", + action=ActionShellCall( + commands=["echo hello"], + max_output_length=1000, + timeout_ms=5000, + ), + ), + # shell_call_output + ResponseFunctionShellToolCallOutput( + id="sho-123", + type="shell_call_output", + status="completed", + call_id="call-123", + output=[ + Output( + stdout="shell output", + stderr="", + outcome=OutputOutcomeExit(type="exit", exit_code=0), + ) + ], + ), + # apply_patch_call + ResponseApplyPatchToolCall( + id="ap-123", + type="apply_patch_call", + status="completed", + created_by="agent", + call_id="call-123", + operation=OperationCreateFile( + type="create_file", + diff="content", + path="/tmp/test.txt", + ), + ), + # apply_patch_call_output + ResponseApplyPatchToolCallOutput( + id="apo-123", + type="apply_patch_call_output", + created_by="agent", + call_id="call-123", + status="completed", + output="Applied successfully", + ), + # mcp_call with output + McpCall( + id="mcp-123", + server_label="server1", + name="tool_name", + type="mcp_call", + arguments='{"key": "value"}', + output="result", + error=None, + status="completed", + ), + # mcp_call with error + McpCall( + id="mcp-124", + server_label="server1", + name="tool_name", + type="mcp_call", + arguments='{"key": "value"}', + output=None, + error="Some error", + status="failed", + ), + # mcp_call without output (no response part) + McpCall( + id="mcp-125", + server_label="server2", + name="another_tool", + type="mcp_call", + arguments='{"key": "value2"}', + output=None, + error=None, + ), + # mcp_list_tools with tools + McpListTools( + id="mcpl-123", + server_label="server1", + type="mcp_list_tools", + tools=[ + McpListToolsTool(name="tool1", input_schema={}), + McpListToolsTool(name="tool2", input_schema={}), + ], + error=None, + ), + # mcp_list_tools without tools (no response part) + McpListTools( + id="mcpl-124", + server_label="server2", + type="mcp_list_tools", + tools=[], + error=None, + ), + # mcp_approval_request + McpApprovalRequest( + id="mcpa-123", + server_label="server1", + name="dangerous_tool", + type="mcp_approval_request", + arguments='{"action": "delete"}', + ), + # custom_tool_call + ResponseCustomToolCall( + name="custom_tool", + id="ct-123", + type="custom_tool_call", + call_id="call-ct-123", + input='input', + ), + # fallback with content string + { + "type": "unknown_type", + "content": "fallback content", + }, + # fallback to stringified (no content attribute) + { + "type": "another_unknown", + "data": "some data", + }, + ] try: with trace("workflow"): @@ -513,9 +792,219 @@ def __init__(self) -> None: ) assert response.attributes[GenAI.GEN_AI_USAGE_INPUT_TOKENS] == 42 assert response.attributes[GenAI.GEN_AI_USAGE_OUTPUT_TOKENS] == 9 - assert response.attributes[GenAI.GEN_AI_RESPONSE_FINISH_REASONS] == ( - "stop", - ) + + # Check output messages are properly normalized + output_messages = json.loads(response.attributes[GEN_AI_OUTPUT_MESSAGES]) + assert len(output_messages) == 1 + assert output_messages[0]["role"] == "assistant" + parts = output_messages[0]["parts"] + tool_calls_by_id = { + part["id"]: {k: v for k, v in part.items() if k != "id"} + for part in parts + if part.get("type") == "tool_call" + } + tool_call_responses_by_id = { + part["id"]: {k: v for k, v in part.items() if k != "id"} + for part in parts + if part.get("type") == "tool_call_response" + } + + assert parts[0] == { + "type": "text", + "content": "Hello!", + "annotations": [], + } + + assert parts[1] == { + "type": "refusal", + "content": "I cannot do that", + } + + assert parts[2] == { + "type": "reasoning", + "content": "Step 1: Think\nStep 2: Act", + } + + assert parts[3] == { + "type": "compaction", + "content": "encrypted_data", + } + + assert tool_calls_by_id["fs-123"] == { + "type": "tool_call", + "name": "file_search", + "arguments": {"queries": ["search query"]}, + } + + assert tool_calls_by_id["fc-123"] == { + "type": "tool_call", + "name": "get_weather", + "arguments": {"city": "Paris"}, + } + + assert tool_calls_by_id["ws-123"] == { + "type": "tool_call", + "name": "web_search", + "arguments": {"action": {"type": "search", "query": "test"}}, + } + + assert tool_calls_by_id["cc-123"] == { + "type": "tool_call", + "name": "computer", + "arguments": { + "action": { + "type": "click", + "x": 100, + "y": 200, + "button": "left", + } + }, + } + + assert tool_calls_by_id["ci-123"] == { + "type": "tool_call", + "name": "code_interpreter", + "arguments": { + "code": "print('hello')", + "container_id": "cont-123", + }, + } + + assert tool_calls_by_id["ls-123"] == { + "type": "tool_call", + "name": "local_shell", + "arguments": { + "command": ["ls", "-la"], + "env": {"PATH": "/usr/bin"}, + "type": "exec", + "user": "root", + "working_directory": "/tmp", + "timeout_ms": 5000, + }, + } + + assert tool_calls_by_id["sh-123"] == { + "type": "tool_call", + "name": "shell", + "arguments": { + "call_id": "call-123", + "commands": ["echo hello"], + "created_by": None, + "environment": None, + "max_output_length": 1000, + "timeout_ms": 5000, + }, + } + + assert tool_calls_by_id["ap-123"] == { + "type": "tool_call", + "name": "apply_patch", + "arguments": { + "call_id": "call-123", + "operation": { + "type": "create_file", + "diff": "content", + "path": "/tmp/test.txt", + }, + "created_by": "agent", + }, + } + + assert tool_call_responses_by_id["apo-123"] == { + "type": "tool_call_response", + "name": "apply_patch", + "response": { + "call_id": "call-123", + "created_by": "agent", + "status": "completed", + "output": "Applied successfully", + }, + } + + assert tool_calls_by_id["mcp-123"] == { + "type": "tool_call", + "name": "mcp_call", + "arguments": { + "server": "server1", + "tool_name": "tool_name", + "tool_args": {"key": "value"}, + }, + } + + assert tool_call_responses_by_id["mcp-123"] == { + "type": "tool_call_response", + "name": "mcp_call", + "response": { + "output": "result", + "error": None, + "status": "completed", + }, + } + + assert tool_call_responses_by_id["mcp-124"] == { + "type": "tool_call_response", + "name": "mcp_call", + "response": { + "output": None, + "error": "Some error", + "status": "failed", + }, + } + + assert tool_calls_by_id["mcp-125"] == { + "type": "tool_call", + "name": "mcp_call", + "arguments": { + "server": "server2", + "tool_name": "another_tool", + "tool_args": {"key": "value2"}, + }, + } + + assert tool_calls_by_id["mcpl-123"] == { + "type": "tool_call", + "name": "mcp_list_tools", + "arguments": { + "server": "server1", + }, + } + + assert tool_call_responses_by_id["mcpl-123"] == { + "type": "tool_call_response", + "name": "mcp_list_tools", + "response": { + "error": None, + "tools": [ + {"name": "tool1", "input_schema": {}}, + {"name": "tool2", "input_schema": {}}, + ] + }, + } + + assert tool_calls_by_id["mcpl-124"] == { + "type": "tool_call", + "name": "mcp_list_tools", + "arguments": { + "server": "server2", + }, + } + + assert tool_calls_by_id["mcpa-123"] == { + "type": "tool_call", + "name": "mcp_approval_request", + "arguments": { + "server": "server1", + "tool_name": "dangerous_tool", + "tool_args": {"action": "delete"}, + }, + } + + assert tool_calls_by_id["ct-123"] == { + "type": "tool_call", + "name": "custom_tool", + "arguments": "input", + } + finally: instrumentor.uninstrument() exporter.clear() From 0771f6e0ba3a2a47d7c37c226d6fdf3c98d238d0 Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Wed, 18 Feb 2026 14:25:15 +1100 Subject: [PATCH 2/7] lint and fix compat with older openai packages --- .../openai_agents/span_processor.py | 696 ++++++++++-------- .../tests/test_tracer.py | 171 +++-- 2 files changed, 513 insertions(+), 354 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py index 620dd81c7a..dcc836d023 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py @@ -17,8 +17,8 @@ from __future__ import annotations import importlib -import logging import json +import logging from dataclasses import dataclass from datetime import datetime, timezone from enum import Enum @@ -884,7 +884,9 @@ def _normalize_messages_to_role_parts( return normalized - def _normalize_response_output_part(self, item: Any) -> list[dict[str, Any]]: + def _normalize_response_output_part( + self, item: Any + ) -> list[dict[str, Any]]: part_type = getattr(item, "type", None) if part_type == "message": # ResponseOutputMessage parts = [] @@ -893,363 +895,450 @@ def _normalize_response_output_part(self, item: Any) -> list[dict[str, Any]]: for c in content: content_type = getattr(c, "type", None) if content_type == "output_text": - parts.append({ - "type": "text", - "annotations": ( # out of spec but useful - ["readacted"] - if not self.include_sensitive_data - else [ - a.to_dict() if hasattr(a, "to_dict") else str(a) - for a in getattr(c, "annotations", []) - ] - ), - "content": ( - "readacted" - if not self.include_sensitive_data - else getattr(c, "text", None) - ) - }) + parts.append( + { + "type": "text", + "annotations": ( # out of spec but useful + ["readacted"] + if not self.include_sensitive_data + else [ + a.to_dict() + if hasattr(a, "to_dict") + else str(a) + for a in getattr(c, "annotations", []) + ] + ), + "content": ( + "readacted" + if not self.include_sensitive_data + else getattr(c, "text", None) + ), + } + ) elif content_type == "refusal": - parts.append({ - "type": "refusal", # custom type - "content": ( - "readacted" - if not self.include_sensitive_data - else getattr(c, "refusal", None) - ) - }) + parts.append( + { + "type": "refusal", # custom type + "content": ( + "readacted" + if not self.include_sensitive_data + else getattr(c, "refusal", None) + ), + } + ) else: - parts.append({ - "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else str(content) - ) - }) + parts.append( + { + "type": "text", + "content": ( + "readacted" + if not self.include_sensitive_data + else str(content) + ), + } + ) return parts if part_type == "file_search_call": # ResponseFileSearchToolCall - return [{ - "type": "tool_call", - "name": "file_search", - "id": getattr(item, "id", None), - "arguments": ({ - "queries": ["readacted"] - if not self.include_sensitive_data - else getattr(item, "queries", None) - }) - }] + return [ + { + "type": "tool_call", + "name": "file_search", + "id": getattr(item, "id", None), + "arguments": ( + { + "queries": ["readacted"] + if not self.include_sensitive_data + else getattr(item, "queries", None) + } + ), + } + ] elif part_type == "function_call": # ResponseFunctionToolCall - return [{ - "type": "tool_call", - "name": getattr(item, "name", None), - "id": getattr(item, "id", None), - "arguments": ( - "readacted" - if not self.include_sensitive_data - else safe_json_loads(getattr(item, "arguments", None)) - ) - }] + return [ + { + "type": "tool_call", + "name": getattr(item, "name", None), + "id": getattr(item, "id", None), + "arguments": ( + "readacted" + if not self.include_sensitive_data + else safe_json_loads(getattr(item, "arguments", None)) + ), + } + ] elif part_type == "web_search_call": # ResponseFunctionWebSearch action = getattr(item, "action", None) action_type = getattr(action, "type", None) action_obj = ( - {"type": action_type} if not self.include_sensitive_data - else action.to_dict() if hasattr(action, "to_dict") else str(action) + {"type": action_type} + if not self.include_sensitive_data + else action.to_dict() + if hasattr(action, "to_dict") + else str(action) ) - return [{ - "type": "tool_call", - "name": "web_search", - "id": getattr(item, "id", None), - "arguments": { - "action": action_obj, + return [ + { + "type": "tool_call", + "name": "web_search", + "id": getattr(item, "id", None), + "arguments": { + "action": action_obj, + }, } - }] + ] elif part_type == "computer_call": # ResponseComputerToolCall action = getattr(item, "action", None) action_type = getattr(action, "type", None) action_obj = ( - {"type": action_type} if not self.include_sensitive_data - else action.to_dict() if hasattr(action, "to_dict") else str(action) + {"type": action_type} + if not self.include_sensitive_data + else action.to_dict() + if hasattr(action, "to_dict") + else str(action) ) - return [{ - "type": "tool_call", - "name": "computer", - "id": getattr(item, "id", None), - "arguments": { - "action": action_obj, + return [ + { + "type": "tool_call", + "name": "computer", + "id": getattr(item, "id", None), + "arguments": { + "action": action_obj, + }, } - }] + ] elif part_type == "reasoning": # ResponseReasoningItem content = getattr(item, "content", None) - content_str = str.join("\n", [ - getattr(c, "text", "") if hasattr(c, "text") else str(c) - for c in content - ]) if isinstance(content, Sequence) else str(content) - return [{ - "type": "reasoning", - "content": ( - "readacted" - if not self.include_sensitive_data - else content_str - ) - }] - elif part_type == "compaction": # ResponseCompactionItem - return [{ - "type": "compaction", # custom type - "content": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "encrypted_content", None) + content_str = ( + str.join( + "\n", + [ + getattr(c, "text", "") + if hasattr(c, "text") + else str(c) + for c in content + ], ) - }] - elif part_type == "image_generation_call": # ImageGenerationCall - return [{ - "type": "tool_call_response", - "name": "image_generation", - "id": getattr(item, "id", None), - "response": { - "result": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "result", None) - ) - } - }] - elif part_type == "code_interpreter_call": # ResponseCodeInterpreterToolCall - return [{ - "type": "tool_call", - "name": "code_interpreter", - "id": getattr(item, "id", None), - "arguments": { - "container_id": getattr(item, "container_id", None), - "code": ( + if isinstance(content, Sequence) + else str(content) + ) + return [ + { + "type": "reasoning", + "content": ( "readacted" if not self.include_sensitive_data - else getattr(item, "code", None) - ) - } - }, { - "type": "tool_call_response", - "name": "code_interpreter", - "id": getattr(item, "id", None), - "response": ( - "readacted" - if not self.include_sensitive_data - else [ - output.to_dict() for output in getattr(item, "outputs", []) - ] - ) - }] - elif part_type == "local_shell_call": # LocalShellCall - action = getattr(item, "action", None) - return [{ - "type": "tool_call", - "name": "local_shell", - "id": getattr(item, "id", None), - "arguments": { - "type": getattr(action, "type", None), - "timeout_ms": getattr(action, "timeout_ms", None), - "command": ( - ["readacted"] - if not self.include_sensitive_data - else getattr(action, "command", None) + else content_str ), - "env": ( + } + ] + elif part_type == "compaction": # ResponseCompactionItem + return [ + { + "type": "compaction", # custom type + "content": ( "readacted" if not self.include_sensitive_data - else getattr(action, "env", None) + else getattr(item, "encrypted_content", None) ), - "user": ( + } + ] + elif part_type == "image_generation_call": # ImageGenerationCall + return [ + { + "type": "tool_call_response", + "name": "image_generation", + "id": getattr(item, "id", None), + "response": { + "result": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "result", None) + ) + }, + } + ] + elif ( + part_type == "code_interpreter_call" + ): # ResponseCodeInterpreterToolCall + return [ + { + "type": "tool_call", + "name": "code_interpreter", + "id": getattr(item, "id", None), + "arguments": { + "container_id": getattr(item, "container_id", None), + "code": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "code", None) + ), + }, + }, + { + "type": "tool_call_response", + "name": "code_interpreter", + "id": getattr(item, "id", None), + "response": ( "readacted" if not self.include_sensitive_data - else getattr(action, "user", None) + else [ + output.to_dict() + for output in getattr(item, "outputs", []) + ] ), - "working_directory": ( - "readacted" - if not self.include_sensitive_data - else getattr(action, "working_directory", None) - ) + }, + ] + elif part_type == "local_shell_call": # LocalShellCall + action = getattr(item, "action", None) + return [ + { + "type": "tool_call", + "name": "local_shell", + "id": getattr(item, "id", None), + "arguments": { + "type": getattr(action, "type", None), + "timeout_ms": getattr(action, "timeout_ms", None), + "command": ( + ["readacted"] + if not self.include_sensitive_data + else getattr(action, "command", None) + ), + "env": ( + "readacted" + if not self.include_sensitive_data + else getattr(action, "env", None) + ), + "user": ( + "readacted" + if not self.include_sensitive_data + else getattr(action, "user", None) + ), + "working_directory": ( + "readacted" + if not self.include_sensitive_data + else getattr(action, "working_directory", None) + ), + }, } - }] + ] elif part_type == "shell_call": # ResponseFunctionShellToolCall action = getattr(item, "action", None) - return [{ - "type": "tool_call", - "name": "shell", - "id": getattr(item, "id", None), - "arguments": { - "created_by": getattr(action, "created_by", None), - "call_id": getattr(item, "call_id", None), - "environment": getattr(action, "environment", None), - "commands": ( - ["readacted"] - if not self.include_sensitive_data - else getattr(action, "commands", None) - ), - "max_output_length": getattr(action, "max_output_length", None), - "timeout_ms": getattr(action, "timeout_ms", None), + return [ + { + "type": "tool_call", + "name": "shell", + "id": getattr(item, "id", None), + "arguments": { + "created_by": getattr(action, "created_by", None), + "call_id": getattr(item, "call_id", None), + "environment": getattr(action, "environment", None), + "commands": ( + ["readacted"] + if not self.include_sensitive_data + else getattr(action, "commands", None) + ), + "max_output_length": getattr( + action, "max_output_length", None + ), + "timeout_ms": getattr(action, "timeout_ms", None), + }, } - }] - elif part_type == "shell_call_output": # ResponseFunctionShellToolCallOutput - return [{ - "type": "tool_call_response", - "name": "shell", - "id": getattr(item, "id", None), - "response": { - "created_by": getattr(item, "created_by", None), - "call_id": getattr(item, "call_id", None), - "result": ( - ["readacted"] - if not self.include_sensitive_data - else [output.to_dict() for output in getattr(item, "output", [])] - ) + ] + elif ( + part_type == "shell_call_output" + ): # ResponseFunctionShellToolCallOutput + return [ + { + "type": "tool_call_response", + "name": "shell", + "id": getattr(item, "id", None), + "response": { + "created_by": getattr(item, "created_by", None), + "call_id": getattr(item, "call_id", None), + "result": ( + ["readacted"] + if not self.include_sensitive_data + else [ + output.to_dict() + for output in getattr(item, "output", []) + ] + ), + }, } - }] + ] elif part_type == "apply_patch_call": # ResponseApplyPatchToolCall operation = getattr(item, "operation", None) operation_type = getattr(operation, "type", None) - return [{ - "type": "tool_call", - "name": "apply_patch", - "id": getattr(item, "id", None), - "arguments": { - "created_by": getattr(item, "created_by", None), - "call_id": getattr(item, "call_id", None), - "operation": ( - { "type": operation_type } - if not self.include_sensitive_data - else operation.to_dict() if hasattr(operation, "to_dict") else safe_json_dumps(operation) - ), - } - }] - elif part_type == "apply_patch_call_output": # ResponseApplyPatchToolCallOutput - return [{ - "type": "tool_call_response", - "name": "apply_patch", - "id": getattr(item, "id", None), - "response": { - "created_by": getattr(item, "created_by", None), - "call_id": getattr(item, "call_id", None), - "status": getattr(item, "status", None), - "output": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "output", None) - ), - } - }] - elif part_type == "mcp_call": # McpCall - parts = [{ - "type": "tool_call", - "name": "mcp_call", - "id": getattr(item, "id", None), - "arguments": { - "server": getattr(item, "server_label", None), - "tool_name": getattr(item, "name", None), - "tool_args": ( - "readacted" - if not self.include_sensitive_data - else safe_json_loads(getattr(item, "arguments", None)) - ), + return [ + { + "type": "tool_call", + "name": "apply_patch", + "id": getattr(item, "id", None), + "arguments": { + "created_by": getattr(item, "created_by", None), + "call_id": getattr(item, "call_id", None), + "operation": ( + {"type": operation_type} + if not self.include_sensitive_data + else operation.to_dict() + if hasattr(operation, "to_dict") + else safe_json_dumps(operation) + ), + }, } - }] - if getattr(item, "output", None) or getattr(item, "error", None): - parts.append({ + ] + elif ( + part_type == "apply_patch_call_output" + ): # ResponseApplyPatchToolCallOutput + return [ + { "type": "tool_call_response", - "name": "mcp_call", + "name": "apply_patch", "id": getattr(item, "id", None), "response": { + "created_by": getattr(item, "created_by", None), + "call_id": getattr(item, "call_id", None), + "status": getattr(item, "status", None), "output": ( "readacted" if not self.include_sensitive_data else getattr(item, "output", None) ), - "error": ( + }, + } + ] + elif part_type == "mcp_call": # McpCall + parts = [ + { + "type": "tool_call", + "name": "mcp_call", + "id": getattr(item, "id", None), + "arguments": { + "server": getattr(item, "server_label", None), + "tool_name": getattr(item, "name", None), + "tool_args": ( "readacted" if not self.include_sensitive_data - else getattr(item, "error", None) + else safe_json_loads( + getattr(item, "arguments", None) + ) ), - "status": getattr(item, "status", None), + }, + } + ] + if getattr(item, "output", None) or getattr(item, "error", None): + parts.append( + { + "type": "tool_call_response", + "name": "mcp_call", + "id": getattr(item, "id", None), + "response": { + "output": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "output", None) + ), + "error": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "error", None) + ), + "status": getattr(item, "status", None), + }, } - }) + ) return parts elif part_type == "mcp_list_tools": # McpListTools - parts = [{ - "type": "tool_call", - "name": "mcp_list_tools", - "id": getattr(item, "id", None), - "arguments": { - "server": getattr(item, "server_label", None), + parts = [ + { + "type": "tool_call", + "name": "mcp_list_tools", + "id": getattr(item, "id", None), + "arguments": { + "server": getattr(item, "server_label", None), + }, } - }] + ] if getattr(item, "tools", None) or getattr(item, "error", None): - parts.append({ - "type": "tool_call_response", - "name": "mcp_list_tools", + parts.append( + { + "type": "tool_call_response", + "name": "mcp_list_tools", + "id": getattr(item, "id", None), + "response": { + "tools": ( + "readacted" + if not self.include_sensitive_data + else [ + tool.to_dict() + for tool in getattr(item, "tools", []) + ] + ), + "error": ( + "readacted" + if not self.include_sensitive_data + else getattr(item, "error", None) + ), + }, + } + ) + return parts + elif part_type == "mcp_approval_request": # McpApprovalRequest + return [ + { + "type": "tool_call", + "name": "mcp_approval_request", "id": getattr(item, "id", None), - "response": { - "tools": ( - "readacted" - if not self.include_sensitive_data - else [tool.to_dict() for tool in getattr(item, "tools", [])] - ), - "error": ( + "arguments": { + "server": getattr(item, "server_label", None), + "tool_name": getattr(item, "name", None), + "tool_args": ( "readacted" if not self.include_sensitive_data - else getattr(item, "error", None) + else safe_json_loads( + getattr(item, "arguments", None) + ) ), - } - }) - return parts - elif part_type == "mcp_approval_request": # McpApprovalRequest - return [{ - "type": "tool_call", - "name": "mcp_approval_request", - "id": getattr(item, "id", None), - "arguments": { - "server": getattr(item, "server_label", None), - "tool_name": getattr(item, "name", None), - "tool_args": ( + }, + } + ] + elif part_type == "custom_tool_call": # ResponseCustomToolCall + return [ + { + "type": "tool_call", + "name": getattr(item, "name", None), + "id": getattr(item, "id", None), + "arguments": ( "readacted" if not self.include_sensitive_data - else safe_json_loads(getattr(item, "arguments", None)) + else getattr(item, "input", None) ), } - }] - elif part_type == "custom_tool_call": # ResponseCustomToolCall - return [{ - "type": "tool_call", - "name": getattr(item, "name", None), - "id": getattr(item, "id", None), - "arguments": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "input", None) - ) - }] + ] # Fallback: content string attribute txt = getattr(item, "content", None) if isinstance(txt, str) and txt: - return [{ - "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else txt - ), - }] + return [ + { + "type": "text", + "content": ( + "readacted" if not self.include_sensitive_data else txt + ), + } + ] else: # Fallback: stringified - return [{ - "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else safe_json_dumps(item) - ), - }] + return [ + { + "type": "text", + "content": ( + "readacted" + if not self.include_sensitive_data + else safe_json_dumps(item) + ), + } + ] def _normalize_output_messages_to_role_parts( self, span_data: Any @@ -1417,7 +1506,11 @@ def _build_content_payload(self, span: Span[Any]) -> ContentPayload: self._normalize_messages_to_role_parts(span_input) ) - if capture_system and response_obj and hasattr(response_obj, "instructions"): + if ( + capture_system + and response_obj + and hasattr(response_obj, "instructions") + ): payload.system_instructions = self._normalize_to_text_parts( response_obj.instructions ) @@ -2390,25 +2483,6 @@ def _get_attributes_from_response_span_data( if output_tokens is not None: yield GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens - # Tool definitions from response - if self._capture_tool_definitions and hasattr( - span_data.response, "tools" - ): - def _serialize_tool_value(value: Any) -> Optional[str]: - if value is None: - return None - return { - "name": getattr(value, "name", None), - "type": getattr(value, "type", None), - "description": getattr(value, "description", None), - "parameters": getattr(value, "parameters", None), - } - - yield ( - GEN_AI_TOOL_DEFINITIONS, - safe_json_dumps(list(map(_serialize_tool_value, span_data.response.tools))), - ) - # Input/output messages if ( self.include_sensitive_data diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py index 57bc92335e..f97e4e274e 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py @@ -17,6 +17,7 @@ sys.modules.pop("agents.tracing", None) import agents.tracing as agents_tracing # noqa: E402 +import openai # noqa: E402 from agents.tracing import ( # noqa: E402 agent_span, function_span, @@ -26,53 +27,38 @@ trace, ) from openai.types.responses import ( # noqa: E402 - ResponseOutputMessage, - ResponseOutputText, - ResponseOutputRefusal, + ResponseCodeInterpreterToolCall, + ResponseComputerToolCall, + ResponseCustomToolCall, ResponseFileSearchToolCall, ResponseFunctionToolCall, ResponseFunctionWebSearch, - ResponseComputerToolCall, + ResponseOutputMessage, + ResponseOutputRefusal, + ResponseOutputText, ResponseReasoningItem, - ResponseCompactionItem, - ResponseCodeInterpreterToolCall, - ResponseFunctionShellToolCall, - ResponseFunctionShellToolCallOutput, - ResponseApplyPatchToolCall, - ResponseApplyPatchToolCallOutput, - ResponseCustomToolCall, +) +from openai.types.responses.response_code_interpreter_tool_call import ( # noqa: E402 + OutputLogs, +) +from openai.types.responses.response_computer_tool_call import ( # noqa: E402 + ActionClick, +) +from openai.types.responses.response_function_web_search import ( # noqa: E402 + ActionSearch as ActionWebSearch, ) from openai.types.responses.response_output_item import ( # noqa: E402 ImageGenerationCall, LocalShellCall, LocalShellCallAction, + McpApprovalRequest, McpCall, McpListTools, - McpApprovalRequest, McpListToolsTool, ) -from openai.types.responses.response_apply_patch_tool_call import ( # noqa: E402 - OperationCreateFile, -) from openai.types.responses.response_reasoning_item import ( # noqa: E402 Content, ) -from openai.types.responses.response_code_interpreter_tool_call import ( # noqa: E402 - OutputLogs, -) -from openai.types.responses.response_function_shell_tool_call_output import ( # noqa: E402 - Output, - OutputOutcomeExit, -) -from openai.types.responses.response_function_web_search import ( # noqa: E402 - ActionSearch as ActionWebSearch, -) -from openai.types.responses.response_computer_tool_call import ( # noqa: E402 - ActionClick, -) -from openai.types.responses.response_function_shell_tool_call import ( # noqa: E402 - Action as ActionShellCall, -) from opentelemetry.instrumentation.openai_agents import ( # noqa: E402 OpenAIAgentsInstrumentor, @@ -528,6 +514,101 @@ def test_capture_mode_can_be_disabled(): def test_response_span_records_response_attributes(): instrumentor, exporter = _instrument_with_provider() + # dummy classes for some response types since concrete type is not available in older `openai` versions + class _ResponseCompactionItem: + def __init__(self, id: str, type: str, encrypted_content: str) -> None: + self.id = id + self.type = type + self.encrypted_content = encrypted_content + + class _ActionShellCall(openai.BaseModel): + def __init__( + self, commands: list[str], max_output_length: int, timeout_ms: int + ) -> None: + super().__init__( + commands=commands, + max_output_length=max_output_length, + timeout_ms=timeout_ms, + ) + + class _ResponseFunctionShellToolCall(openai.BaseModel): + def __init__( + self, + id: str, + type: str, + status: str, + call_id: str, + action: _ActionShellCall, + ) -> None: + super().__init__( + id=id, type=type, status=status, call_id=call_id, action=action + ) + + class _OutputOutcomeExit(openai.BaseModel): + def __init__(self, type: str, exit_code: int) -> None: + super().__init__(type=type, exit_code=exit_code) + + class _ShellToolCallOutput(openai.BaseModel): + def __init__( + self, stdout: str, stderr: str, outcome: _OutputOutcomeExit + ) -> None: + super().__init__(stdout=stdout, stderr=stderr, outcome=outcome) + + class _ResponseFunctionShellToolCallOutput(openai.BaseModel): + def __init__( + self, + id: str, + type: str, + status: str, + call_id: str, + output: list[_ShellToolCallOutput], + ) -> None: + super().__init__( + id=id, type=type, status=status, call_id=call_id, output=output + ) + + class _OperationCreateFile(openai.BaseModel): + def __init__(self, type: str, diff: str, path: str) -> None: + super().__init__(type=type, diff=diff, path=path) + + class _ResponseApplyPatchToolCall(openai.BaseModel): + def __init__( + self, + id: str, + type: str, + status: str, + created_by: str, + call_id: str, + operation: _OperationCreateFile, + ) -> None: + super().__init__( + id=id, + type=type, + status=status, + created_by=created_by, + call_id=call_id, + operation=operation, + ) + + class _ResponseApplyPatchToolCallOutput(openai.BaseModel): + def __init__( + self, + id: str, + type: str, + created_by: str, + call_id: str, + status: str, + output: str, + ) -> None: + super().__init__( + id=id, + type=type, + created_by=created_by, + call_id=call_id, + status=status, + output=output, + ) + class _Usage: def __init__(self, input_tokens: int, output_tokens: int) -> None: self.input_tokens = input_tokens @@ -574,7 +655,7 @@ def __init__(self) -> None: ], ), # compaction - ResponseCompactionItem( + _ResponseCompactionItem( id="compact-1", type="compaction", encrypted_content="encrypted_data", @@ -644,46 +725,48 @@ def __init__(self) -> None: ), ), # shell_call - ResponseFunctionShellToolCall( + _ResponseFunctionShellToolCall( id="sh-123", type="shell_call", status="completed", call_id="call-123", - action=ActionShellCall( + action=_ActionShellCall( commands=["echo hello"], max_output_length=1000, timeout_ms=5000, ), ), # shell_call_output - ResponseFunctionShellToolCallOutput( + _ResponseFunctionShellToolCallOutput( id="sho-123", type="shell_call_output", status="completed", call_id="call-123", output=[ - Output( + _ShellToolCallOutput( stdout="shell output", stderr="", - outcome=OutputOutcomeExit(type="exit", exit_code=0), + outcome=_OutputOutcomeExit( + type="exit", exit_code=0 + ), ) ], ), # apply_patch_call - ResponseApplyPatchToolCall( + _ResponseApplyPatchToolCall( id="ap-123", type="apply_patch_call", status="completed", created_by="agent", call_id="call-123", - operation=OperationCreateFile( + operation=_OperationCreateFile( type="create_file", diff="content", path="/tmp/test.txt", ), ), # apply_patch_call_output - ResponseApplyPatchToolCallOutput( + _ResponseApplyPatchToolCallOutput( id="apo-123", type="apply_patch_call_output", created_by="agent", @@ -756,7 +839,7 @@ def __init__(self) -> None: id="ct-123", type="custom_tool_call", call_id="call-ct-123", - input='input', + input="input", ), # fallback with content string { @@ -794,7 +877,9 @@ def __init__(self) -> None: assert response.attributes[GenAI.GEN_AI_USAGE_OUTPUT_TOKENS] == 9 # Check output messages are properly normalized - output_messages = json.loads(response.attributes[GEN_AI_OUTPUT_MESSAGES]) + output_messages = json.loads( + response.attributes[GEN_AI_OUTPUT_MESSAGES] + ) assert len(output_messages) == 1 assert output_messages[0]["role"] == "assistant" parts = output_messages[0]["parts"] @@ -977,7 +1062,7 @@ def __init__(self) -> None: "tools": [ {"name": "tool1", "input_schema": {}}, {"name": "tool2", "input_schema": {}}, - ] + ], }, } From 60e1a52c8cbbae8d7c25274af9e53a1b5f4ec31a Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Thu, 19 Feb 2026 10:35:32 +1100 Subject: [PATCH 3/7] fix file search queries argument --- .../instrumentation/openai_agents/span_processor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py index dcc836d023..3aa2e765f2 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py @@ -946,9 +946,11 @@ def _normalize_response_output_part( "id": getattr(item, "id", None), "arguments": ( { - "queries": ["readacted"] - if not self.include_sensitive_data - else getattr(item, "queries", None) + "queries": ( + ["readacted"] + if not self.include_sensitive_data + else getattr(item, "queries", []) + ) } ), } From b385cea3b815e621407b23dd78f320d31407da15 Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Thu, 19 Feb 2026 11:28:24 +1100 Subject: [PATCH 4/7] test response output redaction --- .../openai_agents/span_processor.py | 13 +- .../tests/test_tracer.py | 916 +++++++++++------- 2 files changed, 589 insertions(+), 340 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py index 3aa2e765f2..744c94ffa3 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py @@ -1080,14 +1080,15 @@ def _normalize_response_output_part( "type": "tool_call_response", "name": "code_interpreter", "id": getattr(item, "id", None), - "response": ( - "readacted" - if not self.include_sensitive_data - else [ - output.to_dict() + "response": { + "status": getattr(item, "status", None), + "outputs": [ + { "type": output.type } + if not self.include_sensitive_data + else output.to_dict() for output in getattr(item, "outputs", []) ] - ), + }, }, ] elif part_type == "local_shell_call": # LocalShellCall diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py index f97e4e274e..c81df828f4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py @@ -97,6 +97,344 @@ GenAI, "GEN_AI_OUTPUT_MESSAGES", "gen_ai.output.messages" ) +# dummy classes for some response types since concrete type is not available in older `openai` versions +class _ResponseCompactionItem: + def __init__(self, id: str, type: str, encrypted_content: str) -> None: + self.id = id + self.type = type + self.encrypted_content = encrypted_content + +class _ActionShellCall(openai.BaseModel): + def __init__( + self, commands: list[str], max_output_length: int, timeout_ms: int + ) -> None: + super().__init__( + commands=commands, + max_output_length=max_output_length, + timeout_ms=timeout_ms, + ) + +class _ResponseFunctionShellToolCall(openai.BaseModel): + def __init__( + self, + id: str, + type: str, + status: str, + call_id: str, + action: _ActionShellCall, + ) -> None: + super().__init__( + id=id, type=type, status=status, call_id=call_id, action=action + ) + +class _OutputOutcomeExit(openai.BaseModel): + def __init__(self, type: str, exit_code: int) -> None: + super().__init__(type=type, exit_code=exit_code) + +class _ShellToolCallOutput(openai.BaseModel): + def __init__( + self, stdout: str, stderr: str, outcome: _OutputOutcomeExit + ) -> None: + super().__init__(stdout=stdout, stderr=stderr, outcome=outcome) + +class _ResponseFunctionShellToolCallOutput(openai.BaseModel): + def __init__( + self, + id: str, + type: str, + status: str, + call_id: str, + output: list[_ShellToolCallOutput], + ) -> None: + super().__init__( + id=id, type=type, status=status, call_id=call_id, output=output + ) + +class _OperationCreateFile(openai.BaseModel): + def __init__(self, type: str, diff: str, path: str) -> None: + super().__init__(type=type, diff=diff, path=path) + +class _ResponseApplyPatchToolCall(openai.BaseModel): + def __init__( + self, + id: str, + type: str, + status: str, + created_by: str, + call_id: str, + operation: _OperationCreateFile, + ) -> None: + super().__init__( + id=id, + type=type, + status=status, + created_by=created_by, + call_id=call_id, + operation=operation, + ) + +class _ResponseApplyPatchToolCallOutput(openai.BaseModel): + def __init__( + self, + id: str, + type: str, + created_by: str, + call_id: str, + status: str, + output: str, + ) -> None: + super().__init__( + id=id, + type=type, + created_by=created_by, + call_id=call_id, + status=status, + output=output, + ) + +class _Usage: + def __init__(self, input_tokens: int, output_tokens: int) -> None: + self.input_tokens = input_tokens + self.output_tokens = output_tokens + +class _Response: + def __init__(self) -> None: + self.id = "resp-123" + self.model = "gpt-4o-mini" + self.usage = _Usage(42, 9) + self.output = [ + # message type with output_text + ResponseOutputMessage( + id="msg-1", + role="assistant", + type="message", + status="completed", + content=[ + ResponseOutputText( + type="output_text", text="Hello!", annotations=[] + ) + ], + ), + # message type with refusal + ResponseOutputMessage( + id="msg-2", + role="assistant", + type="message", + status="completed", + content=[ + ResponseOutputRefusal( + type="refusal", refusal="I cannot do that" + ) + ], + ), + # reasoning + ResponseReasoningItem( + id="reason-1", + type="reasoning", + summary=[], + content=[ + Content(type="reasoning_text", text="Step 1: Think"), + Content(type="reasoning_text", text="Step 2: Act"), + ], + ), + # compaction + _ResponseCompactionItem( + id="compact-1", + type="compaction", + encrypted_content="encrypted_data", + ), + # file_search_call + ResponseFileSearchToolCall( + type="file_search_call", + id="fs-123", + status="completed", + queries=["search query"], + ), + # function_call + ResponseFunctionToolCall( + name="get_weather", + id="fc-123", + type="function_call", + call_id="call-fc-123", + arguments='{"city": "Paris"}', + ), + # web_search_call + ResponseFunctionWebSearch( + id="ws-123", + type="web_search_call", + status="completed", + action=ActionWebSearch(type="search", query="test"), + ), + # computer_call + ResponseComputerToolCall( + id="cc-123", + type="computer_call", + call_id="call-cc-123", + status="completed", + pending_safety_checks=[], + action=ActionClick( + type="click", x=100, y=200, button="left" + ), + ), + # image_generation_call + ImageGenerationCall( + id="ig-123", + status="completed", + type="image_generation_call", + result="image_url", + ), + # code_interpreter_call + ResponseCodeInterpreterToolCall( + id="ci-123", + type="code_interpreter_call", + status="completed", + container_id="cont-123", + code="print('hello')", + outputs=[OutputLogs(type="logs", logs="hello")], + ), + # local_shell_call + LocalShellCall( + id="ls-123", + call_id="call-ls-123", + type="local_shell_call", + status="completed", + action=LocalShellCallAction( + type="exec", + timeout_ms=5000, + command=["ls", "-la"], + env={"PATH": "/usr/bin"}, + user="root", + working_directory="/tmp", + ), + ), + # shell_call + _ResponseFunctionShellToolCall( + id="sh-123", + type="shell_call", + status="completed", + call_id="call-123", + action=_ActionShellCall( + commands=["echo hello"], + max_output_length=1000, + timeout_ms=5000, + ), + ), + # shell_call_output + _ResponseFunctionShellToolCallOutput( + id="sho-123", + type="shell_call_output", + status="completed", + call_id="call-123", + output=[ + _ShellToolCallOutput( + stdout="shell output", + stderr="", + outcome=_OutputOutcomeExit( + type="exit", exit_code=0 + ), + ) + ], + ), + # apply_patch_call + _ResponseApplyPatchToolCall( + id="ap-123", + type="apply_patch_call", + status="completed", + created_by="agent", + call_id="call-123", + operation=_OperationCreateFile( + type="create_file", + diff="content", + path="/tmp/test.txt", + ), + ), + # apply_patch_call_output + _ResponseApplyPatchToolCallOutput( + id="apo-123", + type="apply_patch_call_output", + created_by="agent", + call_id="call-123", + status="completed", + output="Applied successfully", + ), + # mcp_call with output + McpCall( + id="mcp-123", + server_label="server1", + name="tool_name", + type="mcp_call", + arguments='{"key": "value"}', + output="result", + error=None, + status="completed", + ), + # mcp_call with error + McpCall( + id="mcp-124", + server_label="server1", + name="tool_name", + type="mcp_call", + arguments='{"key": "value"}', + output=None, + error="Some error", + status="failed", + ), + # mcp_call without output (no response part) + McpCall( + id="mcp-125", + server_label="server2", + name="another_tool", + type="mcp_call", + arguments='{"key": "value2"}', + output=None, + error=None, + ), + # mcp_list_tools with tools + McpListTools( + id="mcpl-123", + server_label="server1", + type="mcp_list_tools", + tools=[ + McpListToolsTool(name="tool1", input_schema={}), + McpListToolsTool(name="tool2", input_schema={}), + ], + error=None, + ), + # mcp_list_tools without tools (no response part) + McpListTools( + id="mcpl-124", + server_label="server2", + type="mcp_list_tools", + tools=[], + error=None, + ), + # mcp_approval_request + McpApprovalRequest( + id="mcpa-123", + server_label="server1", + name="dangerous_tool", + type="mcp_approval_request", + arguments='{"action": "delete"}', + ), + # custom_tool_call + ResponseCustomToolCall( + name="custom_tool", + id="ct-123", + type="custom_tool_call", + call_id="call-ct-123", + input="input", + ), + # fallback with content string + { + "type": "unknown_type", + "content": "fallback content", + }, + # fallback to stringified (no content attribute) + { + "type": "another_unknown", + "data": "some data", + }, + ] def _instrument_with_provider(**instrument_kwargs): set_trace_processors([]) @@ -511,347 +849,248 @@ def test_capture_mode_can_be_disabled(): exporter.clear() -def test_response_span_records_response_attributes(): - instrumentor, exporter = _instrument_with_provider() +def test_response_span_records_redacted_response_attributes(): + instrumentor, exporter = _instrument_with_provider( + capture_message_content=False, + ) - # dummy classes for some response types since concrete type is not available in older `openai` versions - class _ResponseCompactionItem: - def __init__(self, id: str, type: str, encrypted_content: str) -> None: - self.id = id - self.type = type - self.encrypted_content = encrypted_content - - class _ActionShellCall(openai.BaseModel): - def __init__( - self, commands: list[str], max_output_length: int, timeout_ms: int - ) -> None: - super().__init__( - commands=commands, - max_output_length=max_output_length, - timeout_ms=timeout_ms, - ) + try: + with trace("workflow"): + with response_span(response=_Response()): + pass - class _ResponseFunctionShellToolCall(openai.BaseModel): - def __init__( - self, - id: str, - type: str, - status: str, - call_id: str, - action: _ActionShellCall, - ) -> None: - super().__init__( - id=id, type=type, status=status, call_id=call_id, action=action - ) + spans = exporter.get_finished_spans() + response = next( + span + for span in spans + if span.attributes[GenAI.GEN_AI_OPERATION_NAME] + == GenAI.GenAiOperationNameValues.CHAT.value + ) - class _OutputOutcomeExit(openai.BaseModel): - def __init__(self, type: str, exit_code: int) -> None: - super().__init__(type=type, exit_code=exit_code) - - class _ShellToolCallOutput(openai.BaseModel): - def __init__( - self, stdout: str, stderr: str, outcome: _OutputOutcomeExit - ) -> None: - super().__init__(stdout=stdout, stderr=stderr, outcome=outcome) - - class _ResponseFunctionShellToolCallOutput(openai.BaseModel): - def __init__( - self, - id: str, - type: str, - status: str, - call_id: str, - output: list[_ShellToolCallOutput], - ) -> None: - super().__init__( - id=id, type=type, status=status, call_id=call_id, output=output - ) + assert response.kind is SpanKind.CLIENT + assert response.name == "chat gpt-4o-mini" + assert response.attributes[GEN_AI_PROVIDER_NAME] == "openai" + assert response.attributes[GenAI.GEN_AI_RESPONSE_ID] == "resp-123" + assert ( + response.attributes[GenAI.GEN_AI_RESPONSE_MODEL] == "gpt-4o-mini" + ) + assert response.attributes[GenAI.GEN_AI_USAGE_INPUT_TOKENS] == 42 + assert response.attributes[GenAI.GEN_AI_USAGE_OUTPUT_TOKENS] == 9 - class _OperationCreateFile(openai.BaseModel): - def __init__(self, type: str, diff: str, path: str) -> None: - super().__init__(type=type, diff=diff, path=path) - - class _ResponseApplyPatchToolCall(openai.BaseModel): - def __init__( - self, - id: str, - type: str, - status: str, - created_by: str, - call_id: str, - operation: _OperationCreateFile, - ) -> None: - super().__init__( - id=id, - type=type, - status=status, - created_by=created_by, - call_id=call_id, - operation=operation, - ) + # Check output messages are redacted + output_messages = json.loads( + response.attributes[GEN_AI_OUTPUT_MESSAGES] + ) + assert len(output_messages) == 1 + assert output_messages[0]["role"] == "assistant" + parts = output_messages[0]["parts"] + assert parts[0] == { + "type": "text", + "content": "readacted", + "annotations": [], + } - class _ResponseApplyPatchToolCallOutput(openai.BaseModel): - def __init__( - self, - id: str, - type: str, - created_by: str, - call_id: str, - status: str, - output: str, - ) -> None: - super().__init__( - id=id, - type=type, - created_by=created_by, - call_id=call_id, - status=status, - output=output, - ) + assert parts[1] == { + "type": "refusal", + "content": "readacted", + } - class _Usage: - def __init__(self, input_tokens: int, output_tokens: int) -> None: - self.input_tokens = input_tokens - self.output_tokens = output_tokens - - class _Response: - def __init__(self) -> None: - self.id = "resp-123" - self.model = "gpt-4o-mini" - self.usage = _Usage(42, 9) - self.output = [ - # message type with output_text - ResponseOutputMessage( - id="msg-1", - role="assistant", - type="message", - status="completed", - content=[ - ResponseOutputText( - type="output_text", text="Hello!", annotations=[] - ) - ], - ), - # message type with refusal - ResponseOutputMessage( - id="msg-2", - role="assistant", - type="message", - status="completed", - content=[ - ResponseOutputRefusal( - type="refusal", refusal="I cannot do that" - ) - ], - ), - # reasoning - ResponseReasoningItem( - id="reason-1", - type="reasoning", - summary=[], - content=[ - Content(type="reasoning_text", text="Step 1: Think"), - Content(type="reasoning_text", text="Step 2: Act"), - ], - ), - # compaction - _ResponseCompactionItem( - id="compact-1", - type="compaction", - encrypted_content="encrypted_data", - ), - # file_search_call - ResponseFileSearchToolCall( - type="file_search_call", - id="fs-123", - status="completed", - queries=["search query"], - ), - # function_call - ResponseFunctionToolCall( - name="get_weather", - id="fc-123", - type="function_call", - call_id="call-fc-123", - arguments='{"city": "Paris"}', - ), - # web_search_call - ResponseFunctionWebSearch( - id="ws-123", - type="web_search_call", - status="completed", - action=ActionWebSearch(type="search", query="test"), - ), - # computer_call - ResponseComputerToolCall( - id="cc-123", - type="computer_call", - call_id="call-cc-123", - status="completed", - pending_safety_checks=[], - action=ActionClick( - type="click", x=100, y=200, button="left" - ), - ), - # image_generation_call - ImageGenerationCall( - id="ig-123", - status="completed", - type="image_generation_call", - result="image_url", - ), - # code_interpreter_call - ResponseCodeInterpreterToolCall( - id="ci-123", - type="code_interpreter_call", - status="completed", - container_id="cont-123", - code="print('hello')", - outputs=[OutputLogs(type="logs", logs="hello")], - ), - # local_shell_call - LocalShellCall( - id="ls-123", - call_id="call-ls-123", - type="local_shell_call", - status="completed", - action=LocalShellCallAction( - type="exec", - timeout_ms=5000, - command=["ls", "-la"], - env={"PATH": "/usr/bin"}, - user="root", - working_directory="/tmp", - ), - ), - # shell_call - _ResponseFunctionShellToolCall( - id="sh-123", - type="shell_call", - status="completed", - call_id="call-123", - action=_ActionShellCall( - commands=["echo hello"], - max_output_length=1000, - timeout_ms=5000, - ), - ), - # shell_call_output - _ResponseFunctionShellToolCallOutput( - id="sho-123", - type="shell_call_output", - status="completed", - call_id="call-123", - output=[ - _ShellToolCallOutput( - stdout="shell output", - stderr="", - outcome=_OutputOutcomeExit( - type="exit", exit_code=0 - ), - ) - ], - ), - # apply_patch_call - _ResponseApplyPatchToolCall( - id="ap-123", - type="apply_patch_call", - status="completed", - created_by="agent", - call_id="call-123", - operation=_OperationCreateFile( - type="create_file", - diff="content", - path="/tmp/test.txt", - ), - ), - # apply_patch_call_output - _ResponseApplyPatchToolCallOutput( - id="apo-123", - type="apply_patch_call_output", - created_by="agent", - call_id="call-123", - status="completed", - output="Applied successfully", - ), - # mcp_call with output - McpCall( - id="mcp-123", - server_label="server1", - name="tool_name", - type="mcp_call", - arguments='{"key": "value"}', - output="result", - error=None, - status="completed", - ), - # mcp_call with error - McpCall( - id="mcp-124", - server_label="server1", - name="tool_name", - type="mcp_call", - arguments='{"key": "value"}', - output=None, - error="Some error", - status="failed", - ), - # mcp_call without output (no response part) - McpCall( - id="mcp-125", - server_label="server2", - name="another_tool", - type="mcp_call", - arguments='{"key": "value2"}', - output=None, - error=None, - ), - # mcp_list_tools with tools - McpListTools( - id="mcpl-123", - server_label="server1", - type="mcp_list_tools", - tools=[ - McpListToolsTool(name="tool1", input_schema={}), - McpListToolsTool(name="tool2", input_schema={}), - ], - error=None, - ), - # mcp_list_tools without tools (no response part) - McpListTools( - id="mcpl-124", - server_label="server2", - type="mcp_list_tools", - tools=[], - error=None, - ), - # mcp_approval_request - McpApprovalRequest( - id="mcpa-123", - server_label="server1", - name="dangerous_tool", - type="mcp_approval_request", - arguments='{"action": "delete"}', - ), - # custom_tool_call - ResponseCustomToolCall( - name="custom_tool", - id="ct-123", - type="custom_tool_call", - call_id="call-ct-123", - input="input", - ), - # fallback with content string - { - "type": "unknown_type", - "content": "fallback content", - }, - # fallback to stringified (no content attribute) - { - "type": "another_unknown", - "data": "some data", + assert parts[2] == { + "type": "reasoning", + "content": "readacted", + } + + assert parts[3] == { + "type": "compaction", + "content": "readacted", + } + + assert tool_calls_by_id["fs-123"] == { + "type": "tool_call", + "name": "file_search", + "arguments": {"queries": ["readacted"]}, + } + + assert tool_calls_by_id["fc-123"] == { + "type": "tool_call", + "name": "get_weather", + "arguments": {"city": "Paris"}, + } + + assert tool_calls_by_id["ws-123"] == { + "type": "tool_call", + "name": "web_search", + "arguments": "readacted", + } + + assert tool_calls_by_id["cc-123"] == { + "type": "tool_call", + "name": "computer", + "arguments": { + "action": { + "type": "click", + } + }, + } + + assert tool_calls_by_id["ci-123"] == { + "type": "tool_call", + "name": "code_interpreter", + "arguments": { + "code": "readacted", + "container_id": "cont-123", + }, + } + + assert tool_call_responses_by_id["ci-123"] == { + "type": "tool_call_response", + "name": "code_interpreter", + "response": { + "status": "completed", + "output": [{"type": "logs"}], + }, + } + + assert tool_calls_by_id["ls-123"] == { + "type": "tool_call", + "name": "local_shell", + "arguments": { + "command": ["readacted"], + "env": "readacted", + "type": "exec", + "user": "readacted", + "working_directory": "readacted", + "timeout_ms": 5000, + }, + } + + assert tool_calls_by_id["sh-123"] == { + "type": "tool_call", + "name": "shell", + "arguments": { + "call_id": "call-123", + "commands": ["readacted"], + "created_by": None, + "environment": None, + "max_output_length": 1000, + "timeout_ms": 5000, + }, + } + + assert tool_calls_by_id["ap-123"] == { + "type": "tool_call", + "name": "apply_patch", + "arguments": { + "call_id": "call-123", + "operation": { + "type": "create_file", + # arguments redacted }, - ] + "created_by": "agent", + }, + } + + assert tool_call_responses_by_id["apo-123"] == { + "type": "tool_call_response", + "name": "apply_patch", + "response": { + "call_id": "call-123", + "created_by": "agent", + "status": "completed", + "output": "readacted", + }, + } + + assert tool_calls_by_id["mcp-123"] == { + "type": "tool_call", + "name": "mcp_call", + "arguments": { + "server": "server1", + "tool_name": "tool_name", + "tool_args": "readacted", + }, + } + + assert tool_call_responses_by_id["mcp-123"] == { + "type": "tool_call_response", + "name": "mcp_call", + "response": { + "output": "readacted", + "error": None, + "status": "completed", + }, + } + + assert tool_call_responses_by_id["mcp-124"] == { + "type": "tool_call_response", + "name": "mcp_call", + "response": { + "output": None, + "error": "Some error", + "status": "failed", + }, + } + + assert tool_calls_by_id["mcp-125"] == { + "type": "tool_call", + "name": "mcp_call", + "arguments": { + "server": "server2", + "tool_name": "another_tool", + "tool_args": "readacted", + }, + } + + assert tool_calls_by_id["mcpl-123"] == { + "type": "tool_call", + "name": "mcp_list_tools", + "arguments": { + "server": "server1", + }, + } + + assert tool_call_responses_by_id["mcpl-123"] == { + "type": "tool_call_response", + "name": "mcp_list_tools", + "response": { + "error": None, + "tools": [ + {"name": "tool1", "input_schema": {}}, + {"name": "tool2", "input_schema": {}}, + ], + }, + } + + assert tool_calls_by_id["mcpl-124"] == { + "type": "tool_call", + "name": "mcp_list_tools", + "arguments": { + "server": "server2", + }, + } + + assert tool_calls_by_id["mcpa-123"] == { + "type": "tool_call", + "name": "mcp_approval_request", + "arguments": { + "server": "server1", + "tool_name": "dangerous_tool", + "tool_args": "readacted", + }, + } + + assert tool_calls_by_id["ct-123"] == { + "type": "tool_call", + "name": "custom_tool", + "arguments": "readacted", + } + + finally: + instrumentor.uninstrument() + exporter.clear() + +def test_response_span_records_response_attributes(): + instrumentor, exporter = _instrument_with_provider() try: with trace("workflow"): @@ -955,6 +1194,15 @@ def __init__(self) -> None: }, } + assert tool_call_responses_by_id["ci-123"] == { + "type": "tool_call_response", + "name": "code_interpreter", + "response": { + "status": "completed", + "output": [{"type": "logs", "logs": "hello"}], + }, + } + assert tool_calls_by_id["ls-123"] == { "type": "tool_call", "name": "local_shell", From 55baca063ad9685a957562761f824273d4cb0020 Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Thu, 19 Feb 2026 11:38:57 +1100 Subject: [PATCH 5/7] Remove include_sensitive_data conditionals from output message parsing --- .../openai_agents/span_processor.py | 229 ++++------------- .../tests/test_tracer.py | 231 ++---------------- 2 files changed, 62 insertions(+), 398 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py index 744c94ffa3..5503a399d3 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py @@ -899,42 +899,28 @@ def _normalize_response_output_part( { "type": "text", "annotations": ( # out of spec but useful - ["readacted"] - if not self.include_sensitive_data - else [ + [ a.to_dict() if hasattr(a, "to_dict") else str(a) for a in getattr(c, "annotations", []) ] ), - "content": ( - "readacted" - if not self.include_sensitive_data - else getattr(c, "text", None) - ), + "content": getattr(c, "text", None), } ) elif content_type == "refusal": parts.append( { "type": "refusal", # custom type - "content": ( - "readacted" - if not self.include_sensitive_data - else getattr(c, "refusal", None) - ), + "content": getattr(c, "refusal", None), } ) else: parts.append( { "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else str(content) - ), + "content": str(content), } ) return parts @@ -944,15 +930,7 @@ def _normalize_response_output_part( "type": "tool_call", "name": "file_search", "id": getattr(item, "id", None), - "arguments": ( - { - "queries": ( - ["readacted"] - if not self.include_sensitive_data - else getattr(item, "queries", []) - ) - } - ), + "arguments": ({"queries": getattr(item, "queries", [])}), } ] elif part_type == "function_call": # ResponseFunctionToolCall @@ -962,21 +940,14 @@ def _normalize_response_output_part( "name": getattr(item, "name", None), "id": getattr(item, "id", None), "arguments": ( - "readacted" - if not self.include_sensitive_data - else safe_json_loads(getattr(item, "arguments", None)) + safe_json_loads(getattr(item, "arguments", None)) ), } ] elif part_type == "web_search_call": # ResponseFunctionWebSearch action = getattr(item, "action", None) - action_type = getattr(action, "type", None) action_obj = ( - {"type": action_type} - if not self.include_sensitive_data - else action.to_dict() - if hasattr(action, "to_dict") - else str(action) + action.to_dict() if hasattr(action, "to_dict") else str(action) ) return [ { @@ -990,13 +961,8 @@ def _normalize_response_output_part( ] elif part_type == "computer_call": # ResponseComputerToolCall action = getattr(item, "action", None) - action_type = getattr(action, "type", None) action_obj = ( - {"type": action_type} - if not self.include_sensitive_data - else action.to_dict() - if hasattr(action, "to_dict") - else str(action) + action.to_dict() if hasattr(action, "to_dict") else str(action) ) return [ { @@ -1026,22 +992,14 @@ def _normalize_response_output_part( return [ { "type": "reasoning", - "content": ( - "readacted" - if not self.include_sensitive_data - else content_str - ), + "content": content_str, } ] elif part_type == "compaction": # ResponseCompactionItem return [ { "type": "compaction", # custom type - "content": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "encrypted_content", None) - ), + "content": getattr(item, "encrypted_content", None), } ] elif part_type == "image_generation_call": # ImageGenerationCall @@ -1050,13 +1008,7 @@ def _normalize_response_output_part( "type": "tool_call_response", "name": "image_generation", "id": getattr(item, "id", None), - "response": { - "result": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "result", None) - ) - }, + "response": {"result": getattr(item, "result", None)}, } ] elif ( @@ -1069,11 +1021,7 @@ def _normalize_response_output_part( "id": getattr(item, "id", None), "arguments": { "container_id": getattr(item, "container_id", None), - "code": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "code", None) - ), + "code": getattr(item, "code", None), }, }, { @@ -1083,11 +1031,9 @@ def _normalize_response_output_part( "response": { "status": getattr(item, "status", None), "outputs": [ - { "type": output.type } - if not self.include_sensitive_data - else output.to_dict() + output.to_dict() for output in getattr(item, "outputs", []) - ] + ], }, }, ] @@ -1101,25 +1047,11 @@ def _normalize_response_output_part( "arguments": { "type": getattr(action, "type", None), "timeout_ms": getattr(action, "timeout_ms", None), - "command": ( - ["readacted"] - if not self.include_sensitive_data - else getattr(action, "command", None) - ), - "env": ( - "readacted" - if not self.include_sensitive_data - else getattr(action, "env", None) - ), - "user": ( - "readacted" - if not self.include_sensitive_data - else getattr(action, "user", None) - ), - "working_directory": ( - "readacted" - if not self.include_sensitive_data - else getattr(action, "working_directory", None) + "command": getattr(action, "command", None), + "env": getattr(action, "env", None), + "user": getattr(action, "user", None), + "working_directory": getattr( + action, "working_directory", None ), }, } @@ -1135,11 +1067,7 @@ def _normalize_response_output_part( "created_by": getattr(action, "created_by", None), "call_id": getattr(item, "call_id", None), "environment": getattr(action, "environment", None), - "commands": ( - ["readacted"] - if not self.include_sensitive_data - else getattr(action, "commands", None) - ), + "commands": getattr(action, "commands", None), "max_output_length": getattr( action, "max_output_length", None ), @@ -1158,14 +1086,10 @@ def _normalize_response_output_part( "response": { "created_by": getattr(item, "created_by", None), "call_id": getattr(item, "call_id", None), - "result": ( - ["readacted"] - if not self.include_sensitive_data - else [ - output.to_dict() - for output in getattr(item, "output", []) - ] - ), + "result": [ + output.to_dict() + for output in getattr(item, "output", []) + ], }, } ] @@ -1181,9 +1105,7 @@ def _normalize_response_output_part( "created_by": getattr(item, "created_by", None), "call_id": getattr(item, "call_id", None), "operation": ( - {"type": operation_type} - if not self.include_sensitive_data - else operation.to_dict() + operation.to_dict() if hasattr(operation, "to_dict") else safe_json_dumps(operation) ), @@ -1202,11 +1124,7 @@ def _normalize_response_output_part( "created_by": getattr(item, "created_by", None), "call_id": getattr(item, "call_id", None), "status": getattr(item, "status", None), - "output": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "output", None) - ), + "output": getattr(item, "output", None), }, } ] @@ -1220,11 +1138,7 @@ def _normalize_response_output_part( "server": getattr(item, "server_label", None), "tool_name": getattr(item, "name", None), "tool_args": ( - "readacted" - if not self.include_sensitive_data - else safe_json_loads( - getattr(item, "arguments", None) - ) + safe_json_loads(getattr(item, "arguments", None)) ), }, } @@ -1236,16 +1150,8 @@ def _normalize_response_output_part( "name": "mcp_call", "id": getattr(item, "id", None), "response": { - "output": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "output", None) - ), - "error": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "error", None) - ), + "output": getattr(item, "output", None), + "error": getattr(item, "error", None), "status": getattr(item, "status", None), }, } @@ -1269,19 +1175,11 @@ def _normalize_response_output_part( "name": "mcp_list_tools", "id": getattr(item, "id", None), "response": { - "tools": ( - "readacted" - if not self.include_sensitive_data - else [ - tool.to_dict() - for tool in getattr(item, "tools", []) - ] - ), - "error": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "error", None) - ), + "tools": [ + tool.to_dict() + for tool in getattr(item, "tools", []) + ], + "error": getattr(item, "error", None), }, } ) @@ -1296,11 +1194,7 @@ def _normalize_response_output_part( "server": getattr(item, "server_label", None), "tool_name": getattr(item, "name", None), "tool_args": ( - "readacted" - if not self.include_sensitive_data - else safe_json_loads( - getattr(item, "arguments", None) - ) + safe_json_loads(getattr(item, "arguments", None)) ), }, } @@ -1311,11 +1205,7 @@ def _normalize_response_output_part( "type": "tool_call", "name": getattr(item, "name", None), "id": getattr(item, "id", None), - "arguments": ( - "readacted" - if not self.include_sensitive_data - else getattr(item, "input", None) - ), + "arguments": getattr(item, "input", None), } ] @@ -1325,9 +1215,7 @@ def _normalize_response_output_part( return [ { "type": "text", - "content": ( - "readacted" if not self.include_sensitive_data else txt - ), + "content": txt, } ] else: @@ -1335,11 +1223,7 @@ def _normalize_response_output_part( return [ { "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else safe_json_dumps(item) - ), + "content": safe_json_dumps(item), } ] @@ -1347,6 +1231,7 @@ def _normalize_output_messages_to_role_parts( self, span_data: Any ) -> list[dict[str, Any]]: """Normalize output messages to enforced role+parts schema. + Contains sensitive content, assumes it will not be called if include_sensitive_data=False. Produces: [{"role": "assistant", "parts": [{"type": "text", "content": "..."}], optional "finish_reason": "..." }] @@ -1374,11 +1259,7 @@ def _normalize_output_messages_to_role_parts( parts.append( { "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else output_text - ), + "content": output_text, } ) @@ -1394,11 +1275,7 @@ def _normalize_output_messages_to_role_parts( parts.append( { "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else txt - ), + "content": txt, } ) elif "content" in item and isinstance( @@ -1407,22 +1284,14 @@ def _normalize_output_messages_to_role_parts( parts.append( { "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else item["content"] - ), + "content": item["content"], } ) else: parts.append( { "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else str(item) - ), + "content": str(item), } ) if not finish_reason and isinstance( @@ -1433,22 +1302,14 @@ def _normalize_output_messages_to_role_parts( parts.append( { "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else item - ), + "content": item, } ) else: parts.append( { "type": "text", - "content": ( - "readacted" - if not self.include_sensitive_data - else str(item) - ), + "content": str(item), } ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py index c81df828f4..f1b8884415 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py @@ -97,6 +97,7 @@ GenAI, "GEN_AI_OUTPUT_MESSAGES", "gen_ai.output.messages" ) + # dummy classes for some response types since concrete type is not available in older `openai` versions class _ResponseCompactionItem: def __init__(self, id: str, type: str, encrypted_content: str) -> None: @@ -104,6 +105,7 @@ def __init__(self, id: str, type: str, encrypted_content: str) -> None: self.type = type self.encrypted_content = encrypted_content + class _ActionShellCall(openai.BaseModel): def __init__( self, commands: list[str], max_output_length: int, timeout_ms: int @@ -114,6 +116,7 @@ def __init__( timeout_ms=timeout_ms, ) + class _ResponseFunctionShellToolCall(openai.BaseModel): def __init__( self, @@ -127,16 +130,19 @@ def __init__( id=id, type=type, status=status, call_id=call_id, action=action ) + class _OutputOutcomeExit(openai.BaseModel): def __init__(self, type: str, exit_code: int) -> None: super().__init__(type=type, exit_code=exit_code) + class _ShellToolCallOutput(openai.BaseModel): def __init__( self, stdout: str, stderr: str, outcome: _OutputOutcomeExit ) -> None: super().__init__(stdout=stdout, stderr=stderr, outcome=outcome) + class _ResponseFunctionShellToolCallOutput(openai.BaseModel): def __init__( self, @@ -150,10 +156,12 @@ def __init__( id=id, type=type, status=status, call_id=call_id, output=output ) + class _OperationCreateFile(openai.BaseModel): def __init__(self, type: str, diff: str, path: str) -> None: super().__init__(type=type, diff=diff, path=path) + class _ResponseApplyPatchToolCall(openai.BaseModel): def __init__( self, @@ -173,6 +181,7 @@ def __init__( operation=operation, ) + class _ResponseApplyPatchToolCallOutput(openai.BaseModel): def __init__( self, @@ -192,11 +201,13 @@ def __init__( output=output, ) + class _Usage: def __init__(self, input_tokens: int, output_tokens: int) -> None: self.input_tokens = input_tokens self.output_tokens = output_tokens + class _Response: def __init__(self) -> None: self.id = "resp-123" @@ -272,9 +283,7 @@ def __init__(self) -> None: call_id="call-cc-123", status="completed", pending_safety_checks=[], - action=ActionClick( - type="click", x=100, y=200, button="left" - ), + action=ActionClick(type="click", x=100, y=200, button="left"), ), # image_generation_call ImageGenerationCall( @@ -329,9 +338,7 @@ def __init__(self) -> None: _ShellToolCallOutput( stdout="shell output", stderr="", - outcome=_OutputOutcomeExit( - type="exit", exit_code=0 - ), + outcome=_OutputOutcomeExit(type="exit", exit_code=0), ) ], ), @@ -436,6 +443,7 @@ def __init__(self) -> None: }, ] + def _instrument_with_provider(**instrument_kwargs): set_trace_processors([]) provider = TracerProvider() @@ -878,217 +886,12 @@ def test_response_span_records_redacted_response_attributes(): assert response.attributes[GenAI.GEN_AI_USAGE_OUTPUT_TOKENS] == 9 # Check output messages are redacted - output_messages = json.loads( - response.attributes[GEN_AI_OUTPUT_MESSAGES] - ) - assert len(output_messages) == 1 - assert output_messages[0]["role"] == "assistant" - parts = output_messages[0]["parts"] - assert parts[0] == { - "type": "text", - "content": "readacted", - "annotations": [], - } - - assert parts[1] == { - "type": "refusal", - "content": "readacted", - } - - assert parts[2] == { - "type": "reasoning", - "content": "readacted", - } - - assert parts[3] == { - "type": "compaction", - "content": "readacted", - } - - assert tool_calls_by_id["fs-123"] == { - "type": "tool_call", - "name": "file_search", - "arguments": {"queries": ["readacted"]}, - } - - assert tool_calls_by_id["fc-123"] == { - "type": "tool_call", - "name": "get_weather", - "arguments": {"city": "Paris"}, - } - - assert tool_calls_by_id["ws-123"] == { - "type": "tool_call", - "name": "web_search", - "arguments": "readacted", - } - - assert tool_calls_by_id["cc-123"] == { - "type": "tool_call", - "name": "computer", - "arguments": { - "action": { - "type": "click", - } - }, - } - - assert tool_calls_by_id["ci-123"] == { - "type": "tool_call", - "name": "code_interpreter", - "arguments": { - "code": "readacted", - "container_id": "cont-123", - }, - } - - assert tool_call_responses_by_id["ci-123"] == { - "type": "tool_call_response", - "name": "code_interpreter", - "response": { - "status": "completed", - "output": [{"type": "logs"}], - }, - } - - assert tool_calls_by_id["ls-123"] == { - "type": "tool_call", - "name": "local_shell", - "arguments": { - "command": ["readacted"], - "env": "readacted", - "type": "exec", - "user": "readacted", - "working_directory": "readacted", - "timeout_ms": 5000, - }, - } - - assert tool_calls_by_id["sh-123"] == { - "type": "tool_call", - "name": "shell", - "arguments": { - "call_id": "call-123", - "commands": ["readacted"], - "created_by": None, - "environment": None, - "max_output_length": 1000, - "timeout_ms": 5000, - }, - } - - assert tool_calls_by_id["ap-123"] == { - "type": "tool_call", - "name": "apply_patch", - "arguments": { - "call_id": "call-123", - "operation": { - "type": "create_file", - # arguments redacted - }, - "created_by": "agent", - }, - } - - assert tool_call_responses_by_id["apo-123"] == { - "type": "tool_call_response", - "name": "apply_patch", - "response": { - "call_id": "call-123", - "created_by": "agent", - "status": "completed", - "output": "readacted", - }, - } - - assert tool_calls_by_id["mcp-123"] == { - "type": "tool_call", - "name": "mcp_call", - "arguments": { - "server": "server1", - "tool_name": "tool_name", - "tool_args": "readacted", - }, - } - - assert tool_call_responses_by_id["mcp-123"] == { - "type": "tool_call_response", - "name": "mcp_call", - "response": { - "output": "readacted", - "error": None, - "status": "completed", - }, - } - - assert tool_call_responses_by_id["mcp-124"] == { - "type": "tool_call_response", - "name": "mcp_call", - "response": { - "output": None, - "error": "Some error", - "status": "failed", - }, - } - - assert tool_calls_by_id["mcp-125"] == { - "type": "tool_call", - "name": "mcp_call", - "arguments": { - "server": "server2", - "tool_name": "another_tool", - "tool_args": "readacted", - }, - } - - assert tool_calls_by_id["mcpl-123"] == { - "type": "tool_call", - "name": "mcp_list_tools", - "arguments": { - "server": "server1", - }, - } - - assert tool_call_responses_by_id["mcpl-123"] == { - "type": "tool_call_response", - "name": "mcp_list_tools", - "response": { - "error": None, - "tools": [ - {"name": "tool1", "input_schema": {}}, - {"name": "tool2", "input_schema": {}}, - ], - }, - } - - assert tool_calls_by_id["mcpl-124"] == { - "type": "tool_call", - "name": "mcp_list_tools", - "arguments": { - "server": "server2", - }, - } - - assert tool_calls_by_id["mcpa-123"] == { - "type": "tool_call", - "name": "mcp_approval_request", - "arguments": { - "server": "server1", - "tool_name": "dangerous_tool", - "tool_args": "readacted", - }, - } - - assert tool_calls_by_id["ct-123"] == { - "type": "tool_call", - "name": "custom_tool", - "arguments": "readacted", - } - + assert GEN_AI_OUTPUT_MESSAGES not in response.attributes finally: instrumentor.uninstrument() exporter.clear() + def test_response_span_records_response_attributes(): instrumentor, exporter = _instrument_with_provider() @@ -1199,7 +1002,7 @@ def test_response_span_records_response_attributes(): "name": "code_interpreter", "response": { "status": "completed", - "output": [{"type": "logs", "logs": "hello"}], + "outputs": [{"type": "logs", "logs": "hello"}], }, } From fa628a6ffdf45ad99fdf5f1c39c73768f1fadf84 Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Thu, 19 Feb 2026 15:36:56 +1100 Subject: [PATCH 6/7] linting fixes --- .../openai_agents/span_processor.py | 49 ++++--- .../tests/test_tracer.py | 126 +++++------------- 2 files changed, 54 insertions(+), 121 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py index 5503a399d3..909dd33310 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py @@ -933,7 +933,7 @@ def _normalize_response_output_part( "arguments": ({"queries": getattr(item, "queries", [])}), } ] - elif part_type == "function_call": # ResponseFunctionToolCall + if part_type == "function_call": # ResponseFunctionToolCall return [ { "type": "tool_call", @@ -944,7 +944,7 @@ def _normalize_response_output_part( ), } ] - elif part_type == "web_search_call": # ResponseFunctionWebSearch + if part_type == "web_search_call": # ResponseFunctionWebSearch action = getattr(item, "action", None) action_obj = ( action.to_dict() if hasattr(action, "to_dict") else str(action) @@ -959,7 +959,7 @@ def _normalize_response_output_part( }, } ] - elif part_type == "computer_call": # ResponseComputerToolCall + if part_type == "computer_call": # ResponseComputerToolCall action = getattr(item, "action", None) action_obj = ( action.to_dict() if hasattr(action, "to_dict") else str(action) @@ -974,7 +974,7 @@ def _normalize_response_output_part( }, } ] - elif part_type == "reasoning": # ResponseReasoningItem + if part_type == "reasoning": # ResponseReasoningItem content = getattr(item, "content", None) content_str = ( str.join( @@ -995,14 +995,14 @@ def _normalize_response_output_part( "content": content_str, } ] - elif part_type == "compaction": # ResponseCompactionItem + if part_type == "compaction": # ResponseCompactionItem return [ { "type": "compaction", # custom type "content": getattr(item, "encrypted_content", None), } ] - elif part_type == "image_generation_call": # ImageGenerationCall + if part_type == "image_generation_call": # ImageGenerationCall return [ { "type": "tool_call_response", @@ -1011,7 +1011,7 @@ def _normalize_response_output_part( "response": {"result": getattr(item, "result", None)}, } ] - elif ( + if ( part_type == "code_interpreter_call" ): # ResponseCodeInterpreterToolCall return [ @@ -1037,7 +1037,7 @@ def _normalize_response_output_part( }, }, ] - elif part_type == "local_shell_call": # LocalShellCall + if part_type == "local_shell_call": # LocalShellCall action = getattr(item, "action", None) return [ { @@ -1056,7 +1056,7 @@ def _normalize_response_output_part( }, } ] - elif part_type == "shell_call": # ResponseFunctionShellToolCall + if part_type == "shell_call": # ResponseFunctionShellToolCall action = getattr(item, "action", None) return [ { @@ -1075,7 +1075,7 @@ def _normalize_response_output_part( }, } ] - elif ( + if ( part_type == "shell_call_output" ): # ResponseFunctionShellToolCallOutput return [ @@ -1093,9 +1093,8 @@ def _normalize_response_output_part( }, } ] - elif part_type == "apply_patch_call": # ResponseApplyPatchToolCall + if part_type == "apply_patch_call": # ResponseApplyPatchToolCall operation = getattr(item, "operation", None) - operation_type = getattr(operation, "type", None) return [ { "type": "tool_call", @@ -1112,7 +1111,7 @@ def _normalize_response_output_part( }, } ] - elif ( + if ( part_type == "apply_patch_call_output" ): # ResponseApplyPatchToolCallOutput return [ @@ -1128,7 +1127,7 @@ def _normalize_response_output_part( }, } ] - elif part_type == "mcp_call": # McpCall + if part_type == "mcp_call": # McpCall parts = [ { "type": "tool_call", @@ -1157,7 +1156,7 @@ def _normalize_response_output_part( } ) return parts - elif part_type == "mcp_list_tools": # McpListTools + if part_type == "mcp_list_tools": # McpListTools parts = [ { "type": "tool_call", @@ -1184,7 +1183,7 @@ def _normalize_response_output_part( } ) return parts - elif part_type == "mcp_approval_request": # McpApprovalRequest + if part_type == "mcp_approval_request": # McpApprovalRequest return [ { "type": "tool_call", @@ -1199,7 +1198,7 @@ def _normalize_response_output_part( }, } ] - elif part_type == "custom_tool_call": # ResponseCustomToolCall + if part_type == "custom_tool_call": # ResponseCustomToolCall return [ { "type": "tool_call", @@ -1218,14 +1217,14 @@ def _normalize_response_output_part( "content": txt, } ] - else: - # Fallback: stringified - return [ - { - "type": "text", - "content": safe_json_dumps(item), - } - ] + + # Fallback: stringified + return [ + { + "type": "text", + "content": safe_json_dumps(item), + } + ] def _normalize_output_messages_to_role_parts( self, span_data: Any diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py index f1b8884415..839be53b86 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py @@ -1,4 +1,5 @@ -# pylint: disable=wrong-import-position,wrong-import-order,import-error,no-name-in-module,unexpected-keyword-arg,no-value-for-parameter,redefined-outer-name +# ruff: noqa: E402 +# pylint: disable=wrong-import-position,wrong-import-order,import-error,no-name-in-module,unexpected-keyword-arg,no-value-for-parameter,redefined-outer-name,too-many-lines from __future__ import annotations @@ -16,9 +17,9 @@ sys.modules.pop("agents", None) sys.modules.pop("agents.tracing", None) -import agents.tracing as agents_tracing # noqa: E402 -import openai # noqa: E402 -from agents.tracing import ( # noqa: E402 +import agents.tracing as agents_tracing +import openai +from agents.tracing import ( agent_span, function_span, generation_span, @@ -26,7 +27,7 @@ set_trace_processors, trace, ) -from openai.types.responses import ( # noqa: E402 +from openai.types.responses import ( ResponseCodeInterpreterToolCall, ResponseComputerToolCall, ResponseCustomToolCall, @@ -38,16 +39,16 @@ ResponseOutputText, ResponseReasoningItem, ) -from openai.types.responses.response_code_interpreter_tool_call import ( # noqa: E402 +from openai.types.responses.response_code_interpreter_tool_call import ( OutputLogs, ) -from openai.types.responses.response_computer_tool_call import ( # noqa: E402 +from openai.types.responses.response_computer_tool_call import ( ActionClick, ) -from openai.types.responses.response_function_web_search import ( # noqa: E402 +from openai.types.responses.response_function_web_search import ( ActionSearch as ActionWebSearch, ) -from openai.types.responses.response_output_item import ( # noqa: E402 +from openai.types.responses.response_output_item import ( ImageGenerationCall, LocalShellCall, LocalShellCallAction, @@ -56,38 +57,38 @@ McpListTools, McpListToolsTool, ) -from openai.types.responses.response_reasoning_item import ( # noqa: E402 +from openai.types.responses.response_reasoning_item import ( Content, ) -from opentelemetry.instrumentation.openai_agents import ( # noqa: E402 +from opentelemetry.instrumentation.openai_agents import ( OpenAIAgentsInstrumentor, ) -from opentelemetry.instrumentation.openai_agents.span_processor import ( # noqa: E402 +from opentelemetry.instrumentation.openai_agents.span_processor import ( ContentPayload, GenAISemanticProcessor, ) -from opentelemetry.sdk.trace import TracerProvider # noqa: E402 +from opentelemetry.sdk.trace import TracerProvider try: - from opentelemetry.sdk.trace.export import ( # type: ignore[attr-defined] + from opentelemetry.sdk.trace.export import ( # type: ignore[attr-defined] pylint: disable=no-name-in-module InMemorySpanExporter, SimpleSpanProcessor, ) except ImportError: # pragma: no cover - support older/newer SDK layouts from opentelemetry.sdk.trace.export import ( - SimpleSpanProcessor, # noqa: E402 + SimpleSpanProcessor, ) - from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( # noqa: E402 + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) -from opentelemetry.semconv._incubating.attributes import ( # noqa: E402 +from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAI, ) -from opentelemetry.semconv._incubating.attributes import ( # noqa: E402 +from opentelemetry.semconv._incubating.attributes import ( server_attributes as ServerAttributes, ) -from opentelemetry.trace import SpanKind # noqa: E402 +from opentelemetry.trace import SpanKind GEN_AI_PROVIDER_NAME = GenAI.GEN_AI_PROVIDER_NAME GEN_AI_INPUT_MESSAGES = getattr( @@ -99,107 +100,40 @@ # dummy classes for some response types since concrete type is not available in older `openai` versions -class _ResponseCompactionItem: - def __init__(self, id: str, type: str, encrypted_content: str) -> None: - self.id = id - self.type = type - self.encrypted_content = encrypted_content +class _ResponseCompactionItem(openai.BaseModel): + pass class _ActionShellCall(openai.BaseModel): - def __init__( - self, commands: list[str], max_output_length: int, timeout_ms: int - ) -> None: - super().__init__( - commands=commands, - max_output_length=max_output_length, - timeout_ms=timeout_ms, - ) + pass class _ResponseFunctionShellToolCall(openai.BaseModel): - def __init__( - self, - id: str, - type: str, - status: str, - call_id: str, - action: _ActionShellCall, - ) -> None: - super().__init__( - id=id, type=type, status=status, call_id=call_id, action=action - ) + pass class _OutputOutcomeExit(openai.BaseModel): - def __init__(self, type: str, exit_code: int) -> None: - super().__init__(type=type, exit_code=exit_code) + pass class _ShellToolCallOutput(openai.BaseModel): - def __init__( - self, stdout: str, stderr: str, outcome: _OutputOutcomeExit - ) -> None: - super().__init__(stdout=stdout, stderr=stderr, outcome=outcome) + pass class _ResponseFunctionShellToolCallOutput(openai.BaseModel): - def __init__( - self, - id: str, - type: str, - status: str, - call_id: str, - output: list[_ShellToolCallOutput], - ) -> None: - super().__init__( - id=id, type=type, status=status, call_id=call_id, output=output - ) + pass class _OperationCreateFile(openai.BaseModel): - def __init__(self, type: str, diff: str, path: str) -> None: - super().__init__(type=type, diff=diff, path=path) + pass class _ResponseApplyPatchToolCall(openai.BaseModel): - def __init__( - self, - id: str, - type: str, - status: str, - created_by: str, - call_id: str, - operation: _OperationCreateFile, - ) -> None: - super().__init__( - id=id, - type=type, - status=status, - created_by=created_by, - call_id=call_id, - operation=operation, - ) + pass class _ResponseApplyPatchToolCallOutput(openai.BaseModel): - def __init__( - self, - id: str, - type: str, - created_by: str, - call_id: str, - status: str, - output: str, - ) -> None: - super().__init__( - id=id, - type=type, - created_by=created_by, - call_id=call_id, - status=status, - output=output, - ) + pass class _Usage: From a2e44b241885267f70d7a13e70585a8c28512ec7 Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Thu, 19 Feb 2026 15:46:44 +1100 Subject: [PATCH 7/7] changelog --- .../opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md index 95f69d6ded..cf960c3f84 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - Document official package metadata and README for the OpenAI Agents instrumentation. ([#3859](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3859)) +- Handle various types of span_data.response.output parts. + ([#4208](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4208)) ## Version 0.1.0 (2025-10-15)