Skip to content

Commit 2cef83b

Browse files
authored
fix: sanitize OpenAI tracing export payloads (#2896)
### Summary Updates the OpenAI tracing exporter to align usage metadata with the trace ingest payload shape. For the default OpenAI tracing endpoint, usage metadata is now only included on generation spans, where it is normalized to the supported token-count shape. Other tracing endpoints continue to receive the raw SDK payload. Adds regression coverage for OpenAI endpoint sanitization and custom endpoint behavior. ### Test plan - `make format` - `make lint` - `make sync && make typecheck` - `uv run pytest tests/test_openai_responses.py::test_get_response_span_exports_usage tests/test_trace_processor.py` (46 passed) - Manual tracing repro: `Runner.run(...)` completed, `flush_traces()` completed, and no tracing client errors were emitted. - `make tests` reached 3858 passed, 10 failed locally in sandbox tests because `sandbox-exec` returned `sandbox_apply: Operation not permitted`. ### Issue number N/A ### Checks - [x] I've added new tests (if relevant) - [ ] I've added/updated the relevant documentation - [x] I've run `make lint` and `make format` - [ ] I've made sure tests pass
1 parent 0a100fb commit 2cef83b

2 files changed

Lines changed: 37 additions & 2 deletions

File tree

src/agents/tracing/processors.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class BackendSpanExporter(TracingExporter):
3939
"output_tokens",
4040
}
4141
)
42+
_OPENAI_TRACING_USAGE_SPAN_TYPES = frozenset({"generation"})
4243
_UNSERIALIZABLE = object()
4344

4445
def __init__(
@@ -203,7 +204,12 @@ def _sanitize_for_openai_tracing_api(self, payload_item: dict[str, Any]) -> dict
203204
did_mutate = True
204205
sanitized_span_data[field_name] = sanitized_field
205206

206-
if span_data.get("type") != "generation":
207+
if span_data.get("type") not in self._OPENAI_TRACING_USAGE_SPAN_TYPES:
208+
if "usage" in span_data:
209+
if not did_mutate:
210+
sanitized_span_data = dict(span_data)
211+
did_mutate = True
212+
sanitized_span_data.pop("usage", None)
207213
if not did_mutate:
208214
return payload_item
209215
sanitized_payload_item = dict(payload_item)

tests/test_trace_processor.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ def export(self):
572572

573573

574574
@patch("httpx.Client")
575-
def test_backend_span_exporter_does_not_modify_non_generation_usage(mock_client):
575+
def test_backend_span_exporter_drops_non_generation_usage_for_openai_endpoint(mock_client):
576576
class DummyItem:
577577
tracing_api_key = None
578578

@@ -592,6 +592,35 @@ def export(self):
592592
exporter = BackendSpanExporter(api_key="test_key")
593593
exporter.export([cast(Any, DummyItem())])
594594

595+
sent_payload = mock_client.return_value.post.call_args.kwargs["json"]["data"][0]
596+
assert "usage" not in sent_payload["span_data"]
597+
exporter.close()
598+
599+
600+
@patch("httpx.Client")
601+
def test_backend_span_exporter_keeps_non_generation_usage_for_custom_endpoint(mock_client):
602+
class DummyItem:
603+
tracing_api_key = None
604+
605+
def export(self):
606+
return {
607+
"object": "trace.span",
608+
"span_data": {
609+
"type": "function",
610+
"usage": {"requests": 1},
611+
},
612+
}
613+
614+
mock_response = MagicMock()
615+
mock_response.status_code = 200
616+
mock_client.return_value.post.return_value = mock_response
617+
618+
exporter = BackendSpanExporter(
619+
api_key="test_key",
620+
endpoint="https://example.com/v1/traces/ingest",
621+
)
622+
exporter.export([cast(Any, DummyItem())])
623+
595624
sent_payload = mock_client.return_value.post.call_args.kwargs["json"]["data"][0]
596625
assert sent_payload["span_data"]["usage"] == {"requests": 1}
597626
exporter.close()

0 commit comments

Comments
 (0)