From 02af5df52ce3230041e188f0bd52460937bbe3a0 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Feb 2026 11:29:06 +0100 Subject: [PATCH 1/9] fix(openai-agents): Patch tool functions following library refactor --- .../integrations/openai_agents/__init__.py | 34 ++++- .../openai_agents/patches/__init__.py | 5 +- .../openai_agents/patches/tools.py | 123 ++++++++++-------- 3 files changed, 104 insertions(+), 58 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index deb136de01..b93d835dc7 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -1,8 +1,10 @@ from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import parse_version from .patches import ( _create_get_model_wrapper, - _create_get_all_tools_wrapper, + _create_runner_get_all_tools_wrapper, + _create_run_loop_get_all_tools_wrapper, _create_run_wrapper, _create_run_streamed_wrapper, _patch_agent_run, @@ -17,11 +19,21 @@ # after it, even if we don't use it. import agents from agents.run import DEFAULT_AGENT_RUNNER + from agents.version import __version__ as OPENAI_AGENTS_VERSION except ImportError: raise DidNotEnable("OpenAI Agents not installed") +try: + # AgentRunner methods moved in v0.8 + # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 + from agents.run_internal import run_loop, turn_preparation +except ImportError: + run_loop = None + turn_preparation = None + + def _patch_runner() -> None: # Create the root span for one full agent run (including eventual handoffs) # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around @@ -45,9 +57,15 @@ def _patch_model() -> None: ) -def _patch_tools() -> None: +def _patch_agent_runner_get_all_tools() -> None: agents.run.AgentRunner._get_all_tools = classmethod( - _create_get_all_tools_wrapper(agents.run.AgentRunner._get_all_tools), + _create_runner_get_all_tools_wrapper(agents.run.AgentRunner._get_all_tools), + ) + + +def _patch_run_get_all_tools() -> None: + agents.run.get_all_tools = _create_run_loop_get_all_tools_wrapper( + run_loop.get_all_tools ) @@ -57,6 +75,14 @@ class OpenAIAgentsIntegration(Integration): @staticmethod def setup_once() -> None: _patch_error_tracing() - _patch_tools() _patch_model() _patch_runner() + + library_version = parse_version(OPENAI_AGENTS_VERSION) + if library_version is not None and library_version >= ( + 0, + 8, + ): + _patch_run_get_all_tools() + + _patch_agent_runner_get_all_tools() diff --git a/sentry_sdk/integrations/openai_agents/patches/__init__.py b/sentry_sdk/integrations/openai_agents/patches/__init__.py index b53ca79e19..675f8c4fc4 100644 --- a/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ b/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -1,5 +1,8 @@ from .models import _create_get_model_wrapper # noqa: F401 -from .tools import _create_get_all_tools_wrapper # noqa: F401 +from .tools import ( + _create_runner_get_all_tools_wrapper, + _create_run_loop_get_all_tools_wrapper, +) # noqa: F401 from .runner import _create_run_wrapper, _create_run_streamed_wrapper # noqa: F401 from .agent_run import _patch_agent_run # noqa: F401 from .error_tracing import _patch_error_tracing # noqa: F401 diff --git a/sentry_sdk/integrations/openai_agents/patches/tools.py b/sentry_sdk/integrations/openai_agents/patches/tools.py index d14a3019aa..bb72949139 100644 --- a/sentry_sdk/integrations/openai_agents/patches/tools.py +++ b/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -1,4 +1,4 @@ -from functools import wraps +from functools import wraps, partial from sentry_sdk.integrations import DidNotEnable @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Callable + from typing import Any, Callable, Awaitable try: import agents @@ -15,13 +15,62 @@ raise DidNotEnable("OpenAI Agents not installed") -def _create_get_all_tools_wrapper( +async def _get_all_tools( + original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", +) -> "list[agents.Tool]": + # 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 + + 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) + + return wrapped_tools + + +def _create_runner_get_all_tools_wrapper( original_get_all_tools: "Callable[..., Any]", ) -> "Callable[..., Any]": - """ - Wraps the agents.Runner._get_all_tools method of the Runner class to wrap all function tools with Sentry instrumentation. - """ - @wraps( original_get_all_tools.__func__ if hasattr(original_get_all_tools, "__func__") @@ -32,51 +81,19 @@ async def wrapped_get_all_tools( agent: "agents.Agent", context_wrapper: "agents.RunContextWrapper", ) -> "list[agents.Tool]": - # 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 - - 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) - - return wrapped_tools + return await _get_all_tools(original_get_all_tools, agent, context_wrapper) + + return wrapped_get_all_tools + + +def _create_run_loop_get_all_tools_wrapper( + original_get_all_tools: "Callable[..., Any]", +) -> "Callable[..., Any]": + @wraps(original_get_all_tools) + async def wrapped_get_all_tools( + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools(original_get_all_tools, agent, context_wrapper) return wrapped_get_all_tools From a7f08cc705fec4a69efdf24eb9836ebcad231156 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Feb 2026 11:30:35 +0100 Subject: [PATCH 2/9] . --- sentry_sdk/integrations/openai_agents/patches/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/patches/tools.py b/sentry_sdk/integrations/openai_agents/patches/tools.py index bb72949139..561d9449e2 100644 --- a/sentry_sdk/integrations/openai_agents/patches/tools.py +++ b/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -1,4 +1,4 @@ -from functools import wraps, partial +from functools import wraps from sentry_sdk.integrations import DidNotEnable From 3679c96648e0effdd8047b12423a80d52703f07c Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Feb 2026 11:31:57 +0100 Subject: [PATCH 3/9] add early return --- sentry_sdk/integrations/openai_agents/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index b93d835dc7..4395e54eb7 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -84,5 +84,6 @@ def setup_once() -> None: 8, ): _patch_run_get_all_tools() + return _patch_agent_runner_get_all_tools() From 0047eb3c932138fd3ee972fb2b274c55e86e3e3d Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Feb 2026 13:37:18 +0100 Subject: [PATCH 4/9] remove indirection --- .../integrations/openai_agents/__init__.py | 45 +++++++++++-------- .../openai_agents/patches/__init__.py | 5 +-- .../openai_agents/patches/tools.py | 31 ------------- 3 files changed, 28 insertions(+), 53 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index 4395e54eb7..93e43f8bb3 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -1,10 +1,11 @@ from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.utils import parse_version +from functools import wraps + from .patches import ( _create_get_model_wrapper, - _create_runner_get_all_tools_wrapper, - _create_run_loop_get_all_tools_wrapper, + _get_all_tools, _create_run_wrapper, _create_run_streamed_wrapper, _patch_agent_run, @@ -19,6 +20,7 @@ # after it, even if we don't use it. import agents from agents.run import DEFAULT_AGENT_RUNNER + from agents.run import AgentRunner from agents.version import __version__ as OPENAI_AGENTS_VERSION except ImportError: @@ -28,10 +30,9 @@ try: # AgentRunner methods moved in v0.8 # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 - from agents.run_internal import run_loop, turn_preparation + from agents.run_internal import run_loop except ImportError: run_loop = None - turn_preparation = None def _patch_runner() -> None: @@ -57,18 +58,6 @@ def _patch_model() -> None: ) -def _patch_agent_runner_get_all_tools() -> None: - agents.run.AgentRunner._get_all_tools = classmethod( - _create_runner_get_all_tools_wrapper(agents.run.AgentRunner._get_all_tools), - ) - - -def _patch_run_get_all_tools() -> None: - agents.run.get_all_tools = _create_run_loop_get_all_tools_wrapper( - run_loop.get_all_tools - ) - - class OpenAIAgentsIntegration(Integration): identifier = "openai_agents" @@ -83,7 +72,27 @@ def setup_once() -> None: 0, 8, ): - _patch_run_get_all_tools() + + @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 return - _patch_agent_runner_get_all_tools() + 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) diff --git a/sentry_sdk/integrations/openai_agents/patches/__init__.py b/sentry_sdk/integrations/openai_agents/patches/__init__.py index 675f8c4fc4..ab3948bdc1 100644 --- a/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ b/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -1,8 +1,5 @@ from .models import _create_get_model_wrapper # noqa: F401 -from .tools import ( - _create_runner_get_all_tools_wrapper, - _create_run_loop_get_all_tools_wrapper, -) # noqa: F401 +from .tools import _get_all_tools # noqa: F401 from .runner import _create_run_wrapper, _create_run_streamed_wrapper # noqa: F401 from .agent_run import _patch_agent_run # noqa: F401 from .error_tracing import _patch_error_tracing # noqa: F401 diff --git a/sentry_sdk/integrations/openai_agents/patches/tools.py b/sentry_sdk/integrations/openai_agents/patches/tools.py index 561d9449e2..7674c24a8d 100644 --- a/sentry_sdk/integrations/openai_agents/patches/tools.py +++ b/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -66,34 +66,3 @@ async def sentry_wrapped_on_invoke_tool( wrapped_tools.append(wrapped_tool) return wrapped_tools - - -def _create_runner_get_all_tools_wrapper( - original_get_all_tools: "Callable[..., Any]", -) -> "Callable[..., Any]": - @wraps( - original_get_all_tools.__func__ - if hasattr(original_get_all_tools, "__func__") - else original_get_all_tools - ) - async def 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) - - return wrapped_get_all_tools - - -def _create_run_loop_get_all_tools_wrapper( - original_get_all_tools: "Callable[..., Any]", -) -> "Callable[..., Any]": - @wraps(original_get_all_tools) - async def wrapped_get_all_tools( - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools(original_get_all_tools, agent, context_wrapper) - - return wrapped_get_all_tools From b9e9d6b84c2e2e15f87f9ebf568ed82e04d0cad4 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Feb 2026 09:35:47 +0100 Subject: [PATCH 5/9] fix(openai-agents): Patch model functions following library refactor --- .../integrations/openai_agents/__init__.py | 21 +- .../openai_agents/patches/__init__.py | 2 +- .../openai_agents/patches/models.py | 235 +++++++++--------- 3 files changed, 124 insertions(+), 134 deletions(-) diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index e12a9c2a65..1882732614 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -4,7 +4,7 @@ from functools import wraps from .patches import ( - _create_get_model_wrapper, + _get_model, _get_all_tools, _create_run_wrapper, _create_run_streamed_wrapper, @@ -52,12 +52,6 @@ def _patch_runner() -> None: _patch_agent_run() -def _patch_model() -> None: - agents.run.AgentRunner._get_model = classmethod( - _create_get_model_wrapper(agents.run.AgentRunner._get_model), - ) - - class OpenAIAgentsIntegration(Integration): """ NOTE: With version 0.8.0, the class methods below have been refactored to functions. @@ -73,7 +67,7 @@ class OpenAIAgentsIntegration(Integration): - `Runner.run()` and `Runner.run_streamed()` are thin wrappers for `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()`. - `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 `_create_get_model_wrapper()` in `_patch_model()`. + - 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 `_get_all_tools()`. - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patched_run_single_turn()` and `patched_run_single_turn_streamed()`. 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patched_execute_final_output()`. @@ -90,7 +84,6 @@ class OpenAIAgentsIntegration(Integration): @staticmethod def setup_once() -> None: _patch_error_tracing() - _patch_model() _patch_runner() library_version = parse_version(OPENAI_AGENTS_VERSION) @@ -122,3 +115,13 @@ async def old_wrapped_get_all_tools( 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__) + def old_wrapped_get_model( + cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" + ) -> "list[agents.Tool]": + return _get_model(original_get_model, agent, run_config) + + agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model) diff --git a/sentry_sdk/integrations/openai_agents/patches/__init__.py b/sentry_sdk/integrations/openai_agents/patches/__init__.py index ab3948bdc1..fe06200793 100644 --- a/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ b/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -1,4 +1,4 @@ -from .models import _create_get_model_wrapper # noqa: F401 +from .models import _get_model # noqa: F401 from .tools import _get_all_tools # noqa: F401 from .runner import _create_run_wrapper, _create_run_streamed_wrapper # noqa: F401 from .agent_run import _patch_agent_run # noqa: F401 diff --git a/sentry_sdk/integrations/openai_agents/patches/models.py b/sentry_sdk/integrations/openai_agents/patches/models.py index 5d4d71185f..6b5dceef97 100644 --- a/sentry_sdk/integrations/openai_agents/patches/models.py +++ b/sentry_sdk/integrations/openai_agents/patches/models.py @@ -66,141 +66,128 @@ def _inject_trace_propagation_headers( headers[key] = value -def _create_get_model_wrapper( - original_get_model: "Callable[..., Any]", -) -> "Callable[..., Any]": +def _get_model( + original_get_model: "Callable[..., agents.Model]", + agent: "agents.Agent", + run_config: "agents.RunConfig", +) -> "agents.Model": """ - Wraps the agents.Runner._get_model method to wrap the get_response method of the model to create a AI client span. - Responsible for - creating and managing AI client spans. - adding trace propagation headers to tools with type HostedMCPTool. - setting the response model on agent invocation spans. """ + # copy the model to double patching its methods. We use copy on purpose here (instead of deepcopy) + # because we only patch its direct methods, all underlying data can remain unchanged. + model = copy.copy(original_get_model(agent, run_config)) + + # Capture the request model name for spans (agent.model can be None when using defaults) + request_model_name = model.model if hasattr(model, "model") else str(model) + agent._sentry_request_model = request_model_name + + # Wrap _fetch_response if it exists (for OpenAI models) to capture response model + if hasattr(model, "_fetch_response"): + original_fetch_response = model._fetch_response + + @wraps(original_fetch_response) + async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": + response = await original_fetch_response(*args, **kwargs) + if hasattr(response, "model") and response.model: + agent._sentry_response_model = str(response.model) + return response + + model._fetch_response = wrapped_fetch_response + + original_get_response = model.get_response + + @wraps(original_get_response) + async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": + mcp_tools = kwargs.get("tools") + hosted_tools = [] + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] + + with ai_client_span(agent, kwargs) as span: + for hosted_tool in hosted_tools: + _inject_trace_propagation_headers(hosted_tool, span=span) + + result = await original_get_response(*args, **kwargs) + + # Get response model captured from _fetch_response and clean up + response_model = getattr(agent, "_sentry_response_model", None) + if response_model: + delattr(agent, "_sentry_response_model") + + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span(span, result, response_model, agent) + + return result + + model.get_response = wrapped_get_response + + # Also wrap stream_response for streaming support + if hasattr(model, "stream_response"): + original_stream_response = model.stream_response + + @wraps(original_stream_response) + async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": + # Uses explicit try/finally instead of context manager to ensure cleanup + # even if the consumer abandons the stream (GeneratorExit). + span_kwargs = dict(kwargs) + if len(args) > 0: + span_kwargs["system_instructions"] = args[0] + if len(args) > 1: + span_kwargs["input"] = args[1] - @wraps( - original_get_model.__func__ - if hasattr(original_get_model, "__func__") - else original_get_model - ) - def wrapped_get_model( - cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "agents.Model": - # copy the model to double patching its methods. We use copy on purpose here (instead of deepcopy) - # because we only patch its direct methods, all underlying data can remain unchanged. - model = copy.copy(original_get_model(agent, run_config)) - - # Capture the request model name for spans (agent.model can be None when using defaults) - request_model_name = model.model if hasattr(model, "model") else str(model) - agent._sentry_request_model = request_model_name - - # Wrap _fetch_response if it exists (for OpenAI models) to capture response model - if hasattr(model, "_fetch_response"): - original_fetch_response = model._fetch_response - - @wraps(original_fetch_response) - async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": - response = await original_fetch_response(*args, **kwargs) - if hasattr(response, "model") and response.model: - agent._sentry_response_model = str(response.model) - return response - - model._fetch_response = wrapped_fetch_response - - original_get_response = model.get_response - - @wraps(original_get_response) - async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": - mcp_tools = kwargs.get("tools") hosted_tools = [] - if mcp_tools is not None: - hosted_tools = [ - tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) - ] + if len(args) > 3: + mcp_tools = args[3] + + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] - with ai_client_span(agent, kwargs) as span: + with ai_client_span(agent, span_kwargs) as span: for hosted_tool in hosted_tools: _inject_trace_propagation_headers(hosted_tool, span=span) - result = await original_get_response(*args, **kwargs) - - # Get response model captured from _fetch_response and clean up - response_model = getattr(agent, "_sentry_response_model", None) - if response_model: - delattr(agent, "_sentry_response_model") - - _set_response_model_on_agent_span(agent, response_model) - update_ai_client_span(span, result, response_model, agent) - - return result - - model.get_response = wrapped_get_response - - # Also wrap stream_response for streaming support - if hasattr(model, "stream_response"): - original_stream_response = model.stream_response - - @wraps(original_stream_response) - async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": - # Uses explicit try/finally instead of context manager to ensure cleanup - # even if the consumer abandons the stream (GeneratorExit). - span_kwargs = dict(kwargs) - if len(args) > 0: - span_kwargs["system_instructions"] = args[0] - if len(args) > 1: - span_kwargs["input"] = args[1] - - hosted_tools = [] - if len(args) > 3: - mcp_tools = args[3] - - if mcp_tools is not None: - hosted_tools = [ - tool - for tool in mcp_tools - if isinstance(tool, HostedMCPTool) - ] - - with ai_client_span(agent, span_kwargs) as span: - for hosted_tool in hosted_tools: - _inject_trace_propagation_headers(hosted_tool, span=span) - - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - streaming_response = None - ttft_recorded = False - # Capture start time locally to avoid race conditions with concurrent requests - start_time = time.perf_counter() - - async for event in original_stream_response(*args, **kwargs): - # Detect first content token (text delta event) - if not ttft_recorded and hasattr(event, "delta"): - ttft = time.perf_counter() - start_time - span.set_data( - SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - ttft_recorded = True - - # Capture the full response from ResponseCompletedEvent - if hasattr(event, "response"): - streaming_response = event.response - yield event - - # Update span with response data (usage, output, model) - if streaming_response: - response_model = ( - str(streaming_response.model) - if hasattr(streaming_response, "model") - and streaming_response.model - else None - ) - _set_response_model_on_agent_span(agent, response_model) - update_ai_client_span( - span, streaming_response, response_model, agent - ) - - model.stream_response = wrapped_stream_response + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - return model + streaming_response = None + ttft_recorded = False + # Capture start time locally to avoid race conditions with concurrent requests + start_time = time.perf_counter() - return wrapped_get_model + async for event in original_stream_response(*args, **kwargs): + # Detect first content token (text delta event) + if not ttft_recorded and hasattr(event, "delta"): + ttft = time.perf_counter() - start_time + span.set_data( + SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + ttft_recorded = True + + # Capture the full response from ResponseCompletedEvent + if hasattr(event, "response"): + streaming_response = event.response + yield event + + # Update span with response data (usage, output, model) + if streaming_response: + response_model = ( + str(streaming_response.model) + if hasattr(streaming_response, "model") + and streaming_response.model + else None + ) + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span( + span, streaming_response, response_model, agent + ) + + model.stream_response = wrapped_stream_response + + return model From 0a2ede87e8691c7b3183b34fdee44c83279c1f31 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Feb 2026 09:38:02 +0100 Subject: [PATCH 6/9] . --- sentry_sdk/integrations/openai_agents/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index e12a9c2a65..0c551fd9bd 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -74,7 +74,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 `_create_get_model_wrapper()` in `_patch_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 `_get_all_tools()`. + - 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 `patched_run_single_turn()` and `patched_run_single_turn_streamed()`. 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patched_execute_final_output()`. From 0970319f2c156e85b15452886208d86dc61d43c5 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Feb 2026 09:44:12 +0100 Subject: [PATCH 7/9] . --- sentry_sdk/integrations/openai_agents/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index c106867a98..62a6da5d40 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -30,9 +30,10 @@ try: # AgentRunner methods moved in v0.8 # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 - from agents.run_internal import run_loop + from agents.run_internal import run_loop, turn_preparation except ImportError: run_loop = None + turn_preparation = None def _patch_runner() -> None: @@ -102,6 +103,14 @@ async def new_wrapped_get_all_tools( ) agents.run.get_all_tools = new_wrapped_get_all_tools + + @wraps(turn_preparation.get_model) + def new_wrapped_get_model( + agent: "agents.Agent", run_config: "agents.RunConfig" + ) -> "agents.Model": + return _get_model(turn_preparation.get_model, agent, run_config) + + agents.run_internal.run_loop.get_model = new_wrapped_get_model return original_get_all_tools = AgentRunner._get_all_tools From 168e40580d3e59971855713e885b4ad5bf7f798e Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Feb 2026 11:20:34 +0100 Subject: [PATCH 8/9] . --- sentry_sdk/integrations/openai_agents/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index 62a6da5d40..89e09169f2 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -130,7 +130,7 @@ async def old_wrapped_get_all_tools( @wraps(AgentRunner._get_model.__func__) def old_wrapped_get_model( cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "list[agents.Tool]": + ) -> "list[agents.Model]": return _get_model(original_get_model, agent, run_config) agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model) From 48aff66d2293c7cc627a1e915321507f6bcfd06d Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Feb 2026 11:56:27 +0100 Subject: [PATCH 9/9] . --- sentry_sdk/integrations/openai_agents/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index 89e09169f2..9b3a670c2c 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -130,7 +130,7 @@ async def old_wrapped_get_all_tools( @wraps(AgentRunner._get_model.__func__) def old_wrapped_get_model( cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "list[agents.Model]": + ) -> "agents.Model": return _get_model(original_get_model, agent, run_config) agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model)