Skip to content

Commit 23844bb

Browse files
fix(openai-agents): support json inputs (#3354)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent 32fdf13 commit 23844bb

File tree

3 files changed

+198
-4
lines changed

3 files changed

+198
-4
lines changed

packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/_hooks.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,17 @@ def on_span_end(self, span):
236236
for i, message in enumerate(input_data):
237237
if hasattr(message, 'role') and hasattr(message, 'content'):
238238
otel_span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{i}.role", message.role)
239-
otel_span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{i}.content", message.content)
239+
content = message.content
240+
if not isinstance(content, str):
241+
content = json.dumps(content)
242+
otel_span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{i}.content", content)
240243
elif isinstance(message, dict):
241244
if 'role' in message and 'content' in message:
242245
otel_span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{i}.role", message['role'])
243-
otel_span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{i}.content", message['content'])
246+
content = message['content']
247+
if isinstance(content, dict):
248+
content = json.dumps(content)
249+
otel_span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{i}.content", content)
244250

245251
# Add function/tool specifications to the request using OpenAI semantic conventions
246252
response = getattr(span_data, 'response', None)
@@ -365,11 +371,17 @@ def on_span_end(self, span):
365371
for i, message in enumerate(input_data):
366372
if hasattr(message, 'role') and hasattr(message, 'content'):
367373
otel_span.set_attribute(f"gen_ai.prompt.{i}.role", message.role)
368-
otel_span.set_attribute(f"gen_ai.prompt.{i}.content", message.content)
374+
content = message.content
375+
if isinstance(content, dict):
376+
content = json.dumps(content)
377+
otel_span.set_attribute(f"gen_ai.prompt.{i}.content", content)
369378
elif isinstance(message, dict):
370379
if 'role' in message and 'content' in message:
371380
otel_span.set_attribute(f"gen_ai.prompt.{i}.role", message['role'])
372-
otel_span.set_attribute(f"gen_ai.prompt.{i}.content", message['content'])
381+
content = message['content']
382+
if isinstance(content, dict):
383+
content = json.dumps(content)
384+
otel_span.set_attribute(f"gen_ai.prompt.{i}.content", content)
373385

374386
response = getattr(span_data, 'response', None)
375387
if response:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
interactions:
2+
- request:
3+
body: '{"include":[],"input":[{"role":"user","content":[{"type":"input_text","text":"Hello,
4+
can you help me?"}]},{"role":"assistant","content":[{"type":"output_text","text":"Of
5+
course! How can I help you?"}]},{"role":"user","content":[{"type":"input_text","text":"What
6+
is the weather like?"}]}],"instructions":"You are a helpful assistant.","model":"gpt-4o","stream":false,"tools":[]}'
7+
headers:
8+
accept:
9+
- application/json
10+
accept-encoding:
11+
- gzip, deflate
12+
connection:
13+
- keep-alive
14+
content-length:
15+
- '377'
16+
content-type:
17+
- application/json
18+
cookie:
19+
- _cfuvid=PWHn6CD5_OXbE3jv9HT7E4FDlSvoTN5AciqTl4Chslg-1755280559217-0.0.1.1-604800000
20+
host:
21+
- api.openai.com
22+
user-agent:
23+
- Agents/Python 0.2.7
24+
x-stainless-arch:
25+
- arm64
26+
x-stainless-async:
27+
- async:asyncio
28+
x-stainless-lang:
29+
- python
30+
x-stainless-os:
31+
- MacOS
32+
x-stainless-package-version:
33+
- 1.99.9
34+
x-stainless-read-timeout:
35+
- '600'
36+
x-stainless-retry-count:
37+
- '0'
38+
x-stainless-runtime:
39+
- CPython
40+
x-stainless-runtime-version:
41+
- 3.11.10
42+
method: POST
43+
uri: https://api.openai.com/v1/responses
44+
response:
45+
body:
46+
string: !!binary |
47+
H4sIAAAAAAAAA3RUTW/bMAy951cIuuzSFPlwHDv/YLddh2IwaIlOtMqiIFFdg6L/fbCcOPGWXgKH
48+
j3x6fKT0sRBCGi0PQgaMvimrdt1u243aaVWt601dVKqua70r1H5XAKyKUu8qVW/KYqt0qeTTQEDt
49+
b1R8JSEXcYyrgMCoGxiw9X5XFvtyu60yFhk4xaFGUe8tMuqxqAX1egyU3KCqAxsxhzEECvIgXLI2
50+
B4y7FjYaGYyNczRySIoNuXzIT0oCAgoQJ7S+S1ZAjCYyOH4ez+3hvaHEPnHD9IpuRjeATGQbBXZ+
51+
UE8a7XDC0fOyoOVmtSmWq2q5Ki/eZEp5EC8LIYT4yL+T6X08Xj3f1tBWg+dVVdadWumy2FWbQu0e
52+
ep45+Owxs2CMcMQb8JW5GVTkGN1N0r2sGe3VDXznqTongHPEcPX25dcMtHT0gdoHSCY6CPldKHDf
53+
WPhAb0ajCAh2yaZH8QeBTxhE8hoY47P4YREiCnVC9SpgwiOGN6NQUBDgvegoCD6hsEMRC+M6Cn3W
54+
J4wTZ0phmD08y0nN5+VrEigD2dz0tBZj8pCYk6SHANaine8BhzTupw/4ZijF5noFmjzhaU98oN5z
55+
o0CdsHnF8z0WECI5447ycJmExK6jwHdJw1RT30O4Vi6E+BxvEnTI58ZodGw6g7NbcnGq4TEuNXaQ
56+
7DhPGZkC3jfB2HsMwCmH18+rSzTP7aJs9Hb6f7cvOW907aL4DUNL0fB53FJtUi8n3aOPJzJqND4x
57+
yQm4rY9k8s3dUq2moL/XGJJTeeS5SxOhtdcXJeXLMTVg3OyOF/un/+N3T8rUZh6dvhWuZq3++3Rs
58+
ikfAI95p+l9RMzHYG7hfTxamOJ92jwwaGAb6z8XnXwAAAP//AwBykjkw3gUAAA==
59+
headers:
60+
CF-RAY:
61+
- 976c9abcf9ddbfae-ATL
62+
Connection:
63+
- keep-alive
64+
Content-Encoding:
65+
- gzip
66+
Content-Type:
67+
- application/json
68+
Date:
69+
- Fri, 29 Aug 2025 14:05:40 GMT
70+
Server:
71+
- cloudflare
72+
Set-Cookie:
73+
- __cf_bm=h5OPTb88ihzuHT.qP.Oj0SjE_tWpmfRrWNu4mUt4S1M-1756476340-1.0.1.1-80gk3ddE8DF8Q.NmuO.XNa5K71CoOLh.zzIRZmQUTtuH9p5jeXANgE.9G0ftTm8nXAM5XP_aHRdq_w_OfFWfYb04R5SPCVAWJdr.st7L2AM;
74+
path=/; expires=Fri, 29-Aug-25 14:35:40 GMT; domain=.api.openai.com; HttpOnly;
75+
Secure; SameSite=None
76+
- _cfuvid=TLb_ZfjHeeZ44eIV0y..9xwip611QAuocoxSqi3HLzY-1756476340100-0.0.1.1-604800000;
77+
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
78+
Transfer-Encoding:
79+
- chunked
80+
X-Content-Type-Options:
81+
- nosniff
82+
alt-svc:
83+
- h3=":443"; ma=86400
84+
cf-cache-status:
85+
- DYNAMIC
86+
openai-organization:
87+
- traceloop
88+
openai-processing-ms:
89+
- '1313'
90+
openai-project:
91+
- proj_tzz1TbPPOXaf6j9tEkVUBIAa
92+
openai-version:
93+
- '2020-10-01'
94+
strict-transport-security:
95+
- max-age=31536000; includeSubDomains; preload
96+
x-envoy-upstream-service-time:
97+
- '1318'
98+
x-ratelimit-limit-requests:
99+
- '10000'
100+
x-ratelimit-limit-tokens:
101+
- '30000000'
102+
x-ratelimit-remaining-requests:
103+
- '9999'
104+
x-ratelimit-remaining-tokens:
105+
- '29999934'
106+
x-ratelimit-reset-requests:
107+
- 6ms
108+
x-ratelimit-reset-tokens:
109+
- 0s
110+
x-request-id:
111+
- req_79d83ab696490c185fd869f472d7deed
112+
status:
113+
code: 200
114+
message: OK
115+
version: 1

packages/opentelemetry-instrumentation-openai-agents/tests/test_openai_agents.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,73 @@ def mock_instrumentor():
2323
return instrumentor
2424

2525

26+
@pytest.mark.vcr
27+
def test_dict_content_serialization(exporter):
28+
"""Test that dictionary content in messages is properly serialized to JSON strings."""
29+
import json
30+
from agents import Agent, Runner
31+
32+
# Create a simple agent
33+
test_agent = Agent(
34+
name="TestAgent",
35+
instructions="You are a helpful assistant.",
36+
model="gpt-4o",
37+
)
38+
39+
# Create a query with structured content as array of objects (multimodal format)
40+
# This should create dict structures that need serialization
41+
structured_query = [
42+
{
43+
"role": "user",
44+
"content": [
45+
{"type": "input_text", "text": "Hello, can you help me?"}
46+
]
47+
},
48+
{
49+
"role": "assistant",
50+
"content": [
51+
{"type": "output_text", "text": "Of course! How can I help you?"}
52+
]
53+
},
54+
{
55+
"role": "user",
56+
"content": [
57+
{"type": "input_text", "text": "What is the weather like?"}
58+
]
59+
}
60+
]
61+
62+
# Run the agent with structured content
63+
Runner.run_sync(test_agent, structured_query)
64+
65+
spans = exporter.get_finished_spans()
66+
67+
# Look for any spans with prompt/content attributes
68+
for span in spans:
69+
for attr_name, attr_value in span.attributes.items():
70+
prompt_content_check = (
71+
("prompt" in attr_name and "content" in attr_name) or
72+
("gen_ai.prompt" in attr_name and "content" in attr_name)
73+
)
74+
if prompt_content_check:
75+
# All content attributes should be strings, not dicts
76+
error_msg = (
77+
f"Attribute {attr_name} should be a string, "
78+
f"got {type(attr_value)}: {attr_value}"
79+
)
80+
assert isinstance(attr_value, str), error_msg
81+
82+
# If it looks like JSON, verify it can be parsed
83+
if attr_value.startswith('{') and attr_value.endswith('}'):
84+
try:
85+
json.loads(attr_value)
86+
except json.JSONDecodeError:
87+
# If it fails to parse, that's still fine - just not JSON
88+
pass
89+
90+
# The test passes if no dict type warnings occurred (all content attributes are strings)
91+
92+
2693
@pytest.mark.vcr
2794
def test_agent_spans(exporter, test_agent):
2895
query = "What is AI?"

0 commit comments

Comments
 (0)