Skip to content

Commit 412b0ac

Browse files
fix(openai-agents): Inject propagation headers for HostedMCPTool when streaming
1 parent 97ee0b1 commit 412b0ac

File tree

2 files changed

+162
-1
lines changed

2 files changed

+162
-1
lines changed

sentry_sdk/integrations/openai_agents/patches/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,21 @@ async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any":
145145
if len(args) > 1:
146146
span_kwargs["input"] = args[1]
147147

148+
hosted_tools = []
149+
if len(args) > 3:
150+
mcp_tools = args[3]
151+
152+
if mcp_tools is not None:
153+
hosted_tools = [
154+
tool
155+
for tool in mcp_tools
156+
if isinstance(tool, HostedMCPTool)
157+
]
158+
148159
with ai_client_span(agent, span_kwargs) as span:
160+
for hosted_tool in hosted_tools:
161+
_inject_trace_propagation_headers(hosted_tool, span=span)
162+
149163
span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)
150164

151165
streaming_response = None

tests/integrations/openai_agents/test_openai_agents.py

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@
3535
from agents.exceptions import MaxTurnsExceeded, ModelBehaviorError
3636
from agents.version import __version__ as OPENAI_AGENTS_VERSION
3737

38-
from openai.types.responses import Response, ResponseUsage
38+
from openai.types.responses import (
39+
ResponseCreatedEvent,
40+
ResponseTextDeltaEvent,
41+
ResponseCompletedEvent,
42+
Response,
43+
ResponseUsage,
44+
)
3945
from openai.types.responses.response_usage import (
4046
InputTokensDetails,
4147
OutputTokensDetails,
@@ -80,6 +86,63 @@
8086
)
8187

8288

89+
async def EXAMPLE_STREAMED_RESPONSE(*args, **kwargs):
90+
yield ResponseCreatedEvent(
91+
response=Response(
92+
id="chat-id",
93+
output=[],
94+
parallel_tool_calls=False,
95+
tool_choice="none",
96+
tools=[],
97+
created_at=10000000,
98+
model="response-model-id",
99+
object="response",
100+
),
101+
type="response.created",
102+
sequence_number=0,
103+
)
104+
105+
yield ResponseCompletedEvent(
106+
response=Response(
107+
id="chat-id",
108+
output=[
109+
ResponseOutputMessage(
110+
id="message-id",
111+
content=[
112+
ResponseOutputText(
113+
annotations=[],
114+
text="the model response",
115+
type="output_text",
116+
),
117+
],
118+
role="assistant",
119+
status="completed",
120+
type="message",
121+
),
122+
],
123+
parallel_tool_calls=False,
124+
tool_choice="none",
125+
tools=[],
126+
created_at=10000000,
127+
model="response-model-id",
128+
object="response",
129+
usage=ResponseUsage(
130+
input_tokens=20,
131+
input_tokens_details=InputTokensDetails(
132+
cached_tokens=5,
133+
),
134+
output_tokens=10,
135+
output_tokens_details=OutputTokensDetails(
136+
reasoning_tokens=8,
137+
),
138+
total_tokens=30,
139+
),
140+
),
141+
type="response.completed",
142+
sequence_number=1,
143+
)
144+
145+
83146
@pytest.fixture
84147
def mock_usage():
85148
return Usage(
@@ -1172,6 +1235,90 @@ def simple_test_tool(message: str) -> str:
11721235
assert ai_client_span2["data"]["gen_ai.usage.total_tokens"] == 25
11731236

11741237

1238+
@pytest.mark.asyncio
1239+
async def test_hosted_mcp_tool_propagation_header_streamed(sentry_init, test_agent):
1240+
"""
1241+
Test responses API is given trace propagation headers with HostedMCPTool.
1242+
"""
1243+
1244+
hosted_tool = HostedMCPTool(
1245+
tool_config={
1246+
"type": "mcp",
1247+
"server_label": "test_server",
1248+
"server_url": "http://example.com/",
1249+
"headers": {
1250+
"baggage": "custom=data",
1251+
},
1252+
},
1253+
)
1254+
1255+
client = AsyncOpenAI(api_key="z")
1256+
client.responses._post = AsyncMock(return_value=EXAMPLE_RESPONSE)
1257+
1258+
model = OpenAIResponsesModel(model="gpt-4", openai_client=client)
1259+
1260+
agent_with_tool = test_agent.clone(
1261+
tools=[hosted_tool],
1262+
model=model,
1263+
)
1264+
1265+
sentry_init(
1266+
integrations=[OpenAIAgentsIntegration()],
1267+
traces_sample_rate=1.0,
1268+
release="d08ebdb9309e1b004c6f52202de58a09c2268e42",
1269+
)
1270+
1271+
with patch.object(
1272+
model._client.responses,
1273+
"create",
1274+
side_effect=EXAMPLE_STREAMED_RESPONSE,
1275+
) as create, mock.patch(
1276+
"sentry_sdk.tracing_utils.Random.randrange", return_value=500000
1277+
):
1278+
with sentry_sdk.start_transaction(
1279+
name="/interactions/other-dogs/new-dog",
1280+
op="greeting.sniff",
1281+
trace_id="01234567890123456789012345678901",
1282+
) as transaction:
1283+
result = agents.Runner.run_streamed(
1284+
agent_with_tool,
1285+
"Please use the simple test tool",
1286+
run_config=test_run_config,
1287+
)
1288+
1289+
async for event in result.stream_events():
1290+
pass
1291+
1292+
ai_client_span = transaction._span_recorder.spans[-1]
1293+
1294+
args, kwargs = create.call_args
1295+
1296+
assert "tools" in kwargs
1297+
assert len(kwargs["tools"]) == 1
1298+
hosted_mcp_tool = kwargs["tools"][0]
1299+
1300+
assert hosted_mcp_tool["headers"][
1301+
"sentry-trace"
1302+
] == "{trace_id}-{parent_span_id}-{sampled}".format(
1303+
trace_id=transaction.trace_id,
1304+
parent_span_id=ai_client_span.span_id,
1305+
sampled=1,
1306+
)
1307+
1308+
expected_outgoing_baggage = (
1309+
"custom=data,"
1310+
"sentry-trace_id=01234567890123456789012345678901,"
1311+
"sentry-sample_rand=0.500000,"
1312+
"sentry-environment=production,"
1313+
"sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42,"
1314+
"sentry-transaction=/interactions/other-dogs/new-dog,"
1315+
"sentry-sample_rate=1.0,"
1316+
"sentry-sampled=true"
1317+
)
1318+
1319+
assert hosted_mcp_tool["headers"]["baggage"] == expected_outgoing_baggage
1320+
1321+
11751322
@pytest.mark.asyncio
11761323
async def test_hosted_mcp_tool_propagation_headers(sentry_init, test_agent):
11771324
"""

0 commit comments

Comments
 (0)