Skip to content

Commit bc374a0

Browse files
authored
Add per-run tracing API key support (#2260)
1 parent afa224b commit bc374a0

16 files changed

Lines changed: 220 additions & 54 deletions

docs/config.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ from agents import set_tracing_export_api_key
3838
set_tracing_export_api_key("sk-...")
3939
```
4040

41+
You can also set a tracing API key per run without changing the global exporter.
42+
43+
```python
44+
from agents import Runner, RunConfig
45+
46+
await Runner.run(
47+
agent,
48+
input="Hello",
49+
run_config=RunConfig(tracing={"api_key": "sk-tracing-123"}),
50+
)
51+
```
52+
4153
You can also disable tracing entirely by using the [`set_tracing_disabled()`][agents.set_tracing_disabled] function.
4254

4355
```python

docs/tracing.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ agent = Agent(
121121
)
122122
```
123123

124+
If you only need a different tracing key for a single run, pass it via `RunConfig` instead of changing the global exporter.
125+
126+
```python
127+
from agents import Runner, RunConfig
128+
129+
await Runner.run(
130+
agent,
131+
input="Hello",
132+
run_config=RunConfig(tracing={"api_key": "sk-tracing-123"}),
133+
)
134+
```
135+
124136
## Notes
125137
- View free traces at Openai Traces dashboard.
126138

@@ -147,4 +159,3 @@ agent = Agent(
147159
- [Portkey AI](https://portkey.ai/docs/integrations/agents/openai-agents)
148160
- [LangDB AI](https://docs.langdb.ai/getting-started/working-with-agent-frameworks/working-with-openai-agents-sdk)
149161
- [Agenta](https://docs.agenta.ai/observability/integrations/openai-agents)
150-

src/agents/_run_impl.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
from .tracing import (
108108
SpanError,
109109
Trace,
110+
TracingConfig,
110111
function_span,
111112
get_current_trace,
112113
guardrail_span,
@@ -1515,13 +1516,15 @@ def __init__(
15151516
group_id: str | None,
15161517
metadata: dict[str, Any] | None,
15171518
disabled: bool,
1519+
tracing: TracingConfig | None = None,
15181520
):
15191521
self.trace: Trace | None = None
15201522
self.workflow_name = workflow_name
15211523
self.trace_id = trace_id
15221524
self.group_id = group_id
15231525
self.metadata = metadata
15241526
self.disabled = disabled
1527+
self.tracing = tracing
15251528

15261529
def __enter__(self) -> TraceCtxManager:
15271530
current_trace = get_current_trace()
@@ -1531,6 +1534,7 @@ def __enter__(self) -> TraceCtxManager:
15311534
trace_id=self.trace_id,
15321535
group_id=self.group_id,
15331536
metadata=self.metadata,
1537+
tracing=self.tracing,
15341538
disabled=self.disabled,
15351539
)
15361540
self.trace.start(mark_as_current=True)

src/agents/run.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
)
7474
from .tool import Tool, dispose_resolved_computers
7575
from .tool_guardrails import ToolInputGuardrailResult, ToolOutputGuardrailResult
76-
from .tracing import Span, SpanError, agent_span, get_current_trace, trace
76+
from .tracing import Span, SpanError, TracingConfig, agent_span, get_current_trace, trace
7777
from .tracing.span_data import AgentSpanData
7878
from .usage import Usage
7979
from .util import _coro, _error_tracing
@@ -226,6 +226,9 @@ class RunConfig:
226226
"""Whether tracing is disabled for the agent run. If disabled, we will not trace the agent run.
227227
"""
228228

229+
tracing: TracingConfig | None = None
230+
"""Tracing configuration for this run."""
231+
229232
trace_include_sensitive_data: bool = field(
230233
default_factory=_default_trace_include_sensitive_data
231234
)
@@ -575,6 +578,7 @@ async def run(
575578
trace_id=run_config.trace_id,
576579
group_id=run_config.group_id,
577580
metadata=run_config.trace_metadata,
581+
tracing=run_config.tracing,
578582
disabled=run_config.tracing_disabled,
579583
):
580584
current_turn = 0
@@ -902,6 +906,7 @@ def run_streamed(
902906
trace_id=run_config.trace_id,
903907
group_id=run_config.group_id,
904908
metadata=run_config.trace_metadata,
909+
tracing=run_config.tracing,
905910
disabled=run_config.tracing_disabled,
906911
)
907912
)

src/agents/tracing/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import atexit
22

3+
from .config import TracingConfig
34
from .create import (
45
agent_span,
56
custom_span,
@@ -53,6 +54,7 @@
5354
"set_trace_processors",
5455
"set_trace_provider",
5556
"set_tracing_disabled",
57+
"TracingConfig",
5658
"trace",
5759
"Trace",
5860
"SpanError",

src/agents/tracing/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import annotations
2+
3+
from typing_extensions import TypedDict
4+
5+
6+
class TracingConfig(TypedDict, total=False):
7+
"""Configuration for tracing export."""
8+
9+
api_key: str

src/agents/tracing/create.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import TYPE_CHECKING, Any
55

66
from ..logger import logger
7+
from .config import TracingConfig
78
from .setup import get_trace_provider
89
from .span_data import (
910
AgentSpanData,
@@ -30,6 +31,7 @@ def trace(
3031
trace_id: str | None = None,
3132
group_id: str | None = None,
3233
metadata: dict[str, Any] | None = None,
34+
tracing: TracingConfig | None = None,
3335
disabled: bool = False,
3436
) -> Trace:
3537
"""
@@ -50,6 +52,7 @@ def trace(
5052
group_id: Optional grouping identifier to link multiple traces from the same conversation
5153
or process. For instance, you might use a chat thread ID.
5254
metadata: Optional dictionary of additional metadata to attach to the trace.
55+
tracing: Optional tracing configuration for exporting this trace.
5356
disabled: If True, we will return a Trace but the Trace will not be recorded.
5457
5558
Returns:
@@ -66,6 +69,7 @@ def trace(
6669
trace_id=trace_id,
6770
group_id=group_id,
6871
metadata=metadata,
72+
tracing=tracing,
6973
disabled=disabled,
7074
)
7175

src/agents/tracing/processors.py

Lines changed: 63 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -92,62 +92,73 @@ def export(self, items: list[Trace | Span[Any]]) -> None:
9292
if not items:
9393
return
9494

95-
if not self.api_key:
96-
logger.warning("OPENAI_API_KEY is not set, skipping trace export")
97-
return
98-
99-
data = [item.export() for item in items if item.export()]
100-
payload = {"data": data}
101-
102-
headers = {
103-
"Authorization": f"Bearer {self.api_key}",
104-
"Content-Type": "application/json",
105-
"OpenAI-Beta": "traces=v1",
106-
}
107-
108-
if self.organization:
109-
headers["OpenAI-Organization"] = self.organization
110-
111-
if self.project:
112-
headers["OpenAI-Project"] = self.project
113-
114-
# Exponential backoff loop
115-
attempt = 0
116-
delay = self.base_delay
117-
while True:
118-
attempt += 1
119-
try:
120-
response = self._client.post(url=self.endpoint, headers=headers, json=payload)
121-
122-
# If the response is successful, break out of the loop
123-
if response.status_code < 300:
124-
logger.debug(f"Exported {len(items)} items")
125-
return
95+
grouped_items: dict[str | None, list[Trace | Span[Any]]] = {}
96+
for item in items:
97+
key = item.tracing_api_key
98+
grouped_items.setdefault(key, []).append(item)
99+
100+
for item_key, grouped in grouped_items.items():
101+
api_key = item_key or self.api_key
102+
if not api_key:
103+
logger.warning("OPENAI_API_KEY is not set, skipping trace export")
104+
continue
105+
106+
data = [item.export() for item in grouped if item.export()]
107+
payload = {"data": data}
108+
109+
headers = {
110+
"Authorization": f"Bearer {api_key}",
111+
"Content-Type": "application/json",
112+
"OpenAI-Beta": "traces=v1",
113+
}
114+
115+
if self.organization:
116+
headers["OpenAI-Organization"] = self.organization
117+
118+
if self.project:
119+
headers["OpenAI-Project"] = self.project
120+
121+
# Exponential backoff loop
122+
attempt = 0
123+
delay = self.base_delay
124+
while True:
125+
attempt += 1
126+
try:
127+
response = self._client.post(url=self.endpoint, headers=headers, json=payload)
128+
129+
# If the response is successful, break out of the loop
130+
if response.status_code < 300:
131+
logger.debug(f"Exported {len(grouped)} items")
132+
break
133+
134+
# If the response is a client error (4xx), we won't retry
135+
if 400 <= response.status_code < 500:
136+
logger.error(
137+
"[non-fatal] Tracing client error %s: %s",
138+
response.status_code,
139+
response.text,
140+
)
141+
break
142+
143+
# For 5xx or other unexpected codes, treat it as transient and retry
144+
logger.warning(
145+
f"[non-fatal] Tracing: server error {response.status_code}, retrying."
146+
)
147+
except httpx.RequestError as exc:
148+
# Network or other I/O error, we'll retry
149+
logger.warning(f"[non-fatal] Tracing: request failed: {exc}")
126150

127-
# If the response is a client error (4xx), we won't retry
128-
if 400 <= response.status_code < 500:
151+
# If we reach here, we need to retry or give up
152+
if attempt >= self.max_retries:
129153
logger.error(
130-
f"[non-fatal] Tracing client error {response.status_code}: {response.text}"
154+
"[non-fatal] Tracing: max retries reached, giving up on this batch."
131155
)
132-
return
133-
134-
# For 5xx or other unexpected codes, treat it as transient and retry
135-
logger.warning(
136-
f"[non-fatal] Tracing: server error {response.status_code}, retrying."
137-
)
138-
except httpx.RequestError as exc:
139-
# Network or other I/O error, we'll retry
140-
logger.warning(f"[non-fatal] Tracing: request failed: {exc}")
141-
142-
# If we reach here, we need to retry or give up
143-
if attempt >= self.max_retries:
144-
logger.error("[non-fatal] Tracing: max retries reached, giving up on this batch.")
145-
return
156+
break
146157

147-
# Exponential backoff + jitter
148-
sleep_time = delay + random.uniform(0, 0.1 * delay) # 10% jitter
149-
time.sleep(sleep_time)
150-
delay = min(delay * 2, self.max_delay)
158+
# Exponential backoff + jitter
159+
sleep_time = delay + random.uniform(0, 0.1 * delay) # 10% jitter
160+
time.sleep(sleep_time)
161+
delay = min(delay * 2, self.max_delay)
151162

152163
def close(self):
153164
"""Close the underlying HTTP client."""

src/agents/tracing/provider.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Any
99

1010
from ..logger import logger
11+
from .config import TracingConfig
1112
from .processor_interface import TracingProcessor
1213
from .scope import Scope
1314
from .spans import NoOpSpan, Span, SpanImpl, TSpanData
@@ -147,6 +148,7 @@ def create_trace(
147148
group_id: str | None = None,
148149
metadata: dict[str, Any] | None = None,
149150
disabled: bool = False,
151+
tracing: TracingConfig | None = None,
150152
) -> Trace:
151153
"""Create a new trace."""
152154

@@ -226,6 +228,7 @@ def create_trace(
226228
group_id: str | None = None,
227229
metadata: dict[str, Any] | None = None,
228230
disabled: bool = False,
231+
tracing: TracingConfig | None = None,
229232
) -> Trace:
230233
"""
231234
Create a new trace.
@@ -244,6 +247,7 @@ def create_trace(
244247
group_id=group_id,
245248
metadata=metadata,
246249
processor=self._multi_processor,
250+
tracing_api_key=tracing.get("api_key") if tracing else None,
247251
)
248252

249253
def create_span(
@@ -256,6 +260,7 @@ def create_span(
256260
"""
257261
Create a new span.
258262
"""
263+
tracing_api_key: str | None = None
259264
if self._disabled or disabled:
260265
logger.debug(f"Tracing is disabled. Not creating span {span_data}")
261266
return NoOpSpan(span_data)
@@ -277,19 +282,22 @@ def create_span(
277282

278283
parent_id = current_span.span_id if current_span else None
279284
trace_id = current_trace.trace_id
285+
tracing_api_key = current_trace.tracing_api_key
280286

281287
elif isinstance(parent, Trace):
282288
if isinstance(parent, NoOpTrace):
283289
logger.debug(f"Parent {parent} is no-op, returning NoOpSpan")
284290
return NoOpSpan(span_data)
285291
trace_id = parent.trace_id
286292
parent_id = None
293+
tracing_api_key = parent.tracing_api_key
287294
elif isinstance(parent, Span):
288295
if isinstance(parent, NoOpSpan):
289296
logger.debug(f"Parent {parent} is no-op, returning NoOpSpan")
290297
return NoOpSpan(span_data)
291298
parent_id = parent.span_id
292299
trace_id = parent.trace_id
300+
tracing_api_key = parent.tracing_api_key
293301

294302
logger.debug(f"Creating span {span_data} with id {span_id}")
295303

@@ -299,6 +307,7 @@ def create_span(
299307
parent_id=parent_id,
300308
processor=self._multi_processor,
301309
span_data=span_data,
310+
tracing_api_key=tracing_api_key,
302311
)
303312

304313
def shutdown(self) -> None:

0 commit comments

Comments
 (0)