From 1afe5d6e8abcf9aa79a2475fed4a319eb7b473bc Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 11:09:42 +0200 Subject: [PATCH 01/22] feat(openai): Set gen_ai.tool.definitions attribute --- sentry_sdk/ai/_openai_completions_api.py | 55 +++++- sentry_sdk/integrations/openai.py | 16 +- tests/integrations/openai/test_openai.py | 233 ++++++++++++++++------- 3 files changed, 225 insertions(+), 79 deletions(-) diff --git a/sentry_sdk/ai/_openai_completions_api.py b/sentry_sdk/ai/_openai_completions_api.py index b5eb8c55ef..57e35397bd 100644 --- a/sentry_sdk/ai/_openai_completions_api.py +++ b/sentry_sdk/ai/_openai_completions_api.py @@ -8,9 +8,10 @@ ChatCompletionContentPartParam, ChatCompletionMessageParam, ChatCompletionSystemMessageParam, + ChatCompletionToolUnionParam, ) - from sentry_sdk._types import TextPart + from sentry_sdk._types import TextPart, ToolDefinition def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: @@ -64,3 +65,55 @@ def _transform_system_instructions( instruction_text_parts += text_parts return instruction_text_parts + + +def _transform_tool_definitions( + tools: "Iterable[ChatCompletionToolUnionParam]", +) -> "list[ToolDefinition]": + """ + Transform tool definitions to the schema used by the "gen_ai.tool.definitions" attribute. + """ + if not isinstance(tools, Iterable): + return [] + + tool_definitions = [] + for tool in tools: + if not isinstance(tool, dict): + continue + + if tool["type"] == "function": + tool_definition: ToolDefinition = { + "type": "function", + } + + if "function" not in tool: + tool_definitions.append(tool_definition) + continue + + if "name" in tool["function"]: + tool_definition["name"] = tool["function"]["name"] + + if "description" in tool["function"]: + tool_definition["description"] = tool["function"]["description"] + + if "parameters" in tool["function"]: + tool_definition["parameters"] = tool["function"]["parameters"] + + tool_definitions.append(tool_definition) + continue + + if tool["type"] == "custom": + tool_definition = { + "type": "custom", + } + + if "name" in tool["custom"]: + tool_definition["name"] = tool["custom"]["name"] + + if "description" in tool["custom"]: + tool_definition["description"] = tool["custom"]["description"] + + tool_definitions.append(tool_definition) + continue + + return tool_definitions diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 8a77668329..1b25b6b962 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -17,6 +17,9 @@ from sentry_sdk.ai._openai_completions_api import ( _is_system_instruction as _is_system_instruction_completions, ) +from sentry_sdk.ai._openai_completions_api import ( + _transform_tool_definitions as _transform_tool_definitions_completions, +) from sentry_sdk.ai._openai_responses_api import ( _get_system_instructions as _get_system_instructions_responses, ) @@ -463,15 +466,16 @@ def _set_completions_api_input_data( "messages" ) - tools = kwargs.get("tools") - if tools is not None and _is_given(tools) and len(tools) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - set_on_span = ( span.set_attribute if isinstance(span, StreamedSpan) else span.set_data ) + tools = kwargs.get("tools") + if tools is not None and _is_given(tools): + set_on_span( + SPANDATA.GEN_AI_TOOL_DEFINITIONS, + json.dumps(_transform_tool_definitions_completions(tools)), + ) + model = kwargs.get("model") if model is not None: set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 7d243657f7..36e37f7fbd 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -107,6 +107,167 @@ async def __call__(self, *args, **kwargs): ) +@pytest.mark.parametrize("span_streaming", [True, False]) +@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) +def test_chat_completion_tool_definitions( + sentry_init, + capture_events, + capture_items, + nonstreaming_chat_completions_model_response, + stream_gen_ai_spans, + span_streaming, +): + sentry_init( + integrations=[OpenAIIntegration()], + disabled_integrations=[StdlibIntegration], + traces_sample_rate=1.0, + stream_gen_ai_spans=stream_gen_ai_spans, + trace_lifecycle="stream" if span_streaming else "static", + ) + + client = OpenAI(api_key="z") + client.chat.completions._post = mock.Mock( + return_value=nonstreaming_chat_completions_model_response( + response_id="chat-id", + response_model="gpt-3.5-turbo", + message_content="the model response", + created=10000000, + usage=CompletionUsage( + prompt_tokens=20, + completion_tokens=10, + total_tokens=30, + ), + ) + ) + + if span_streaming or stream_gen_ai_spans: + items = capture_items("span") + + with start_transaction(name="openai tx"): + client.chat.completions.create( + model="some-model", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "hello"}, + ], + tools=[ + { + "type": "function", + "function": { + "name": "name", + "description": "description", + "parameters": { + "type": "object", + "properties": { + "city": {"type": "string"}, + "state": {"type": "string"}, + }, + "required": ["city", "state"], + "additionalProperties": False, + }, + "strict": True, + }, + }, + { + "type": "custom", + "custom": { + "name": "name", + "description": "description", + }, + }, + ], + ) + + sentry_sdk.flush() + span = next(item.payload for item in items) + + assert json.loads(span["attributes"][SPANDATA.GEN_AI_TOOL_DEFINITIONS]) == [ + { + "type": "function", + "name": "name", + "description": "description", + "parameters": { + "type": "object", + "properties": { + "city": {"type": "string"}, + "state": {"type": "string"}, + }, + "required": ["city", "state"], + "additionalProperties": False, + }, + }, + { + "type": "custom", + "name": "name", + "description": "description", + }, + ] + else: + events = capture_events() + + with start_transaction(name="openai tx"): + client.chat.completions.create( + model="some-model", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "hello"}, + ], + tools=[ + { + "type": "function", + "function": { + "name": "name", + "description": "description", + "parameters": { + "type": "object", + "properties": { + "city": {"type": "string"}, + "state": {"type": "string"}, + }, + "required": ["city", "state"], + "additionalProperties": False, + }, + "strict": True, + }, + }, + { + "type": "custom", + "custom": { + "name": "name", + "description": "description", + }, + }, + ], + ) + + tx = events[0] + assert tx["type"] == "transaction" + span = tx["spans"][0] + print(tx["spans"]) + + assert json.loads(span["data"][SPANDATA.GEN_AI_TOOL_DEFINITIONS]) == [ + { + "type": "function", + "name": "name", + "description": "description", + "parameters": { + "type": "object", + "properties": { + "city": {"type": "string"}, + "state": {"type": "string"}, + }, + "required": ["city", "state"], + "additionalProperties": False, + }, + }, + { + "type": "custom", + "name": "name", + "description": "description", + }, + ] + + @pytest.mark.parametrize("span_streaming", [True, False]) @pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) @pytest.mark.parametrize( @@ -5617,78 +5778,6 @@ async def test_streaming_responses_api_async( assert span["data"]["gen_ai.usage.total_tokens"] == 30 -@pytest.mark.parametrize("span_streaming", [True, False]) -@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) -@pytest.mark.skipif( - OPENAI_VERSION <= (1, 1, 0), - reason="OpenAI versions <=1.1.0 do not support the tools parameter.", -) -@pytest.mark.parametrize( - "tools", - [[], None, NOT_GIVEN, omit], -) -def test_empty_tools_in_chat_completion( - sentry_init, - capture_events, - capture_items, - tools, - nonstreaming_chat_completions_model_response, - stream_gen_ai_spans, - span_streaming, -): - sentry_init( - integrations=[OpenAIIntegration()], - disabled_integrations=[StdlibIntegration], - traces_sample_rate=1.0, - stream_gen_ai_spans=stream_gen_ai_spans, - trace_lifecycle="stream" if span_streaming else "static", - ) - - client = OpenAI(api_key="z") - client.chat.completions._post = mock.Mock( - return_value=nonstreaming_chat_completions_model_response( - response_id="chat-id", - response_model="gpt-3.5-turbo", - message_content="the model response", - created=10000000, - usage=CompletionUsage( - prompt_tokens=20, - completion_tokens=10, - total_tokens=30, - ), - ) - ) - - if span_streaming or stream_gen_ai_spans: - items = capture_items("span") - - with start_transaction(name="openai tx"): - client.chat.completions.create( - model="some-model", - messages=[{"role": "system", "content": "hello"}], - tools=tools, - ) - - sentry_sdk.flush() - span = next(item.payload for item in items) - - assert "gen_ai.request.available_tools" not in span["attributes"] - else: - events = capture_events() - - with start_transaction(name="openai tx"): - client.chat.completions.create( - model="some-model", - messages=[{"role": "system", "content": "hello"}], - tools=tools, - ) - - (event,) = events - span = event["spans"][0] - - assert "gen_ai.request.available_tools" not in span["data"] - - @pytest.mark.parametrize("span_streaming", [True, False]) @pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) # Feature added in https://github.com/openai/openai-python/pull/1952 From 33faef1bb98568041d4fddc88b7d0326ca69b3ca Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 11:11:29 +0200 Subject: [PATCH 02/22] add type --- sentry_sdk/_types.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index e91fed097c..d3c70ddfa4 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -472,6 +472,12 @@ class TextPart(TypedDict): type: Literal["text"] content: str + class ToolDefinition(TypedDict): + type: str + name: NotRequired[str] + description: NotRequired[str] + parameters: NotRequired[dict[str, object]] + IgnoreSpansName = Union[str, Pattern[str]] IgnoreSpansContext = TypedDict( "IgnoreSpansContext", From 9ba36b2a42ee206a4e47845c6004cb36c7c2fdc2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 11:47:51 +0200 Subject: [PATCH 03/22] feat(openai): Set attribute for the Responses API --- sentry_sdk/ai/_openai_responses_api.py | 64 +++++++- sentry_sdk/integrations/openai.py | 18 ++- tests/integrations/openai/test_openai.py | 196 +++++++++++++++++++++++ 3 files changed, 268 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/ai/_openai_responses_api.py b/sentry_sdk/ai/_openai_responses_api.py index 8f751c3248..15841b0ca4 100644 --- a/sentry_sdk/ai/_openai_responses_api.py +++ b/sentry_sdk/ai/_openai_responses_api.py @@ -1,9 +1,15 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Iterable if TYPE_CHECKING: - from typing import Union + from typing import Iterable, Union - from openai.types.responses import ResponseInputItemParam, ResponseInputParam + from openai.types.responses import ( + ResponseInputItemParam, + ResponseInputParam, + ToolParam, + ) + + from sentry_sdk._types import ToolDefinition def _is_system_instruction(message: "ResponseInputItemParam") -> bool: @@ -20,3 +26,55 @@ def _get_system_instructions( return [] return [message for message in messages if _is_system_instruction(message)] + + +def _transform_tool_definitions(tools: "Iterable[ToolParam]") -> "list[ToolDefinition]": + """ + Transform tool definitions to the schema used by the "gen_ai.tool.definitions" attribute. + Includes special handling for tools where the type includes a name, description or parameters. + """ + if not isinstance(tools, Iterable): + return [] + + tool_definitions = [] + for tool in tools: + if not isinstance(tool, dict) or "type" not in tool: + continue + + if tool["type"] == "function": + tool_definition: ToolDefinition = { + "type": "function", + } + + if "name" in tool: + tool_definition["name"] = tool["name"] + + if "description" in tool: + tool_definition["description"] = tool["description"] + + if "parameters" in tool: + tool_definition["parameters"] = tool["parameters"] + + tool_definitions.append(tool_definition) + continue + + if tool["type"] == "custom": + tool_definition = { + "type": "custom", + } + + if "name" in tool: + tool_definition["name"] = tool["name"] + + if "description" in tool: + tool_definition["description"] = tool["description"] + + tool_definitions.append(tool_definition) + continue + + if "type" not in tool: + continue + + tool_definitions.append({"type": tool["type"]}) + + return tool_definitions diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 1b25b6b962..a43aad0bfc 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -26,6 +26,9 @@ from sentry_sdk.ai._openai_responses_api import ( _is_system_instruction as _is_system_instruction_responses, ) +from sentry_sdk.ai._openai_responses_api import ( + _transform_tool_definitions as _transform_tool_definitions_responses, +) from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.ai.utils import ( get_start_span_function, @@ -46,7 +49,6 @@ capture_internal_exceptions, event_from_exception, reraise, - safe_serialize, ) if TYPE_CHECKING: @@ -330,15 +332,17 @@ def _set_responses_api_input_data( explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions") messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input") - tools = kwargs.get("tools") - if tools is not None and _is_given(tools) and len(tools) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - set_on_span = ( span.set_attribute if isinstance(span, StreamedSpan) else span.set_data ) + + tools = kwargs.get("tools") + if tools is not None and _is_given(tools): + set_on_span( + SPANDATA.GEN_AI_TOOL_DEFINITIONS, + json.dumps(_transform_tool_definitions_responses(tools)), + ) + model = kwargs.get("model") if model is not None: set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 36e37f7fbd..6a1cf19c51 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -22,6 +22,11 @@ from openai.types.chat.chat_completion_chunk import Choice as DeltaChoice from openai.types.chat.chat_completion_chunk import ChoiceDelta from openai.types.create_embedding_response import Usage as EmbeddingTokenUsage +from openai.types.responses import ( + CustomToolParam, + FunctionToolParam, + WebSearchToolParam, +) SKIP_RESPONSES_TESTS = False @@ -4173,6 +4178,197 @@ def test_ai_client_span_responses_api_no_pii( assert "gen_ai.response.text" not in spans[0]["data"] +@pytest.mark.parametrize("span_streaming", [True, False]) +@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) +@pytest.mark.skipif(SKIP_RESPONSES_TESTS, reason="Responses API not available") +def test_ai_client_span_responses_tool_definitions( + sentry_init, + capture_events, + capture_items, + stream_gen_ai_spans, + span_streaming, +): + sentry_init( + integrations=[OpenAIIntegration()], + disabled_integrations=[StdlibIntegration], + traces_sample_rate=1.0, + stream_gen_ai_spans=stream_gen_ai_spans, + trace_lifecycle="stream" if span_streaming else "static", + ) + + client = OpenAI(api_key="z") + client.responses._post = mock.Mock(return_value=EXAMPLE_RESPONSE) + + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="openai tx"): + client.responses.create( + model="gpt-4o", + input="How do I check if a Python object is an instance of a class?", + tools=[ + FunctionToolParam( + type="function", + name="name", + description="description", + parameters={ + "type": "object", + "properties": { + "city": {"type": "string"}, + "state": {"type": "string"}, + }, + "required": ["city", "state"], + "additionalProperties": False, + }, + strict=True, + ), + CustomToolParam( + type="custom", name="name", description="description" + ), + WebSearchToolParam(type="web_search"), + ], + ) + + sentry_sdk.flush() + spans = [item.payload for item in items] + assert json.loads(spans[0]["attributes"][SPANDATA.GEN_AI_TOOL_DEFINITIONS]) == [ + { + "type": "function", + "name": "name", + "description": "description", + "parameters": { + "type": "object", + "properties": { + "city": {"type": "string"}, + "state": {"type": "string"}, + }, + "required": ["city", "state"], + "additionalProperties": False, + }, + }, + { + "type": "custom", + "name": "name", + "description": "description", + }, + { + "type": "web_search", + }, + ] + elif stream_gen_ai_spans: + items = capture_items("span") + + with start_transaction(name="openai tx"): + client.responses.create( + model="gpt-4o", + input="How do I check if a Python object is an instance of a class?", + tools=[ + FunctionToolParam( + type="function", + name="name", + description="description", + parameters={ + "type": "object", + "properties": { + "city": {"type": "string"}, + "state": {"type": "string"}, + }, + "required": ["city", "state"], + "additionalProperties": False, + }, + strict=True, + ), + CustomToolParam( + type="custom", name="name", description="description" + ), + WebSearchToolParam(type="web_search"), + ], + ) + + spans = [item.payload for item in items] + assert json.loads(spans[0]["attributes"][SPANDATA.GEN_AI_TOOL_DEFINITIONS]) == [ + { + "type": "function", + "name": "name", + "description": "description", + "parameters": { + "type": "object", + "properties": { + "city": {"type": "string"}, + "state": {"type": "string"}, + }, + "required": ["city", "state"], + "additionalProperties": False, + }, + }, + { + "type": "custom", + "name": "name", + "description": "description", + }, + { + "type": "web_search", + }, + ] + else: + events = capture_events() + + with start_transaction(name="openai tx"): + client.responses.create( + model="gpt-4o", + input="How do I check if a Python object is an instance of a class?", + tools=[ + FunctionToolParam( + type="function", + name="name", + description="description", + parameters={ + "type": "object", + "properties": { + "city": {"type": "string"}, + "state": {"type": "string"}, + }, + "required": ["city", "state"], + "additionalProperties": False, + }, + strict=True, + ), + CustomToolParam( + type="custom", name="name", description="description" + ), + WebSearchToolParam(type="web_search"), + ], + ) + + (transaction,) = events + spans = transaction["spans"] + + assert json.loads(spans[0]["data"][SPANDATA.GEN_AI_TOOL_DEFINITIONS]) == [ + { + "type": "function", + "name": "name", + "description": "description", + "parameters": { + "type": "object", + "properties": { + "city": {"type": "string"}, + "state": {"type": "string"}, + }, + "required": ["city", "state"], + "additionalProperties": False, + }, + }, + { + "type": "custom", + "name": "name", + "description": "description", + }, + { + "type": "web_search", + }, + ] + + @pytest.mark.parametrize("span_streaming", [True, False]) @pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) @pytest.mark.parametrize( From 7398e26bc2a1a42e1484fc76652fd5fccb442b6f Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 11:49:46 +0200 Subject: [PATCH 04/22] make iteration safer --- sentry_sdk/ai/_openai_completions_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/ai/_openai_completions_api.py b/sentry_sdk/ai/_openai_completions_api.py index 57e35397bd..a61a3e7814 100644 --- a/sentry_sdk/ai/_openai_completions_api.py +++ b/sentry_sdk/ai/_openai_completions_api.py @@ -78,7 +78,7 @@ def _transform_tool_definitions( tool_definitions = [] for tool in tools: - if not isinstance(tool, dict): + if not isinstance(tool, dict) or "type" not in tool: continue if tool["type"] == "function": From 0cd572b407edacca658f17a1869990710f35d23d Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 11:52:56 +0200 Subject: [PATCH 05/22] clean up test --- tests/integrations/openai/test_openai.py | 49 ++++++++++++++---------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 36e37f7fbd..2510ca970a 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -17,11 +17,18 @@ from openai import AsyncOpenAI, AsyncStream, OpenAI, OpenAIError, Stream from openai.types import CompletionUsage, CreateEmbeddingResponse, Embedding -from openai.types.chat import ChatCompletionChunk, ChatCompletionMessage +from openai.types.chat import ( + ChatCompletionChunk, + ChatCompletionCustomToolParam, + ChatCompletionFunctionToolParam, + ChatCompletionMessage, +) from openai.types.chat.chat_completion import Choice from openai.types.chat.chat_completion_chunk import Choice as DeltaChoice from openai.types.chat.chat_completion_chunk import ChoiceDelta +from openai.types.chat.chat_completion_custom_tool_param import Custom from openai.types.create_embedding_response import Usage as EmbeddingTokenUsage +from openai.types.shared_params import FunctionDefinition SKIP_RESPONSES_TESTS = False @@ -107,6 +114,10 @@ async def __call__(self, *args, **kwargs): ) +@pytest.mark.skipif( + OPENAI_VERSION <= (1, 1, 0), + reason="OpenAI versions <=1.1.0 do not support the tools parameter.", +) @pytest.mark.parametrize("span_streaming", [True, False]) @pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) def test_chat_completion_tool_definitions( @@ -151,12 +162,12 @@ def test_chat_completion_tool_definitions( {"role": "user", "content": "hello"}, ], tools=[ - { - "type": "function", - "function": { - "name": "name", - "description": "description", - "parameters": { + ChatCompletionFunctionToolParam( + type="function", + function=FunctionDefinition( + name="name", + description="description", + parameters={ "type": "object", "properties": { "city": {"type": "string"}, @@ -165,16 +176,16 @@ def test_chat_completion_tool_definitions( "required": ["city", "state"], "additionalProperties": False, }, - "strict": True, - }, - }, - { - "type": "custom", - "custom": { - "name": "name", - "description": "description", - }, - }, + strict=True, + ), + ), + ChatCompletionCustomToolParam( + type="custom", + custom=Custom( + name="name", + description="description", + ), + ), ], ) @@ -242,10 +253,8 @@ def test_chat_completion_tool_definitions( tx = events[0] assert tx["type"] == "transaction" - span = tx["spans"][0] - print(tx["spans"]) - assert json.loads(span["data"][SPANDATA.GEN_AI_TOOL_DEFINITIONS]) == [ + assert json.loads(tx["spans"][0]["data"][SPANDATA.GEN_AI_TOOL_DEFINITIONS]) == [ { "type": "function", "name": "name", From 6d8546aaa32da6d2a4ad00fb3a0d24e34c1ccff5 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 11:53:46 +0200 Subject: [PATCH 06/22] clean up --- tests/integrations/openai/test_openai.py | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 2510ca970a..d3c88cc2b4 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -224,12 +224,12 @@ def test_chat_completion_tool_definitions( {"role": "user", "content": "hello"}, ], tools=[ - { - "type": "function", - "function": { - "name": "name", - "description": "description", - "parameters": { + ChatCompletionFunctionToolParam( + type="function", + function=FunctionDefinition( + name="name", + description="description", + parameters={ "type": "object", "properties": { "city": {"type": "string"}, @@ -238,16 +238,16 @@ def test_chat_completion_tool_definitions( "required": ["city", "state"], "additionalProperties": False, }, - "strict": True, - }, - }, - { - "type": "custom", - "custom": { - "name": "name", - "description": "description", - }, - }, + strict=True, + ), + ), + ChatCompletionCustomToolParam( + type="custom", + custom=Custom( + name="name", + description="description", + ), + ), ], ) From 54ca1d9e337782c637ac89b2f17a5358d76998d2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 12:04:06 +0200 Subject: [PATCH 07/22] higher version bound --- tests/integrations/openai/test_openai.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index d3c88cc2b4..1a233986e3 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -30,6 +30,11 @@ from openai.types.create_embedding_response import Usage as EmbeddingTokenUsage from openai.types.shared_params import FunctionDefinition +try: + from openai.types.chat import ChatCompletionCustomToolParam +except ImportError: + pass + SKIP_RESPONSES_TESTS = False try: @@ -115,8 +120,8 @@ async def __call__(self, *args, **kwargs): @pytest.mark.skipif( - OPENAI_VERSION <= (1, 1, 0), - reason="OpenAI versions <=1.1.0 do not support the tools parameter.", + OPENAI_VERSION <= (2, 10, 0), + reason="ChatCompletionCustomToolParam is unavailable before.", ) @pytest.mark.parametrize("span_streaming", [True, False]) @pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) From f94e12e5a12ace4e587c5e7864a1cbdefaf0af58 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 13:15:27 +0200 Subject: [PATCH 08/22] fix import error --- tests/integrations/openai/test_openai.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 1a233986e3..9dcadcfab0 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -19,7 +19,6 @@ from openai.types import CompletionUsage, CreateEmbeddingResponse, Embedding from openai.types.chat import ( ChatCompletionChunk, - ChatCompletionCustomToolParam, ChatCompletionFunctionToolParam, ChatCompletionMessage, ) From 8048f1dad3fb2dad19090d6f4bfe5bc59e58d55e Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 13:18:35 +0200 Subject: [PATCH 09/22] fix import --- tests/integrations/openai/test_openai.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 9dcadcfab0..aad6113d7e 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -19,7 +19,6 @@ from openai.types import CompletionUsage, CreateEmbeddingResponse, Embedding from openai.types.chat import ( ChatCompletionChunk, - ChatCompletionFunctionToolParam, ChatCompletionMessage, ) from openai.types.chat.chat_completion import Choice @@ -30,7 +29,10 @@ from openai.types.shared_params import FunctionDefinition try: - from openai.types.chat import ChatCompletionCustomToolParam + from openai.types.chat import ( + ChatCompletionCustomToolParam, + ChatCompletionFunctionToolParam, + ) except ImportError: pass From a9e263c2f142a3f0bec6f530be0869c5421c0673 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 13:21:55 +0200 Subject: [PATCH 10/22] more import problems --- tests/integrations/openai/test_openai.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index aad6113d7e..2202d7ed42 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -24,15 +24,15 @@ from openai.types.chat.chat_completion import Choice from openai.types.chat.chat_completion_chunk import Choice as DeltaChoice from openai.types.chat.chat_completion_chunk import ChoiceDelta -from openai.types.chat.chat_completion_custom_tool_param import Custom from openai.types.create_embedding_response import Usage as EmbeddingTokenUsage -from openai.types.shared_params import FunctionDefinition try: from openai.types.chat import ( ChatCompletionCustomToolParam, ChatCompletionFunctionToolParam, ) + from openai.types.chat.chat_completion_custom_tool_param import Custom + from openai.types.shared_params import FunctionDefinition except ImportError: pass From 2ceb5ef45af9ccaf52c8fdf13875db0cb18326f0 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 13:23:10 +0200 Subject: [PATCH 11/22] restore import changes --- tests/integrations/openai/test_openai.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 2202d7ed42..53f993b5c0 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -17,10 +17,7 @@ from openai import AsyncOpenAI, AsyncStream, OpenAI, OpenAIError, Stream from openai.types import CompletionUsage, CreateEmbeddingResponse, Embedding -from openai.types.chat import ( - ChatCompletionChunk, - ChatCompletionMessage, -) +from openai.types.chat import ChatCompletionChunk, ChatCompletionMessage from openai.types.chat.chat_completion import Choice from openai.types.chat.chat_completion_chunk import Choice as DeltaChoice from openai.types.chat.chat_completion_chunk import ChoiceDelta From 7c30a052ac225236a1b2441fecbd5571ae4bbee2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 13:37:08 +0200 Subject: [PATCH 12/22] ref(openai-agents): Stop setting gen_ai.request.available_tools on Execute Tool spans --- .../openai_agents/spans/ai_client.py | 12 ++++++++++++ .../openai_agents/spans/invoke_agent.py | 10 ++++++++++ sentry_sdk/integrations/openai_agents/utils.py | 6 ------ .../openai_agents/test_openai_agents.py | 17 ----------------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/sentry_sdk/integrations/openai_agents/spans/ai_client.py index f4f02cb674..4edec05770 100644 --- a/sentry_sdk/integrations/openai_agents/spans/ai_client.py +++ b/sentry_sdk/integrations/openai_agents/spans/ai_client.py @@ -4,6 +4,7 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import safe_serialize from ..consts import SPAN_ORIGIN from ..utils import ( @@ -40,6 +41,8 @@ def ai_client_span( SPANDATA.GEN_AI_OPERATION_NAME: "chat", }, ) + + set_on_span = span.set_attribute else: span = sentry_sdk.start_span( op=OP.GEN_AI_CHAT, @@ -49,7 +52,16 @@ def ai_client_span( # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + set_on_span = span.set_data + _set_agent_data(span, agent) + + if len(agent.tools) > 0: + set_on_span( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + safe_serialize([vars(tool) for tool in agent.tools]), + ) + _set_input_data(span, get_response_kwargs) return span diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index c21145ac4a..908910e177 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -38,6 +38,8 @@ def invoke_agent_span( SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", }, ) + + set_on_span = span.set_attribute else: start_span_function = get_start_span_function() span = start_span_function( @@ -49,6 +51,8 @@ def invoke_agent_span( span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + set_on_span = span.set_data + if should_send_default_pii(): messages = [] if agent.instructions: @@ -97,6 +101,12 @@ def invoke_agent_span( _set_agent_data(span, agent) + if len(agent.tools) > 0: + set_on_span( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + safe_serialize([vars(tool) for tool in agent.tools]), + ) + return span diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 224a5f66ba..08cc076510 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -90,12 +90,6 @@ def _set_agent_data( agent.model_settings.frequency_penalty, ) - if len(agent.tools) > 0: - set_on_span( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - safe_serialize([vars(tool) for tool in agent.tools]), - ) - def _set_usage_data( span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", usage: "Usage" diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 1b03989b6a..69712ae4c6 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -2073,12 +2073,6 @@ def simple_test_tool(message: str) -> str: assert tool_span["attributes"]["gen_ai.agent.name"] == "test_agent" assert tool_span["attributes"]["gen_ai.operation.name"] == "execute_tool" - tool_span_available_tool = json.loads( - tool_span["attributes"]["gen_ai.request.available_tools"] - )[0] - - assert all(tool_span_available_tool[k] == v for k, v in available_tool.items()) - assert tool_span["attributes"]["gen_ai.request.max_tokens"] == 100 assert tool_span["attributes"]["gen_ai.request.model"] == "gpt-4" assert tool_span["attributes"]["gen_ai.request.temperature"] == 0.7 @@ -2305,12 +2299,6 @@ def simple_test_tool(message: str) -> str: assert tool_span["attributes"]["gen_ai.agent.name"] == "test_agent" assert tool_span["attributes"]["gen_ai.operation.name"] == "execute_tool" - tool_span_available_tool = json.loads( - tool_span["attributes"]["gen_ai.request.available_tools"] - )[0] - - assert all(tool_span_available_tool[k] == v for k, v in available_tool.items()) - assert tool_span["attributes"]["gen_ai.request.max_tokens"] == 100 assert tool_span["attributes"]["gen_ai.request.model"] == "gpt-4" assert tool_span["attributes"]["gen_ai.request.temperature"] == 0.7 @@ -2525,11 +2513,6 @@ def simple_test_tool(message: str) -> str: assert tool_span["data"]["gen_ai.agent.name"] == "test_agent" assert tool_span["data"]["gen_ai.operation.name"] == "execute_tool" - tool_span_available_tool = json.loads( - tool_span["data"]["gen_ai.request.available_tools"] - )[0] - assert all(tool_span_available_tool[k] == v for k, v in available_tool.items()) - assert tool_span["data"]["gen_ai.request.max_tokens"] == 100 assert tool_span["data"]["gen_ai.request.model"] == "gpt-4" assert tool_span["data"]["gen_ai.request.temperature"] == 0.7 From df36922d469bbbd48fbb79f2fd74e068bf8149e4 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 13:40:44 +0200 Subject: [PATCH 13/22] ref(openai-agents): Stop setting gen_ai.request.available_tools on Invoke Agent spans --- .../openai_agents/spans/invoke_agent.py | 10 ---------- .../openai_agents/test_openai_agents.py | 17 ----------------- 2 files changed, 27 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index 908910e177..c21145ac4a 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -38,8 +38,6 @@ def invoke_agent_span( SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", }, ) - - set_on_span = span.set_attribute else: start_span_function = get_start_span_function() span = start_span_function( @@ -51,8 +49,6 @@ def invoke_agent_span( span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - set_on_span = span.set_data - if should_send_default_pii(): messages = [] if agent.instructions: @@ -101,12 +97,6 @@ def invoke_agent_span( _set_agent_data(span, agent) - if len(agent.tools) > 0: - set_on_span( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - safe_serialize([vars(tool) for tool in agent.tools]), - ) - return span diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 69712ae4c6..60add19f8c 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -2004,12 +2004,6 @@ def simple_test_tool(message: str) -> str: assert agent_span["attributes"]["gen_ai.agent.name"] == "test_agent" assert agent_span["attributes"]["gen_ai.operation.name"] == "invoke_agent" - agent_span_available_tool = json.loads( - agent_span["attributes"]["gen_ai.request.available_tools"] - )[0] - - assert all(agent_span_available_tool[k] == v for k, v in available_tool.items()) - assert agent_span["attributes"]["gen_ai.request.max_tokens"] == 100 assert agent_span["attributes"]["gen_ai.request.model"] == "gpt-4" assert agent_span["attributes"]["gen_ai.request.temperature"] == 0.7 @@ -2230,12 +2224,6 @@ def simple_test_tool(message: str) -> str: assert agent_span["attributes"]["gen_ai.agent.name"] == "test_agent" assert agent_span["attributes"]["gen_ai.operation.name"] == "invoke_agent" - agent_span_available_tool = json.loads( - agent_span["attributes"]["gen_ai.request.available_tools"] - )[0] - - assert all(agent_span_available_tool[k] == v for k, v in available_tool.items()) - assert agent_span["attributes"]["gen_ai.request.max_tokens"] == 100 assert agent_span["attributes"]["gen_ai.request.model"] == "gpt-4" assert agent_span["attributes"]["gen_ai.request.temperature"] == 0.7 @@ -2450,11 +2438,6 @@ def simple_test_tool(message: str) -> str: assert agent_span["data"]["gen_ai.agent.name"] == "test_agent" assert agent_span["data"]["gen_ai.operation.name"] == "invoke_agent" - agent_span_available_tool = json.loads( - agent_span["data"]["gen_ai.request.available_tools"] - )[0] - assert all(agent_span_available_tool[k] == v for k, v in available_tool.items()) - assert agent_span["data"]["gen_ai.request.max_tokens"] == 100 assert agent_span["data"]["gen_ai.request.model"] == "gpt-4" assert agent_span["data"]["gen_ai.request.temperature"] == 0.7 From 74b082312dc5ebdab83344f08a66ac3f3dd2cf25 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 13:53:14 +0200 Subject: [PATCH 14/22] type safety --- sentry_sdk/ai/_openai_responses_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/ai/_openai_responses_api.py b/sentry_sdk/ai/_openai_responses_api.py index 15841b0ca4..81d402c045 100644 --- a/sentry_sdk/ai/_openai_responses_api.py +++ b/sentry_sdk/ai/_openai_responses_api.py @@ -49,10 +49,10 @@ def _transform_tool_definitions(tools: "Iterable[ToolParam]") -> "list[ToolDefin if "name" in tool: tool_definition["name"] = tool["name"] - if "description" in tool: + if "description" in tool and tool["description"] is not None: tool_definition["description"] = tool["description"] - if "parameters" in tool: + if "parameters" in tool and tool["parameters"] is not None: tool_definition["parameters"] = tool["parameters"] tool_definitions.append(tool_definition) From 18a45a25bc0948dc138961ec6ccee394b1412dea Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 30 Jul 2026 16:23:01 +0200 Subject: [PATCH 15/22] fix(openai-agents): Stop destructively wrapping tools --- .../integrations/openai_agents/patches/tools.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/tools.py b/sentry_sdk/integrations/openai_agents/patches/tools.py index bd13b9d61a..ab49df8b9e 100644 --- a/sentry_sdk/integrations/openai_agents/patches/tools.py +++ b/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -57,14 +57,7 @@ async def sentry_wrapped_on_invoke_tool( return sentry_wrapped_on_invoke_tool - wrapped_tool = agents.FunctionTool( - name=tool.name, - description=tool.description, - params_json_schema=tool.params_json_schema, - on_invoke_tool=create_wrapped_invoke(tool, original_on_invoke), - strict_json_schema=tool.strict_json_schema, - is_enabled=tool.is_enabled, - ) - wrapped_tools.append(wrapped_tool) + tool.on_invoke_tool = create_wrapped_invoke(tool, original_on_invoke) + wrapped_tools.append(tool) return wrapped_tools From 89f78bf8d77114307d1f7e5c1badc9b43ecc45d1 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 31 Jul 2026 09:57:26 +0200 Subject: [PATCH 16/22] add quotes around type annotation --- sentry_sdk/ai/_openai_responses_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/ai/_openai_responses_api.py b/sentry_sdk/ai/_openai_responses_api.py index 81d402c045..f2b685e864 100644 --- a/sentry_sdk/ai/_openai_responses_api.py +++ b/sentry_sdk/ai/_openai_responses_api.py @@ -42,7 +42,7 @@ def _transform_tool_definitions(tools: "Iterable[ToolParam]") -> "list[ToolDefin continue if tool["type"] == "function": - tool_definition: ToolDefinition = { + tool_definition: "ToolDefinition" = { "type": "function", } From 4efb6fb40a56f0c6d06a06e702e406432fc7fcfe Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 31 Jul 2026 10:40:08 +0200 Subject: [PATCH 17/22] try out RunHooks --- .../integrations/openai_agents/__init__.py | 26 -------- .../openai_agents/patches/__init__.py | 1 - .../openai_agents/patches/runner.py | 63 ++++++++++++++++++- .../openai_agents/patches/tools.py | 63 ------------------- .../openai_agents/spans/execute_tool.py | 10 +-- 5 files changed, 63 insertions(+), 100 deletions(-) delete mode 100644 sentry_sdk/integrations/openai_agents/patches/tools.py diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index 5895f53ad3..bc53f9115e 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -8,7 +8,6 @@ _create_run_wrapper, _execute_final_output, _execute_handoffs, - _get_all_tools, _get_model, _patch_error_tracing, _run_single_turn, @@ -64,7 +63,6 @@ class OpenAIAgentsIntegration(Integration): """ NOTE: With version 0.8.0, the class methods below have been refactored to functions. - `AgentRunner._get_model()` -> `agents.run_internal.turn_preparation.get_model()` - - `AgentRunner._get_all_tools()` -> `agents.run_internal.turn_preparation.get_all_tools()` - `AgentRunner._run_single_turn()` -> `agents.run_internal.run_loop.run_single_turn()` - `RunImpl.execute_handoffs()` -> `agents.run_internal.turn_resolution.execute_handoffs()` - `RunImpl.execute_final_output()` -> `agents.run_internal.turn_resolution.execute_final_output()` @@ -76,7 +74,6 @@ class OpenAIAgentsIntegration(Integration): - `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()` are patched in `_patch_runner()` with `_create_run_wrapper()` and `_create_run_streamed_wrapper()`, respectively. 3. In a loop, the agent repeatedly calls the Responses API, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. - A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`. - - Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`. - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`. 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`. @@ -101,17 +98,6 @@ def setup_once() -> None: ): if run_loop is not None: - @wraps(run_loop.get_all_tools) - async def new_wrapped_get_all_tools( - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools( - run_loop.get_all_tools, agent, context_wrapper - ) - - agents.run.get_all_tools = new_wrapped_get_all_tools - @wraps(run_loop.run_single_turn) async def new_wrapped_run_single_turn( *args: "Any", **kwargs: "Any" @@ -175,18 +161,6 @@ async def new_wrapped_final_output( return - original_get_all_tools = AgentRunner._get_all_tools - - @wraps(AgentRunner._get_all_tools.__func__) - async def old_wrapped_get_all_tools( - cls: "agents.Runner", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools(original_get_all_tools, agent, context_wrapper) - - agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) - original_get_model = AgentRunner._get_model @wraps(AgentRunner._get_model.__func__) diff --git a/sentry_sdk/integrations/openai_agents/patches/__init__.py b/sentry_sdk/integrations/openai_agents/patches/__init__.py index 85d48f2d41..11d6a13978 100644 --- a/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ b/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -7,4 +7,3 @@ from .error_tracing import _patch_error_tracing # noqa: F401 from .models import _get_model # noqa: F401 from .runner import _create_run_streamed_wrapper, _create_run_wrapper # noqa: F401 -from .tools import _get_all_tools # noqa: F401 diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index 5f9996595f..f3c3ce1c50 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -1,13 +1,21 @@ import sys from functools import wraps +from agents import FunctionTool, RunHooks + import sentry_sdk from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii from sentry_sdk.traces import StreamedSpan from sentry_sdk.utils import capture_internal_exceptions, reraise -from ..spans import agent_workflow_span, update_invoke_agent_span +from ..spans import ( + agent_workflow_span, + execute_tool_span, + update_execute_tool_span, + update_invoke_agent_span, +) from ..utils import _capture_exception try: @@ -21,6 +29,39 @@ from typing import Any, AsyncIterator, Callable +class _SentryRunHooks(RunHooks): + """ + Creates and mangages tool execution spans. + + The start hook creates a span and sets input attributes. + The end hook finishes the span and sets remaining attributes. + """ + + async def on_tool_start(self, context, agent, tool): + if not isinstance(tool, FunctionTool): + return + + span = execute_tool_span(tool) + + if isinstance(span, StreamedSpan) and should_send_default_pii(): + span.set_attribute(SPANDATA.GEN_AI_TOOL_INPUT, context.tool_arguments) + elif should_send_default_pii(): + span.set_data(SPANDATA.GEN_AI_TOOL_INPUT, context.tool_arguments) + + span.__enter__() + context._sentry_span = span + + async def on_tool_end(self, context, agent, tool, result): + if not isinstance(tool, FunctionTool): + return + + span = getattr(context, "_sentry_span", None) + if span is not None: + del context._sentry_span + update_execute_tool_span(span, agent, tool, result) + span.__exit__(None, None, None) + + def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": """ Wraps the agents.Runner.run methods to @@ -33,6 +74,26 @@ def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., A @wraps(original_func) async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + hooks = kwargs.get("hooks") + if hooks is None: + kwargs["hooks"] = _SentryRunHooks() + elif isinstance(hooks, RunHooks): + original_on_tool_start = hooks.on_tool_start + original_on_tool_end = hooks.on_tool_end + + @wraps(original_on_tool_start) + async def sentry_on_tool_start(context, agent, tool): + await original_on_tool_start(context, agent, tool) + await _SentryRunHooks.on_tool_start(None, context, agent, tool) + + @wraps(original_on_tool_end) + async def sentry_on_tool_end(context, agent, tool, result): + await original_on_tool_end(context, agent, tool, result) + await _SentryRunHooks.on_tool_end(None, context, agent, tool, result) + + hooks.on_tool_start = sentry_on_tool_start + hooks.on_tool_end = sentry_on_tool_end + # Isolate each workflow so that when agents are run in asyncio tasks they # don't touch each other's scopes with sentry_sdk.isolation_scope(): diff --git a/sentry_sdk/integrations/openai_agents/patches/tools.py b/sentry_sdk/integrations/openai_agents/patches/tools.py deleted file mode 100644 index ab49df8b9e..0000000000 --- a/sentry_sdk/integrations/openai_agents/patches/tools.py +++ /dev/null @@ -1,63 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import execute_tool_span, update_execute_tool_span - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -async def _get_all_tools( - original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", -) -> "list[agents.Tool]": - """ - Responsible for creating and managing `gen_ai.execute_tool` spans. - """ - # Get the original tools - tools = await original_get_all_tools(agent, context_wrapper) - - wrapped_tools = [] - for tool in tools: - # Wrap only the function tools (for now) - if tool.__class__.__name__ != "FunctionTool": - wrapped_tools.append(tool) - continue - - # Create a new FunctionTool with our wrapped invoke method - original_on_invoke = tool.on_invoke_tool - - def create_wrapped_invoke( - current_tool: "agents.Tool", current_on_invoke: "Callable[..., Any]" - ) -> "Callable[..., Any]": - @wraps(current_on_invoke) - async def sentry_wrapped_on_invoke_tool( - *args: "Any", **kwargs: "Any" - ) -> "Any": - with execute_tool_span(current_tool, *args, **kwargs) as span: - # We can not capture exceptions in tool execution here because - # `_on_invoke_tool` is swallowing the exception here: - # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 - # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter - # I was unable to monkey patch it because those are evaluated at module import time - # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` - # because it is nested inside this import time code. As if they made it hard to patch on purpose... - result = await current_on_invoke(*args, **kwargs) - update_execute_tool_span(span, agent, current_tool, result) - - return result - - return sentry_wrapped_on_invoke_tool - - tool.on_invoke_tool = create_wrapped_invoke(tool, original_on_invoke) - wrapped_tools.append(tool) - - return wrapped_tools diff --git a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py index fd3a430951..4ae9e3ef6c 100644 --- a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -16,7 +16,7 @@ def execute_tool_span( - tool: "agents.Tool", *args: "Any", **kwargs: "Any" + tool: "agents.Tool", ) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: @@ -30,8 +30,6 @@ def execute_tool_span( SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, }, ) - - set_on_span = span.set_attribute else: span = sentry_sdk.start_span( op=OP.GEN_AI_EXECUTE_TOOL, @@ -44,12 +42,6 @@ def execute_tool_span( span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) - set_on_span = span.set_data - - if should_send_default_pii(): - input = args[1] - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) - return span From 4eec0bb7dbe07d8d0398f301c1702251c7a317cf Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 31 Jul 2026 10:44:13 +0200 Subject: [PATCH 18/22] Revert "try out RunHooks" This reverts commit 4efb6fb40a56f0c6d06a06e702e406432fc7fcfe. --- .../integrations/openai_agents/__init__.py | 26 ++++++++ .../openai_agents/patches/__init__.py | 1 + .../openai_agents/patches/runner.py | 63 +------------------ .../openai_agents/patches/tools.py | 63 +++++++++++++++++++ .../openai_agents/spans/execute_tool.py | 10 ++- 5 files changed, 100 insertions(+), 63 deletions(-) create mode 100644 sentry_sdk/integrations/openai_agents/patches/tools.py diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index bc53f9115e..5895f53ad3 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -8,6 +8,7 @@ _create_run_wrapper, _execute_final_output, _execute_handoffs, + _get_all_tools, _get_model, _patch_error_tracing, _run_single_turn, @@ -63,6 +64,7 @@ class OpenAIAgentsIntegration(Integration): """ NOTE: With version 0.8.0, the class methods below have been refactored to functions. - `AgentRunner._get_model()` -> `agents.run_internal.turn_preparation.get_model()` + - `AgentRunner._get_all_tools()` -> `agents.run_internal.turn_preparation.get_all_tools()` - `AgentRunner._run_single_turn()` -> `agents.run_internal.run_loop.run_single_turn()` - `RunImpl.execute_handoffs()` -> `agents.run_internal.turn_resolution.execute_handoffs()` - `RunImpl.execute_final_output()` -> `agents.run_internal.turn_resolution.execute_final_output()` @@ -74,6 +76,7 @@ class OpenAIAgentsIntegration(Integration): - `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()` are patched in `_patch_runner()` with `_create_run_wrapper()` and `_create_run_streamed_wrapper()`, respectively. 3. In a loop, the agent repeatedly calls the Responses API, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. - A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`. + - Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`. - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`. 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`. @@ -98,6 +101,17 @@ def setup_once() -> None: ): if run_loop is not None: + @wraps(run_loop.get_all_tools) + async def new_wrapped_get_all_tools( + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools( + run_loop.get_all_tools, agent, context_wrapper + ) + + agents.run.get_all_tools = new_wrapped_get_all_tools + @wraps(run_loop.run_single_turn) async def new_wrapped_run_single_turn( *args: "Any", **kwargs: "Any" @@ -161,6 +175,18 @@ async def new_wrapped_final_output( return + original_get_all_tools = AgentRunner._get_all_tools + + @wraps(AgentRunner._get_all_tools.__func__) + async def old_wrapped_get_all_tools( + cls: "agents.Runner", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools(original_get_all_tools, agent, context_wrapper) + + agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) + original_get_model = AgentRunner._get_model @wraps(AgentRunner._get_model.__func__) diff --git a/sentry_sdk/integrations/openai_agents/patches/__init__.py b/sentry_sdk/integrations/openai_agents/patches/__init__.py index 11d6a13978..85d48f2d41 100644 --- a/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ b/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -7,3 +7,4 @@ from .error_tracing import _patch_error_tracing # noqa: F401 from .models import _get_model # noqa: F401 from .runner import _create_run_streamed_wrapper, _create_run_wrapper # noqa: F401 +from .tools import _get_all_tools # noqa: F401 diff --git a/sentry_sdk/integrations/openai_agents/patches/runner.py b/sentry_sdk/integrations/openai_agents/patches/runner.py index f3c3ce1c50..5f9996595f 100644 --- a/sentry_sdk/integrations/openai_agents/patches/runner.py +++ b/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -1,21 +1,13 @@ import sys from functools import wraps -from agents import FunctionTool, RunHooks - import sentry_sdk from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import should_send_default_pii from sentry_sdk.traces import StreamedSpan from sentry_sdk.utils import capture_internal_exceptions, reraise -from ..spans import ( - agent_workflow_span, - execute_tool_span, - update_execute_tool_span, - update_invoke_agent_span, -) +from ..spans import agent_workflow_span, update_invoke_agent_span from ..utils import _capture_exception try: @@ -29,39 +21,6 @@ from typing import Any, AsyncIterator, Callable -class _SentryRunHooks(RunHooks): - """ - Creates and mangages tool execution spans. - - The start hook creates a span and sets input attributes. - The end hook finishes the span and sets remaining attributes. - """ - - async def on_tool_start(self, context, agent, tool): - if not isinstance(tool, FunctionTool): - return - - span = execute_tool_span(tool) - - if isinstance(span, StreamedSpan) and should_send_default_pii(): - span.set_attribute(SPANDATA.GEN_AI_TOOL_INPUT, context.tool_arguments) - elif should_send_default_pii(): - span.set_data(SPANDATA.GEN_AI_TOOL_INPUT, context.tool_arguments) - - span.__enter__() - context._sentry_span = span - - async def on_tool_end(self, context, agent, tool, result): - if not isinstance(tool, FunctionTool): - return - - span = getattr(context, "_sentry_span", None) - if span is not None: - del context._sentry_span - update_execute_tool_span(span, agent, tool, result) - span.__exit__(None, None, None) - - def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": """ Wraps the agents.Runner.run methods to @@ -74,26 +33,6 @@ def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., A @wraps(original_func) async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - hooks = kwargs.get("hooks") - if hooks is None: - kwargs["hooks"] = _SentryRunHooks() - elif isinstance(hooks, RunHooks): - original_on_tool_start = hooks.on_tool_start - original_on_tool_end = hooks.on_tool_end - - @wraps(original_on_tool_start) - async def sentry_on_tool_start(context, agent, tool): - await original_on_tool_start(context, agent, tool) - await _SentryRunHooks.on_tool_start(None, context, agent, tool) - - @wraps(original_on_tool_end) - async def sentry_on_tool_end(context, agent, tool, result): - await original_on_tool_end(context, agent, tool, result) - await _SentryRunHooks.on_tool_end(None, context, agent, tool, result) - - hooks.on_tool_start = sentry_on_tool_start - hooks.on_tool_end = sentry_on_tool_end - # Isolate each workflow so that when agents are run in asyncio tasks they # don't touch each other's scopes with sentry_sdk.isolation_scope(): diff --git a/sentry_sdk/integrations/openai_agents/patches/tools.py b/sentry_sdk/integrations/openai_agents/patches/tools.py new file mode 100644 index 0000000000..ab49df8b9e --- /dev/null +++ b/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -0,0 +1,63 @@ +from functools import wraps +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable + +from ..spans import execute_tool_span, update_execute_tool_span + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable + +try: + import agents +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +async def _get_all_tools( + original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", +) -> "list[agents.Tool]": + """ + Responsible for creating and managing `gen_ai.execute_tool` spans. + """ + # Get the original tools + tools = await original_get_all_tools(agent, context_wrapper) + + wrapped_tools = [] + for tool in tools: + # Wrap only the function tools (for now) + if tool.__class__.__name__ != "FunctionTool": + wrapped_tools.append(tool) + continue + + # Create a new FunctionTool with our wrapped invoke method + original_on_invoke = tool.on_invoke_tool + + def create_wrapped_invoke( + current_tool: "agents.Tool", current_on_invoke: "Callable[..., Any]" + ) -> "Callable[..., Any]": + @wraps(current_on_invoke) + async def sentry_wrapped_on_invoke_tool( + *args: "Any", **kwargs: "Any" + ) -> "Any": + with execute_tool_span(current_tool, *args, **kwargs) as span: + # We can not capture exceptions in tool execution here because + # `_on_invoke_tool` is swallowing the exception here: + # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 + # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter + # I was unable to monkey patch it because those are evaluated at module import time + # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` + # because it is nested inside this import time code. As if they made it hard to patch on purpose... + result = await current_on_invoke(*args, **kwargs) + update_execute_tool_span(span, agent, current_tool, result) + + return result + + return sentry_wrapped_on_invoke_tool + + tool.on_invoke_tool = create_wrapped_invoke(tool, original_on_invoke) + wrapped_tools.append(tool) + + return wrapped_tools diff --git a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py index 4ae9e3ef6c..fd3a430951 100644 --- a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -16,7 +16,7 @@ def execute_tool_span( - tool: "agents.Tool", + tool: "agents.Tool", *args: "Any", **kwargs: "Any" ) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: @@ -30,6 +30,8 @@ def execute_tool_span( SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, }, ) + + set_on_span = span.set_attribute else: span = sentry_sdk.start_span( op=OP.GEN_AI_EXECUTE_TOOL, @@ -42,6 +44,12 @@ def execute_tool_span( span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) + set_on_span = span.set_data + + if should_send_default_pii(): + input = args[1] + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) + return span From 1a2a2322967d5e454c4c836835ea296cc82f72a3 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 31 Jul 2026 10:49:29 +0200 Subject: [PATCH 19/22] make idempotent --- sentry_sdk/integrations/openai_agents/patches/tools.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/tools.py b/sentry_sdk/integrations/openai_agents/patches/tools.py index ab49df8b9e..06fc64c763 100644 --- a/sentry_sdk/integrations/openai_agents/patches/tools.py +++ b/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -28,7 +28,9 @@ async def _get_all_tools( wrapped_tools = [] for tool in tools: # Wrap only the function tools (for now) - if tool.__class__.__name__ != "FunctionTool": + if tool.__class__.__name__ != "FunctionTool" or getattr( + tool.on_invoke_tool, "_sentry_wrapped", False + ): wrapped_tools.append(tool) continue @@ -57,6 +59,7 @@ async def sentry_wrapped_on_invoke_tool( return sentry_wrapped_on_invoke_tool + tool.on_invoke_tool._sentry_wrapped = True tool.on_invoke_tool = create_wrapped_invoke(tool, original_on_invoke) wrapped_tools.append(tool) From 23696f302b299c257c471d2166e244f00aab8a0d Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 31 Jul 2026 10:59:32 +0200 Subject: [PATCH 20/22] gate custom access --- sentry_sdk/ai/_openai_completions_api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry_sdk/ai/_openai_completions_api.py b/sentry_sdk/ai/_openai_completions_api.py index a61a3e7814..213129b714 100644 --- a/sentry_sdk/ai/_openai_completions_api.py +++ b/sentry_sdk/ai/_openai_completions_api.py @@ -107,6 +107,10 @@ def _transform_tool_definitions( "type": "custom", } + if "custom" not in tool: + tool_definitions.append(tool_definition) + continue + if "name" in tool["custom"]: tool_definition["name"] = tool["custom"]["name"] From 08f26fef87f9c9b572245b5ef6ecdd7c14de918b Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 31 Jul 2026 14:18:06 +0200 Subject: [PATCH 21/22] remove attribute --- .../integrations/openai_agents/spans/invoke_agent.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index 908910e177..c21145ac4a 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -38,8 +38,6 @@ def invoke_agent_span( SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", }, ) - - set_on_span = span.set_attribute else: start_span_function = get_start_span_function() span = start_span_function( @@ -51,8 +49,6 @@ def invoke_agent_span( span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - set_on_span = span.set_data - if should_send_default_pii(): messages = [] if agent.instructions: @@ -101,12 +97,6 @@ def invoke_agent_span( _set_agent_data(span, agent) - if len(agent.tools) > 0: - set_on_span( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - safe_serialize([vars(tool) for tool in agent.tools]), - ) - return span From 7f5cc967ad635c5d3f937557b7f69c3e2a53e55f Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 31 Jul 2026 14:35:48 +0200 Subject: [PATCH 22/22] no idempotency guard --- sentry_sdk/integrations/openai_agents/patches/tools.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/tools.py b/sentry_sdk/integrations/openai_agents/patches/tools.py index 06fc64c763..ab49df8b9e 100644 --- a/sentry_sdk/integrations/openai_agents/patches/tools.py +++ b/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -28,9 +28,7 @@ async def _get_all_tools( wrapped_tools = [] for tool in tools: # Wrap only the function tools (for now) - if tool.__class__.__name__ != "FunctionTool" or getattr( - tool.on_invoke_tool, "_sentry_wrapped", False - ): + if tool.__class__.__name__ != "FunctionTool": wrapped_tools.append(tool) continue @@ -59,7 +57,6 @@ async def sentry_wrapped_on_invoke_tool( return sentry_wrapped_on_invoke_tool - tool.on_invoke_tool._sentry_wrapped = True tool.on_invoke_tool = create_wrapped_invoke(tool, original_on_invoke) wrapped_tools.append(tool)