Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ee55a3a
feat(openai-agents): use upstream gen_ai constants directly, rename L…
max-deygin-traceloop Mar 18, 2026
e1fb150
fix: remove duplicate GEN_AI_OPERATION_NAME dict keys and migrate too…
max-deygin-traceloop Mar 18, 2026
cd88432
fix(openai-agents): migrate to GEN_AI_INPUT/OUTPUT_MESSAGES, fix brok…
max-deygin-traceloop Mar 18, 2026
1133ac6
fix(openai-agents): update recipe hierarchy test to use JSON array me…
max-deygin-traceloop Mar 18, 2026
a763e85
fix(openai-agents): migrate realtime flat format to JSON arrays, remo…
max-deygin-traceloop Mar 18, 2026
2f6394c
fix(openai-agents): restore Speech/Transcription/SpeechGroup handlers…
max-deygin-traceloop Mar 18, 2026
741bdce
Improving coverage
max-deygin-traceloop Mar 31, 2026
2021b3e
fixed finish_reason assertion
max-deygin-traceloop Mar 31, 2026
826a2a7
Minor fixes
max-deygin-traceloop Mar 31, 2026
50bfc23
added gen_ai.tool.call responce/request
max-deygin-traceloop Mar 31, 2026
6ea5fc0
adjust test
max-deygin-traceloop Mar 31, 2026
4f95dfe
test additions
max-deygin-traceloop Apr 12, 2026
a98c409
Merge branch 'main' into max/tlp-1928-openai-agents-insturmentation
max-deygin-traceloop Apr 12, 2026
f2e7241
Merge remote-tracking branch 'origin/main' into max/tlp-1928-openai-a…
max-deygin-traceloop Apr 15, 2026
1a0bc38
fix review comments
max-deygin-traceloop Apr 16, 2026
ca942bc
Merge remote-tracking branch 'origin/main' into max/tlp-1928-openai-a…
max-deygin-traceloop Apr 19, 2026
aa8707c
semconv final adjustments
max-deygin-traceloop Apr 19, 2026
b6b02aa
Review comments #2
max-deygin-traceloop Apr 19, 2026
8190972
missing tests
max-deygin-traceloop Apr 20, 2026
6b78202
bump
max-deygin-traceloop Apr 20, 2026
4bcca80
review fixes
max-deygin-traceloop Apr 27, 2026
cb10561
lint
max-deygin-traceloop Apr 27, 2026
1beb398
Merge branch 'main' into max/tlp-1928-openai-agents-insturmentation
max-deygin-traceloop Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
so we need to patch the RealtimeSession class directly to add OpenTelemetry tracing.
"""

import json
import logging
import time
from collections import OrderedDict
from typing import Dict, Any, Optional, List, Tuple
from opentelemetry.trace import Tracer, Status, StatusCode, SpanKind, Span
from opentelemetry.trace import set_span_in_context
Expand Down Expand Up @@ -108,18 +110,19 @@ def __init__(self, tracer: Tracer):
self.prompt_agent_name: Optional[str] = None
self.starting_agent_name: Optional[str] = None
self.model_name: str = "gpt-4o-realtime-preview"
self.seen_completions: set = set()
self._seen_completions: OrderedDict = OrderedDict()
self._seen_completions_max: int = 1000
self.pending_usage: Optional[Dict[str, int]] = None

def start_workflow_span(self, agent_name: str):
"""Start the root workflow span for the session."""
self.starting_agent_name = agent_name
self.workflow_span = self.tracer.start_span(
"Realtime Session",
kind=SpanKind.CLIENT,
kind=SpanKind.INTERNAL,
attributes={
SpanAttributes.TRACELOOP_SPAN_KIND: TraceloopSpanKindValues.WORKFLOW.value,
GenAIAttributes.GEN_AI_SYSTEM: "openai_agents",
GenAIAttributes.GEN_AI_PROVIDER_NAME: "openai",
SpanAttributes.TRACELOOP_WORKFLOW_NAME: "Realtime Session",
},
)
Expand Down Expand Up @@ -170,12 +173,13 @@ def start_agent_span(self, agent_name: str):

span = self.tracer.start_span(
f"{agent_name}.agent",
kind=SpanKind.CLIENT,
kind=SpanKind.INTERNAL,
context=parent_context,
attributes={
SpanAttributes.TRACELOOP_SPAN_KIND: TraceloopSpanKindValues.AGENT.value,
GenAIAttributes.GEN_AI_AGENT_NAME: agent_name,
GenAIAttributes.GEN_AI_SYSTEM: "openai_agents",
GenAIAttributes.GEN_AI_PROVIDER_NAME: "openai",
GenAIAttributes.GEN_AI_OPERATION_NAME: "invoke_agent",
},
)
self.agent_spans[agent_name] = span
Expand All @@ -202,7 +206,8 @@ def start_tool_span(self, tool_name: str, agent_name: Optional[str] = None):
SpanAttributes.TRACELOOP_SPAN_KIND: TraceloopSpanKindValues.TOOL.value,
GenAIAttributes.GEN_AI_TOOL_NAME: tool_name,
GenAIAttributes.GEN_AI_TOOL_TYPE: "function",
GenAIAttributes.GEN_AI_SYSTEM: "openai_agents",
GenAIAttributes.GEN_AI_PROVIDER_NAME: "openai",
GenAIAttributes.GEN_AI_OPERATION_NAME: "execute_tool",
},
)
self.tool_spans[tool_name] = span
Expand All @@ -214,8 +219,9 @@ def end_tool_span(
"""End a tool span."""
if tool_name in self.tool_spans:
span = self.tool_spans[tool_name]
if output is not None:
span.set_attribute(GenAIAttributes.GEN_AI_TOOL_CALL_RESULT, str(output))
if output is not None and should_send_prompts():
result = output if isinstance(output, str) else json.dumps(output, default=str)
span.set_attribute(GenAIAttributes.GEN_AI_TOOL_CALL_RESULT, result)
if error:
span.set_status(Status(StatusCode.ERROR, str(error)))
else:
Expand All @@ -239,7 +245,8 @@ def create_handoff_span(self, from_agent: str, to_agent: str):
context=parent_context,
attributes={
SpanAttributes.TRACELOOP_SPAN_KIND: "handoff",
GenAIAttributes.GEN_AI_SYSTEM: "openai_agents",
GenAIAttributes.GEN_AI_PROVIDER_NAME: "openai",
GenAIAttributes.GEN_AI_OPERATION_NAME: "handoff",
GEN_AI_HANDOFF_FROM_AGENT: from_agent,
GEN_AI_HANDOFF_TO_AGENT: to_agent,
},
Expand All @@ -258,8 +265,8 @@ def start_audio_span(self, item_id: str, content_index: int):
kind=SpanKind.CLIENT,
context=parent_context,
attributes={
SpanAttributes.LLM_REQUEST_TYPE: "realtime",
GenAIAttributes.GEN_AI_SYSTEM: "openai",
GenAIAttributes.GEN_AI_OPERATION_NAME: "realtime",
GenAIAttributes.GEN_AI_PROVIDER_NAME: "openai",
},
)
Comment thread
max-deygin-traceloop marked this conversation as resolved.
self.audio_spans[span_key] = span
Expand Down Expand Up @@ -309,14 +316,20 @@ def record_usage(self, usage: Any):
"total_tokens": getattr(usage, "total_tokens", 0) or 0,
}

@property
def seen_completions(self):
return self._seen_completions

def record_completion(self, role: str, content: str):
"""Record a completion message - creates an LLM span with prompt and completion."""
if not content:
return
content_hash = hash(content[:100])
if content_hash in self.seen_completions:
content_hash = hash(content)
if content_hash in self._seen_completions:
return
self.seen_completions.add(content_hash)
self._seen_completions[content_hash] = None
if len(self._seen_completions) > self._seen_completions_max:
self._seen_completions.popitem(last=False)
self.create_llm_span(content)
Comment thread
max-deygin-traceloop marked this conversation as resolved.

def create_llm_span(self, completion_content: str):
Expand Down Expand Up @@ -351,13 +364,16 @@ def create_llm_span(self, completion_content: str):
context=parent_context,
start_time=start_time,
attributes={
SpanAttributes.LLM_REQUEST_TYPE: "realtime",
SpanAttributes.LLM_SYSTEM: "openai",
GenAIAttributes.GEN_AI_SYSTEM: "openai",
GenAIAttributes.GEN_AI_OPERATION_NAME: "realtime",
GenAIAttributes.GEN_AI_PROVIDER_NAME: "openai",
GenAIAttributes.GEN_AI_REQUEST_MODEL: model_name_str,
},
)

span.set_attribute(
GenAIAttributes.GEN_AI_RESPONSE_MODEL, model_name_str,
)

if self.pending_usage:
if self.pending_usage.get("input_tokens"):
span.set_attribute(
Expand All @@ -369,27 +385,38 @@ def create_llm_span(self, completion_content: str):
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
self.pending_usage["output_tokens"],
)
if self.pending_usage.get("total_tokens"):
span.set_attribute(
SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS,
self.pending_usage["total_tokens"],
)
self.pending_usage = None

if should_send_prompts():
if prompt_content:
input_msg = {
"role": prompt_role or "user",
"parts": [{"type": "text", "content": prompt_content}],
}
span.set_attribute(
f"{GenAIAttributes.GEN_AI_PROMPT}.0.role", prompt_role or "user"
)
span.set_attribute(
f"{GenAIAttributes.GEN_AI_PROMPT}.0.content", prompt_content
GenAIAttributes.GEN_AI_INPUT_MESSAGES,
json.dumps([input_msg]),
)

out_msg = {
"role": "assistant",
"parts": [{"type": "text", "content": completion_content}],
"finish_reason": "",
}
span.set_attribute(
f"{GenAIAttributes.GEN_AI_COMPLETION}.0.role", "assistant"
)
span.set_attribute(
f"{GenAIAttributes.GEN_AI_COMPLETION}.0.content", completion_content
)
span.set_attribute(
f"{GenAIAttributes.GEN_AI_COMPLETION}.0.finish_reason", "stop"
GenAIAttributes.GEN_AI_OUTPUT_MESSAGES,
json.dumps([out_msg]),
)

# Realtime API does not provide finish reasons; set top-level
# attribute only when a meaningful value is available (consistent
# with _hooks.py which omits the attribute when mapped value is None).

span.set_status(Status(StatusCode.OK))
span.end()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# Handoff span attribute names
GEN_AI_HANDOFF_FROM_AGENT = "gen_ai.handoff.from_agent"
GEN_AI_HANDOFF_TO_AGENT = "gen_ai.handoff.to_agent"
GEN_AI_HANDOFF_PARENT_AGENT = "gen_ai.agent.handoff_parent"
OPENAI_AGENT_HANDOFFS = "openai.agent.handoffs"
_TRACELOOP_TRACE_CONTENT = "TRACELOOP_TRACE_CONTENT"


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ dev = [
]
test = [
"litellm>=1.71.2,<2",
"openai-agents>=0.6.9",
"openai-agents>=0.14.2",
"opentelemetry-sdk>=1.38.0,<2",
"pytest-asyncio>=1.0.0,<2",
"pytest-recording>=0.13.1,<0.14.0",
Expand Down
Loading
Loading