openlit 1.34.29__py3-none-any.whl → 1.34.31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. openlit/__helpers.py +235 -86
  2. openlit/__init__.py +16 -13
  3. openlit/_instrumentors.py +2 -1
  4. openlit/evals/all.py +50 -21
  5. openlit/evals/bias_detection.py +47 -20
  6. openlit/evals/hallucination.py +53 -22
  7. openlit/evals/toxicity.py +50 -21
  8. openlit/evals/utils.py +54 -30
  9. openlit/guard/all.py +61 -19
  10. openlit/guard/prompt_injection.py +34 -14
  11. openlit/guard/restrict_topic.py +46 -15
  12. openlit/guard/sensitive_topic.py +34 -14
  13. openlit/guard/utils.py +58 -22
  14. openlit/instrumentation/ag2/__init__.py +24 -8
  15. openlit/instrumentation/ag2/ag2.py +34 -13
  16. openlit/instrumentation/ag2/async_ag2.py +34 -13
  17. openlit/instrumentation/ag2/utils.py +133 -30
  18. openlit/instrumentation/ai21/__init__.py +43 -14
  19. openlit/instrumentation/ai21/ai21.py +47 -21
  20. openlit/instrumentation/ai21/async_ai21.py +47 -21
  21. openlit/instrumentation/ai21/utils.py +299 -78
  22. openlit/instrumentation/anthropic/__init__.py +21 -4
  23. openlit/instrumentation/anthropic/anthropic.py +28 -17
  24. openlit/instrumentation/anthropic/async_anthropic.py +28 -17
  25. openlit/instrumentation/anthropic/utils.py +145 -35
  26. openlit/instrumentation/assemblyai/__init__.py +11 -2
  27. openlit/instrumentation/assemblyai/assemblyai.py +15 -4
  28. openlit/instrumentation/assemblyai/utils.py +120 -25
  29. openlit/instrumentation/astra/__init__.py +43 -10
  30. openlit/instrumentation/astra/astra.py +28 -5
  31. openlit/instrumentation/astra/async_astra.py +28 -5
  32. openlit/instrumentation/astra/utils.py +151 -55
  33. openlit/instrumentation/azure_ai_inference/__init__.py +43 -10
  34. openlit/instrumentation/azure_ai_inference/async_azure_ai_inference.py +53 -21
  35. openlit/instrumentation/azure_ai_inference/azure_ai_inference.py +53 -21
  36. openlit/instrumentation/azure_ai_inference/utils.py +307 -83
  37. openlit/instrumentation/bedrock/__init__.py +21 -4
  38. openlit/instrumentation/bedrock/bedrock.py +63 -25
  39. openlit/instrumentation/bedrock/utils.py +139 -30
  40. openlit/instrumentation/chroma/__init__.py +89 -16
  41. openlit/instrumentation/chroma/chroma.py +28 -6
  42. openlit/instrumentation/chroma/utils.py +167 -51
  43. openlit/instrumentation/cohere/__init__.py +63 -18
  44. openlit/instrumentation/cohere/async_cohere.py +63 -24
  45. openlit/instrumentation/cohere/cohere.py +63 -24
  46. openlit/instrumentation/cohere/utils.py +286 -73
  47. openlit/instrumentation/controlflow/__init__.py +35 -9
  48. openlit/instrumentation/controlflow/controlflow.py +66 -33
  49. openlit/instrumentation/crawl4ai/__init__.py +25 -10
  50. openlit/instrumentation/crawl4ai/async_crawl4ai.py +78 -31
  51. openlit/instrumentation/crawl4ai/crawl4ai.py +78 -31
  52. openlit/instrumentation/crewai/__init__.py +111 -24
  53. openlit/instrumentation/crewai/async_crewai.py +114 -0
  54. openlit/instrumentation/crewai/crewai.py +104 -131
  55. openlit/instrumentation/crewai/utils.py +615 -0
  56. openlit/instrumentation/dynamiq/__init__.py +46 -12
  57. openlit/instrumentation/dynamiq/dynamiq.py +74 -33
  58. openlit/instrumentation/elevenlabs/__init__.py +23 -4
  59. openlit/instrumentation/elevenlabs/async_elevenlabs.py +16 -4
  60. openlit/instrumentation/elevenlabs/elevenlabs.py +16 -4
  61. openlit/instrumentation/elevenlabs/utils.py +128 -25
  62. openlit/instrumentation/embedchain/__init__.py +11 -2
  63. openlit/instrumentation/embedchain/embedchain.py +68 -35
  64. openlit/instrumentation/firecrawl/__init__.py +24 -7
  65. openlit/instrumentation/firecrawl/firecrawl.py +46 -20
  66. openlit/instrumentation/google_ai_studio/__init__.py +45 -10
  67. openlit/instrumentation/google_ai_studio/async_google_ai_studio.py +67 -44
  68. openlit/instrumentation/google_ai_studio/google_ai_studio.py +67 -44
  69. openlit/instrumentation/google_ai_studio/utils.py +180 -67
  70. openlit/instrumentation/gpt4all/__init__.py +22 -7
  71. openlit/instrumentation/gpt4all/gpt4all.py +67 -29
  72. openlit/instrumentation/gpt4all/utils.py +285 -61
  73. openlit/instrumentation/gpu/__init__.py +128 -47
  74. openlit/instrumentation/groq/__init__.py +21 -4
  75. openlit/instrumentation/groq/async_groq.py +33 -21
  76. openlit/instrumentation/groq/groq.py +33 -21
  77. openlit/instrumentation/groq/utils.py +192 -55
  78. openlit/instrumentation/haystack/__init__.py +70 -24
  79. openlit/instrumentation/haystack/async_haystack.py +28 -6
  80. openlit/instrumentation/haystack/haystack.py +28 -6
  81. openlit/instrumentation/haystack/utils.py +196 -74
  82. openlit/instrumentation/julep/__init__.py +69 -19
  83. openlit/instrumentation/julep/async_julep.py +53 -27
  84. openlit/instrumentation/julep/julep.py +53 -28
  85. openlit/instrumentation/langchain/__init__.py +74 -63
  86. openlit/instrumentation/langchain/callback_handler.py +1100 -0
  87. openlit/instrumentation/langchain_community/__init__.py +13 -2
  88. openlit/instrumentation/langchain_community/async_langchain_community.py +23 -5
  89. openlit/instrumentation/langchain_community/langchain_community.py +23 -5
  90. openlit/instrumentation/langchain_community/utils.py +35 -9
  91. openlit/instrumentation/letta/__init__.py +68 -15
  92. openlit/instrumentation/letta/letta.py +99 -54
  93. openlit/instrumentation/litellm/__init__.py +43 -14
  94. openlit/instrumentation/litellm/async_litellm.py +51 -26
  95. openlit/instrumentation/litellm/litellm.py +51 -26
  96. openlit/instrumentation/litellm/utils.py +312 -101
  97. openlit/instrumentation/llamaindex/__init__.py +267 -90
  98. openlit/instrumentation/llamaindex/async_llamaindex.py +28 -6
  99. openlit/instrumentation/llamaindex/llamaindex.py +28 -6
  100. openlit/instrumentation/llamaindex/utils.py +204 -91
  101. openlit/instrumentation/mem0/__init__.py +11 -2
  102. openlit/instrumentation/mem0/mem0.py +50 -29
  103. openlit/instrumentation/milvus/__init__.py +10 -2
  104. openlit/instrumentation/milvus/milvus.py +31 -6
  105. openlit/instrumentation/milvus/utils.py +166 -67
  106. openlit/instrumentation/mistral/__init__.py +63 -18
  107. openlit/instrumentation/mistral/async_mistral.py +63 -24
  108. openlit/instrumentation/mistral/mistral.py +63 -24
  109. openlit/instrumentation/mistral/utils.py +277 -69
  110. openlit/instrumentation/multion/__init__.py +69 -19
  111. openlit/instrumentation/multion/async_multion.py +57 -26
  112. openlit/instrumentation/multion/multion.py +57 -26
  113. openlit/instrumentation/ollama/__init__.py +39 -18
  114. openlit/instrumentation/ollama/async_ollama.py +57 -26
  115. openlit/instrumentation/ollama/ollama.py +57 -26
  116. openlit/instrumentation/ollama/utils.py +226 -50
  117. openlit/instrumentation/openai/__init__.py +156 -32
  118. openlit/instrumentation/openai/async_openai.py +147 -67
  119. openlit/instrumentation/openai/openai.py +150 -67
  120. openlit/instrumentation/openai/utils.py +660 -186
  121. openlit/instrumentation/openai_agents/__init__.py +6 -2
  122. openlit/instrumentation/openai_agents/processor.py +409 -537
  123. openlit/instrumentation/phidata/__init__.py +13 -5
  124. openlit/instrumentation/phidata/phidata.py +67 -32
  125. openlit/instrumentation/pinecone/__init__.py +48 -9
  126. openlit/instrumentation/pinecone/async_pinecone.py +27 -5
  127. openlit/instrumentation/pinecone/pinecone.py +27 -5
  128. openlit/instrumentation/pinecone/utils.py +153 -47
  129. openlit/instrumentation/premai/__init__.py +22 -7
  130. openlit/instrumentation/premai/premai.py +51 -26
  131. openlit/instrumentation/premai/utils.py +246 -59
  132. openlit/instrumentation/pydantic_ai/__init__.py +49 -22
  133. openlit/instrumentation/pydantic_ai/pydantic_ai.py +69 -16
  134. openlit/instrumentation/pydantic_ai/utils.py +89 -24
  135. openlit/instrumentation/qdrant/__init__.py +19 -4
  136. openlit/instrumentation/qdrant/async_qdrant.py +33 -7
  137. openlit/instrumentation/qdrant/qdrant.py +33 -7
  138. openlit/instrumentation/qdrant/utils.py +228 -93
  139. openlit/instrumentation/reka/__init__.py +23 -10
  140. openlit/instrumentation/reka/async_reka.py +17 -11
  141. openlit/instrumentation/reka/reka.py +17 -11
  142. openlit/instrumentation/reka/utils.py +138 -36
  143. openlit/instrumentation/together/__init__.py +44 -12
  144. openlit/instrumentation/together/async_together.py +50 -27
  145. openlit/instrumentation/together/together.py +50 -27
  146. openlit/instrumentation/together/utils.py +301 -71
  147. openlit/instrumentation/transformers/__init__.py +2 -1
  148. openlit/instrumentation/transformers/transformers.py +13 -3
  149. openlit/instrumentation/transformers/utils.py +139 -36
  150. openlit/instrumentation/vertexai/__init__.py +81 -16
  151. openlit/instrumentation/vertexai/async_vertexai.py +33 -15
  152. openlit/instrumentation/vertexai/utils.py +123 -27
  153. openlit/instrumentation/vertexai/vertexai.py +33 -15
  154. openlit/instrumentation/vllm/__init__.py +12 -5
  155. openlit/instrumentation/vllm/utils.py +121 -31
  156. openlit/instrumentation/vllm/vllm.py +16 -10
  157. openlit/otel/events.py +35 -10
  158. openlit/otel/metrics.py +32 -24
  159. openlit/otel/tracing.py +24 -9
  160. openlit/semcov/__init__.py +101 -7
  161. {openlit-1.34.29.dist-info → openlit-1.34.31.dist-info}/METADATA +2 -1
  162. openlit-1.34.31.dist-info/RECORD +166 -0
  163. openlit/instrumentation/langchain/async_langchain.py +0 -102
  164. openlit/instrumentation/langchain/langchain.py +0 -102
  165. openlit/instrumentation/langchain/utils.py +0 -252
  166. openlit-1.34.29.dist-info/RECORD +0 -166
  167. {openlit-1.34.29.dist-info → openlit-1.34.31.dist-info}/LICENSE +0 -0
  168. {openlit-1.34.29.dist-info → openlit-1.34.31.dist-info}/WHEEL +0 -0
@@ -2,26 +2,22 @@
2
2
  OpenLIT OpenAI Agents Instrumentation - Native TracingProcessor Implementation
3
3
  """
4
4
 
5
- import json
6
5
  import time
7
- from datetime import datetime
8
- from typing import Any, Dict, Optional, TYPE_CHECKING
6
+ from typing import Any, Dict, TYPE_CHECKING
9
7
 
10
- from opentelemetry import context as context_api
11
8
  from opentelemetry.trace import SpanKind, Status, StatusCode, set_span_in_context
12
- from opentelemetry.context import detach
13
9
 
14
10
  from openlit.__helpers import (
15
11
  common_framework_span_attributes,
16
12
  handle_exception,
17
- record_framework_metrics,
18
- get_chat_model_cost
13
+ get_chat_model_cost,
19
14
  )
20
15
  from openlit.semcov import SemanticConvention
21
16
 
22
17
  # Try to import agents framework components with fallback
23
18
  try:
24
19
  from agents import TracingProcessor
20
+
25
21
  if TYPE_CHECKING:
26
22
  from agents import Trace, Span
27
23
  TRACING_AVAILABLE = True
@@ -48,553 +44,429 @@ except ImportError:
48
44
 
49
45
  class OpenLITTracingProcessor(TracingProcessor):
50
46
  """
51
- OpenLIT processor that integrates with OpenAI Agents' native tracing system
52
- Provides superior business intelligence while maintaining perfect hierarchy
47
+ OpenAI Agents tracing processor that integrates with OpenLIT observability.
48
+
49
+ This processor enhances OpenAI Agents' native tracing system with OpenLIT's
50
+ comprehensive observability features including business intelligence,
51
+ cost tracking, and performance metrics.
53
52
  """
54
53
 
55
- def __init__(self, tracer: Any, version: str, environment: str,
56
- application_name: str, pricing_info: dict, capture_message_content: bool,
57
- metrics: Optional[Any], disable_metrics: bool, detailed_tracing: bool):
58
- if not TRACING_AVAILABLE:
59
- return
60
-
61
- self._tracer = tracer
62
- self._version = version
63
- self._environment = environment
64
- self._application_name = application_name
65
- self._pricing_info = pricing_info
66
- self._capture_message_content = capture_message_content
67
- self._metrics = metrics
68
- self._disable_metrics = disable_metrics
69
- self._detailed_tracing = detailed_tracing
70
-
71
- # Track spans for hierarchy
72
- self._root_spans: Dict[str, Any] = {}
73
- self._otel_spans: Dict[str, Any] = {}
74
- self._tokens: Dict[str, object] = {}
75
- self._span_start_times: Dict[str, float] = {}
76
-
77
- # Track handoff context for better span naming
78
- self._last_handoff_from: Optional[str] = None
79
-
80
- def on_trace_start(self, trace: "Trace") -> None:
81
- """Called when a trace is started - creates root workflow span"""
82
- if not TRACING_AVAILABLE:
83
- return
84
-
85
- # Create root workflow span with {operation_type} {operation_name} format
86
- workflow_name = getattr(trace, 'name', 'workflow')
87
- span_name = f"agent {workflow_name}" # Follow {operation_type} {operation_name} pattern
88
-
89
- # Use tracer.start_span for TracingProcessor pattern with proper context
90
- otel_span = self._tracer.start_span(
91
- name=span_name,
92
- kind=SpanKind.CLIENT
93
- )
94
-
95
- # Set common framework attributes for root span
96
- self._set_common_attributes(otel_span, trace.trace_id)
97
-
98
- # Set agent name for root span using semantic conventions
99
- if hasattr(trace, 'name') and trace.name:
100
- otel_span.set_attribute(SemanticConvention.GEN_AI_AGENT_NAME, trace.name)
101
-
102
- # Set default model for root span
103
- otel_span.set_attribute(SemanticConvention.GEN_AI_REQUEST_MODEL, "gpt-4o")
104
-
105
- self._root_spans[trace.trace_id] = otel_span
106
- self._span_start_times[trace.trace_id] = time.time()
107
-
108
- def on_span_start(self, span: "Span[Any]") -> None:
109
- """Called when a span is started - creates child spans with proper hierarchy"""
110
- if not TRACING_AVAILABLE or not hasattr(span, 'started_at') or not span.started_at:
111
- return
112
-
113
- start_time = self._parse_timestamp(span.started_at)
114
-
115
- # Determine parent span for proper hierarchy
116
- parent_span = None
117
- if span.parent_id and span.parent_id in self._otel_spans:
118
- parent_span = self._otel_spans[span.parent_id]
119
- elif span.trace_id in self._root_spans:
120
- parent_span = self._root_spans[span.trace_id]
121
-
122
- # Set context for parent-child relationship
123
- context = set_span_in_context(parent_span) if parent_span else None
124
-
125
- # Get semantic span name and operation type
126
- span_name = self._get_span_name(span)
127
- operation_type = self._get_operation_type(span.span_data)
128
-
129
- # Create span with proper context
130
- otel_span = self._tracer.start_span(
131
- name=span_name,
132
- context=context,
133
- start_time=self._as_utc_nano(start_time),
134
- kind=SpanKind.CLIENT
135
- )
136
-
137
- # Set common framework attributes for all spans
138
- self._set_common_framework_attributes(otel_span, operation_type)
139
-
140
- # Set span-specific attributes
141
- self._set_span_attributes(otel_span, span)
142
-
143
- # Track span and context
144
- self._otel_spans[span.span_id] = otel_span
145
- self._tokens[span.span_id] = context_api.attach(set_span_in_context(otel_span))
146
- self._span_start_times[span.span_id] = time.time()
147
-
148
- def on_span_end(self, span: "Span[Any]") -> None:
149
- """Called when a span is finished - adds business intelligence and ends span"""
150
- if not TRACING_AVAILABLE or span.span_id not in self._otel_spans:
151
- return
152
-
153
- otel_span = self._otel_spans[span.span_id]
54
+ def __init__(
55
+ self,
56
+ tracer,
57
+ version,
58
+ environment,
59
+ application_name,
60
+ pricing_info,
61
+ capture_message_content,
62
+ metrics,
63
+ disable_metrics,
64
+ detailed_tracing,
65
+ **kwargs,
66
+ ):
67
+ """Initialize the OpenLIT tracing processor."""
68
+ super().__init__()
69
+
70
+ # Core configuration
71
+ self.tracer = tracer
72
+ self.version = version
73
+ self.environment = environment
74
+ self.application_name = application_name
75
+ self.pricing_info = pricing_info
76
+ self.capture_message_content = capture_message_content
77
+ self.metrics = metrics
78
+ self.disable_metrics = disable_metrics
79
+ self.detailed_tracing = detailed_tracing
80
+
81
+ # Internal tracking
82
+ self.active_spans = {}
83
+ self.span_stack = []
84
+
85
+ def start_trace(self, trace_id: str, name: str, **kwargs):
86
+ """
87
+ Start a new trace with OpenLIT enhancements.
88
+
89
+ Args:
90
+ trace_id: Unique trace identifier
91
+ name: Trace name
92
+ **kwargs: Additional trace metadata
93
+ """
94
+ try:
95
+ # Generate span name using OpenTelemetry conventions
96
+ span_name = self._get_span_name(name, **kwargs)
97
+
98
+ # Start root span with OpenLIT context
99
+ span = self.tracer.start_as_current_span(
100
+ span_name,
101
+ kind=SpanKind.CLIENT,
102
+ attributes={
103
+ SemanticConvention.GEN_AI_SYSTEM: "openai_agents",
104
+ SemanticConvention.GEN_AI_OPERATION: SemanticConvention.GEN_AI_OPERATION_TYPE_WORKFLOW,
105
+ "trace.id": trace_id,
106
+ "trace.name": name,
107
+ },
108
+ )
109
+
110
+ # Create scope for common attributes
111
+ scope = type("GenericScope", (), {})()
112
+ scope._span = span # pylint: disable=protected-access
113
+ scope._start_time = time.time() # pylint: disable=protected-access
114
+ scope._end_time = None # pylint: disable=protected-access
115
+
116
+ # Apply common framework attributes
117
+ common_framework_span_attributes(
118
+ scope,
119
+ "openai_agents",
120
+ "api.openai.com",
121
+ 443,
122
+ self.environment,
123
+ self.application_name,
124
+ self.version,
125
+ name,
126
+ )
127
+
128
+ # Track active span
129
+ self.active_spans[trace_id] = span
130
+ self.span_stack.append(span)
131
+
132
+ return span
133
+
134
+ except Exception as e: # pylint: disable=broad-exception-caught
135
+ # Graceful degradation
136
+ handle_exception(None, e)
137
+ return None
138
+
139
+ def end_trace(self, trace_id: str, **kwargs):
140
+ """
141
+ End an active trace.
154
142
 
143
+ Args:
144
+ trace_id: Trace identifier to end
145
+ **kwargs: Additional metadata
146
+ """
155
147
  try:
156
- # Add response data and business intelligence
157
- self._process_span_completion(otel_span, span)
158
-
159
- # Set successful status
160
- otel_span.set_status(Status(StatusCode.OK))
161
-
162
- # Record metrics if enabled
163
- if not self._disable_metrics and self._metrics and span.span_id in self._span_start_times:
164
- start_time = self._span_start_times[span.span_id]
165
- end_time = time.time()
166
- operation_type = self._get_operation_type(span.span_data)
167
- record_framework_metrics(
168
- self._metrics, operation_type, SemanticConvention.GEN_AI_SYSTEM_OPENAI_AGENTS,
169
- "localhost", 80, self._environment, self._application_name,
170
- start_time, end_time
148
+ span = self.active_spans.get(trace_id)
149
+ if span:
150
+ # Set final attributes and status
151
+ span.set_status(Status(StatusCode.OK))
152
+
153
+ # End span
154
+ span.end()
155
+
156
+ # Cleanup tracking
157
+ if trace_id in self.active_spans:
158
+ del self.active_spans[trace_id]
159
+ if span in self.span_stack:
160
+ self.span_stack.remove(span)
161
+
162
+ except Exception as e: # pylint: disable=broad-exception-caught
163
+ handle_exception(span if span else None, e)
164
+
165
+ def _get_span_name(self, operation_name: str, **metadata) -> str:
166
+ """
167
+ Generate OpenTelemetry-compliant span names.
168
+
169
+ Args:
170
+ operation_name: Base operation name
171
+ **metadata: Additional context for naming
172
+
173
+ Returns:
174
+ Formatted span name following semantic conventions
175
+ """
176
+ # Extract context for naming
177
+ agent_name = metadata.get("agent_name", "")
178
+ model_name = metadata.get("model_name", "")
179
+ tool_name = metadata.get("tool_name", "")
180
+ workflow_name = metadata.get("workflow_name", "")
181
+
182
+ # Apply OpenTelemetry semantic conventions for GenAI agents
183
+ if "agent" in operation_name.lower():
184
+ if agent_name:
185
+ return f"invoke_agent {agent_name}"
186
+ return "invoke_agent"
187
+ if "chat" in operation_name.lower():
188
+ if model_name:
189
+ return f"chat {model_name}"
190
+ return "chat response"
191
+ if "tool" in operation_name.lower():
192
+ if tool_name:
193
+ return f"execute_tool {tool_name}"
194
+ return "execute_tool"
195
+ if "handoff" in operation_name.lower():
196
+ target_agent = metadata.get("target_agent", "unknown")
197
+ return f"invoke_agent {target_agent}"
198
+ if "workflow" in operation_name.lower():
199
+ if workflow_name:
200
+ return f"workflow {workflow_name}"
201
+ return "workflow"
202
+
203
+ # Default case
204
+ return operation_name
205
+
206
+ def span_start(self, span_data, trace_id: str):
207
+ """
208
+ Handle span start events from OpenAI Agents.
209
+
210
+ Args:
211
+ span_data: Span data from agents framework
212
+ trace_id: Associated trace identifier
213
+ """
214
+ try:
215
+ # Extract span information
216
+ span_name = getattr(span_data, "name", "unknown_operation")
217
+ span_type = getattr(span_data, "type", "unknown")
218
+
219
+ # Generate enhanced span name
220
+ enhanced_name = self._get_span_name(
221
+ span_name,
222
+ agent_name=getattr(span_data, "agent_name", None),
223
+ model_name=getattr(span_data, "model_name", None),
224
+ tool_name=getattr(span_data, "tool_name", None),
225
+ )
226
+
227
+ # Determine span operation type
228
+ operation_type = self._get_operation_type(span_type, span_name)
229
+
230
+ # Start span with proper context
231
+ parent_span = self.span_stack[-1] if self.span_stack else None
232
+ context = set_span_in_context(parent_span) if parent_span else None
233
+
234
+ span = self.tracer.start_as_current_span(
235
+ enhanced_name,
236
+ kind=SpanKind.CLIENT,
237
+ context=context,
238
+ attributes={
239
+ SemanticConvention.GEN_AI_SYSTEM: "openai_agents",
240
+ SemanticConvention.GEN_AI_OPERATION: operation_type,
241
+ "span.type": span_type,
242
+ "span.id": getattr(span_data, "span_id", ""),
243
+ },
244
+ )
245
+
246
+ # Process specific span types
247
+ self._process_span_attributes(span, span_data, span_type)
248
+
249
+ # Track span
250
+ span_id = getattr(span_data, "span_id", len(self.span_stack))
251
+ self.active_spans[f"{trace_id}:{span_id}"] = span
252
+ self.span_stack.append(span)
253
+
254
+ except Exception as e: # pylint: disable=broad-exception-caught
255
+ handle_exception(None, e)
256
+
257
+ def _get_operation_type(self, span_type: str, span_name: str) -> str:
258
+ """Get operation type based on span characteristics."""
259
+ type_mapping = {
260
+ "agent": SemanticConvention.GEN_AI_OPERATION_TYPE_AGENT,
261
+ "generation": SemanticConvention.GEN_AI_OPERATION_CHAT,
262
+ "function": SemanticConvention.GEN_AI_OPERATION_CHAT,
263
+ "tool": SemanticConvention.GEN_AI_OPERATION_CHAT,
264
+ "handoff": SemanticConvention.GEN_AI_OPERATION_TYPE_AGENT,
265
+ }
266
+
267
+ # Check span type first
268
+ for key, operation in type_mapping.items():
269
+ if key in span_type.lower():
270
+ return operation
271
+
272
+ # Check span name
273
+ for key, operation in type_mapping.items():
274
+ if key in span_name.lower():
275
+ return operation
276
+
277
+ return SemanticConvention.GEN_AI_OPERATION_CHAT
278
+
279
+ def _process_span_attributes(self, span, span_data, span_type: str):
280
+ """Process and set span attributes based on span type."""
281
+ try:
282
+ # Common attributes
283
+ if hasattr(span_data, "agent_name"):
284
+ span.set_attribute(
285
+ SemanticConvention.GEN_AI_AGENT_NAME, span_data.agent_name
171
286
  )
172
287
 
173
- except Exception as e:
174
- handle_exception(otel_span, e)
175
- finally:
176
- # End span and cleanup
177
- otel_span.end()
288
+ if hasattr(span_data, "model_name"):
289
+ span.set_attribute(
290
+ SemanticConvention.GEN_AI_REQUEST_MODEL, span_data.model_name
291
+ )
178
292
 
179
- # Cleanup context
180
- if span.span_id in self._tokens:
181
- detach(self._tokens[span.span_id])
182
- del self._tokens[span.span_id]
293
+ # Agent-specific attributes
294
+ if span_type == "agent":
295
+ self._process_agent_span(span, span_data)
296
+
297
+ # Generation-specific attributes
298
+ elif span_type == "generation":
299
+ self._process_generation_span(span, span_data)
300
+
301
+ # Function/Tool-specific attributes
302
+ elif span_type in ["function", "tool"]:
303
+ self._process_function_span(span, span_data)
304
+
305
+ # Handoff-specific attributes
306
+ elif span_type == "handoff":
307
+ self._process_handoff_span(span, span_data)
308
+
309
+ except Exception as e: # pylint: disable=broad-exception-caught
310
+ handle_exception(span, e)
311
+
312
+ def _process_agent_span(self, span, agent_span):
313
+ """Process agent span data (unused parameter)."""
314
+ # Agent-specific processing
315
+ if hasattr(agent_span, "instructions"):
316
+ span.set_attribute(
317
+ SemanticConvention.GEN_AI_AGENT_DESCRIPTION,
318
+ str(agent_span.instructions)[:500],
319
+ )
320
+
321
+ if hasattr(agent_span, "model"):
322
+ span.set_attribute(
323
+ SemanticConvention.GEN_AI_REQUEST_MODEL, agent_span.model
324
+ )
325
+
326
+ def _process_generation_span(self, span, generation_span):
327
+ """Process generation span data."""
328
+ # Set generation-specific attributes
329
+ if hasattr(generation_span, "prompt"):
330
+ span.set_attribute(
331
+ SemanticConvention.GEN_AI_PROMPT, str(generation_span.prompt)[:1000]
332
+ )
333
+
334
+ if hasattr(generation_span, "completion"):
335
+ span.set_attribute(
336
+ SemanticConvention.GEN_AI_COMPLETION,
337
+ str(generation_span.completion)[:1000],
338
+ )
339
+
340
+ if hasattr(generation_span, "usage"):
341
+ usage = generation_span.usage
342
+ if hasattr(usage, "prompt_tokens"):
343
+ span.set_attribute(
344
+ SemanticConvention.GEN_AI_USAGE_PROMPT_TOKENS, usage.prompt_tokens
345
+ )
346
+ if hasattr(usage, "completion_tokens"):
347
+ span.set_attribute(
348
+ SemanticConvention.GEN_AI_USAGE_COMPLETION_TOKENS,
349
+ usage.completion_tokens,
350
+ )
183
351
 
184
- # Cleanup tracking
185
- if span.span_id in self._otel_spans:
186
- del self._otel_spans[span.span_id]
187
- if span.span_id in self._span_start_times:
188
- del self._span_start_times[span.span_id]
352
+ def _process_function_span(self, span, function_span):
353
+ """Process function/tool span data."""
354
+ if hasattr(function_span, "function_name"):
355
+ span.set_attribute(
356
+ SemanticConvention.GEN_AI_TOOL_NAME, function_span.function_name
357
+ )
189
358
 
190
- def on_trace_end(self, trace: "Trace") -> None:
191
- """Called when a trace is finished - ends root span with business intelligence"""
192
- if not TRACING_AVAILABLE or trace.trace_id not in self._root_spans:
193
- return
359
+ if hasattr(function_span, "arguments"):
360
+ span.set_attribute(
361
+ "gen_ai.tool.arguments", str(function_span.arguments)[:500]
362
+ )
194
363
 
195
- root_span = self._root_spans[trace.trace_id]
364
+ if hasattr(function_span, "result"):
365
+ span.set_attribute("gen_ai.tool.result", str(function_span.result)[:500])
196
366
 
197
- try:
198
- # Add trace-level business intelligence
199
- self._process_trace_completion(root_span, trace)
200
- root_span.set_status(Status(StatusCode.OK))
201
- except Exception as e:
202
- handle_exception(root_span, e)
203
- finally:
204
- root_span.end()
205
-
206
- # Cleanup
207
- if trace.trace_id in self._root_spans:
208
- del self._root_spans[trace.trace_id]
209
- if trace.trace_id in self._span_start_times:
210
- del self._span_start_times[trace.trace_id]
211
-
212
- def _get_span_name(self, span: "Span[Any]") -> str:
213
- """Get semantic span name using {operation_type} {operation_name} format"""
214
- data = span.span_data
215
- operation_type = self._get_operation_type(data)
216
-
217
- # Extract operation name based on span type
218
- operation_name = "unknown"
219
-
220
- # Special handling for handoffs
221
- if hasattr(data, '__class__') and data.__class__.__name__ == 'HandoffSpanData':
222
- if hasattr(data, 'to_agent') and data.to_agent:
223
- operation_name = f"to {data.to_agent}"
224
- else:
225
- operation_name = "handoff"
226
-
227
- # Use agent name for agent spans
228
- elif hasattr(data, '__class__') and data.__class__.__name__ == 'AgentSpanData':
229
- # Try multiple possible attribute names for agent name
230
- agent_name = None
231
-
232
- for attr in ['agent_name', 'name', 'agent', 'agent_id']:
233
- if hasattr(data, attr):
234
- agent_name = getattr(data, attr)
235
- if agent_name and isinstance(agent_name, str):
236
- break
237
-
238
- # If still no agent name, try looking in context or other attributes
239
- if not agent_name:
240
- # Try context or other nested attributes
241
- if hasattr(data, 'context') and hasattr(data.context, 'agent'):
242
- agent_name = getattr(data.context.agent, 'name', None)
243
- elif hasattr(data, 'metadata') and hasattr(data.metadata, 'agent_name'):
244
- agent_name = data.metadata.agent_name
367
+ def _process_handoff_span(self, span, handoff_span):
368
+ """Process handoff span data."""
369
+ if hasattr(handoff_span, "target_agent"):
370
+ span.set_attribute("gen_ai.handoff.target_agent", handoff_span.target_agent)
245
371
 
246
- if agent_name:
247
- operation_name = agent_name
248
- else:
249
- # If no agent name found, use a more descriptive fallback
250
- operation_name = "execution"
251
-
252
- # Use name if available for other spans
253
- elif hasattr(data, 'name') and isinstance(data.name, str):
254
- operation_name = data.name
255
-
256
- # Fallback to type-based names
257
- else:
258
- operation_name = getattr(data, 'type', 'operation')
259
-
260
- # Return formatted name: {operation_type} {operation_name}
261
- return f"{operation_type} {operation_name}"
262
-
263
- def _get_operation_type(self, data: Any) -> str:
264
- """Map span data to operation types"""
265
- class_name = data.__class__.__name__ if hasattr(data, '__class__') else str(type(data))
266
-
267
- mapping = {
268
- 'AgentSpanData': SemanticConvention.GEN_AI_OPERATION_TYPE_AGENT,
269
- 'GenerationSpanData': SemanticConvention.GEN_AI_OPERATION_TYPE_CHAT,
270
- 'FunctionSpanData': SemanticConvention.GEN_AI_OPERATION_TYPE_TOOLS,
271
- 'HandoffSpanData': SemanticConvention.GEN_AI_OPERATION_TYPE_AGENT,
272
- 'ResponseSpanData': SemanticConvention.GEN_AI_OPERATION_TYPE_CHAT,
273
- }
372
+ if hasattr(handoff_span, "reason"):
373
+ span.set_attribute("gen_ai.handoff.reason", str(handoff_span.reason)[:200])
274
374
 
275
- return mapping.get(class_name, SemanticConvention.GEN_AI_OPERATION_TYPE_FRAMEWORK)
276
-
277
- def _set_common_framework_attributes(self, span: Any, operation_type: str) -> None:
278
- """Set common framework attributes using semantic conventions"""
279
- # Create scope object for common_framework_span_attributes
280
- scope = type("GenericScope", (), {})()
281
- scope._span = span
282
- scope._start_time = time.time()
283
- scope._end_time = time.time()
284
-
285
- # Use common framework attributes helper
286
- # For framework operations, use localhost like other agent frameworks (AG2, Pydantic AI)
287
- common_framework_span_attributes(
288
- scope, SemanticConvention.GEN_AI_SYSTEM_OPENAI_AGENTS,
289
- "localhost", 80, self._environment, self._application_name,
290
- self._version, operation_type, None
291
- )
292
-
293
- def _set_common_attributes(self, span: Any, trace_id: str) -> None:
294
- """Set common framework attributes for root spans"""
295
- self._set_common_framework_attributes(span, SemanticConvention.GEN_AI_OPERATION_TYPE_FRAMEWORK)
296
-
297
- def _set_span_attributes(self, span: Any, agent_span: "Span[Any]") -> None:
298
- """Set span-specific attributes based on span data using semantic conventions"""
299
- data = agent_span.span_data
300
-
301
- # Agent-specific attributes using semantic conventions
302
- if hasattr(data, 'agent_name') and data.agent_name:
303
- span.set_attribute(SemanticConvention.GEN_AI_AGENT_NAME, data.agent_name)
304
- elif hasattr(data, 'name') and data.name:
305
- span.set_attribute(SemanticConvention.GEN_AI_AGENT_NAME, data.name)
306
-
307
- # Enhanced model information extraction
308
- model = self._extract_model_info(data, agent_span)
309
- if model:
310
- span.set_attribute(SemanticConvention.GEN_AI_REQUEST_MODEL, str(model))
311
-
312
- # Enhanced input/output capture with MIME types (OpenLIT enhancement)
313
- if self._capture_message_content:
314
- self._capture_input_output(span, data)
315
-
316
- # Enhanced token usage details (inspired by OpenInference)
317
- self._capture_detailed_token_usage(span, data)
318
-
319
- # Model invocation parameters as JSON (new feature from OpenInference)
320
- self._capture_model_parameters(span, data)
321
-
322
- # Tool/function information for tool calls
323
- if hasattr(data, '__class__') and 'Function' in data.__class__.__name__:
324
- if hasattr(data, 'function_name'):
325
- span.set_attribute(SemanticConvention.GEN_AI_TOOL_NAME, data.function_name)
326
- if hasattr(data, 'arguments'):
327
- span.set_attribute(SemanticConvention.GEN_AI_TOOL_ARGS, str(data.arguments))
328
-
329
- # Enhanced handoff information extraction
330
- if hasattr(data, '__class__') and 'Handoff' in data.__class__.__name__:
331
- target_agent = self._extract_handoff_target(data, agent_span)
332
- if target_agent:
333
- span.set_attribute(SemanticConvention.GEN_AI_AGENT_NAME, target_agent)
334
- else:
335
- # Fallback for handoff spans without clear target
336
- span.set_attribute(SemanticConvention.GEN_AI_AGENT_NAME, "agent handoff")
337
-
338
- # Request/response IDs if available
339
- if hasattr(data, 'request_id'):
340
- span.set_attribute(SemanticConvention.GEN_AI_RESPONSE_ID, data.request_id)
341
- elif hasattr(data, 'response_id'):
342
- span.set_attribute(SemanticConvention.GEN_AI_RESPONSE_ID, data.response_id)
343
-
344
- def _extract_model_info(self, data: Any, agent_span: "Span[Any]") -> Optional[str]:
345
- """Extract model information from span data or agent configuration"""
346
- # Try direct model attributes first
347
- model_attrs = ['model', 'model_name', 'model_id', 'llm_model', 'openai_model']
348
-
349
- model = self._check_model_attrs(data, model_attrs)
350
- if model:
351
- return model
352
-
353
- # Try nested configuration objects
354
- config_attrs = ['config', 'configuration', 'client_config', 'llm_config']
355
- model = self._check_config_model_attrs(data, config_attrs, model_attrs)
356
- if model:
357
- return model
358
-
359
- # Try looking in the agent span itself
360
- if hasattr(agent_span, 'model'):
361
- return str(agent_span.model)
362
-
363
- # Try agent_config if available
364
- if hasattr(agent_span, 'agent_config'):
365
- model = self._check_model_attrs(agent_span.agent_config, model_attrs)
366
- if model:
367
- return model
368
-
369
- # Default fallback
370
- return "gpt-4o"
371
-
372
- def _check_model_attrs(self, obj: Any, model_attrs: list) -> Optional[str]:
373
- """Helper method to check model attributes on an object"""
374
- for attr in model_attrs:
375
- if not hasattr(obj, attr):
376
- continue
377
- model_value = getattr(obj, attr)
378
- if model_value and isinstance(model_value, str):
379
- return model_value
380
- return None
381
-
382
- def _check_config_model_attrs(self, data: Any, config_attrs: list, model_attrs: list) -> Optional[str]:
383
- """Helper method to check model attributes in nested configuration objects"""
384
- for config_attr in config_attrs:
385
- if not hasattr(data, config_attr):
386
- continue
387
- config = getattr(data, config_attr)
388
- if not config:
389
- continue
390
- model = self._check_model_attrs(config, model_attrs)
391
- if model:
392
- return model
393
- return None
394
-
395
- def _extract_handoff_target(self, data: Any, agent_span: "Span[Any]") -> Optional[str]:
396
- """Extract handoff target information with enhanced logic"""
397
- # Try direct target attributes
398
- target_attrs = ['to_agent', 'target_agent', 'destination_agent', 'next_agent']
399
- for attr in target_attrs:
400
- if hasattr(data, attr):
401
- target = getattr(data, attr)
402
- if target and isinstance(target, str):
403
- return f"to {target}"
404
-
405
- # Try from_agent for better handoff description
406
- from_attrs = ['from_agent', 'source_agent', 'previous_agent']
407
- for attr in from_attrs:
408
- if hasattr(data, attr):
409
- source = getattr(data, attr)
410
- if source and isinstance(source, str):
411
- return f"from {source}"
412
-
413
- # Try nested objects
414
- if hasattr(data, 'handoff_info'):
415
- info = data.handoff_info
416
- for attr in target_attrs + from_attrs:
417
- if hasattr(info, attr):
418
- value = getattr(info, attr)
419
- if value and isinstance(value, str):
420
- prefix = "to" if attr in target_attrs else "from"
421
- return f"{prefix} {value}"
422
-
423
- return None
424
-
425
- def _capture_input_output(self, span: Any, data: Any) -> None:
426
- """Capture input/output content with MIME type detection (OpenLIT enhancement)"""
375
+ def span_end(self, span_data, trace_id: str):
376
+ """Handle span end events."""
427
377
  try:
428
- # Capture input content
429
- if hasattr(data, 'input') and data.input is not None:
430
- content = str(data.input)
431
- span.set_attribute(SemanticConvention.GEN_AI_CONTENT_PROMPT, content)
432
- # Set MIME type based on content structure
433
- if content.startswith('{') or content.startswith('['):
434
- span.set_attribute("gen_ai.content.prompt.mime_type", "application/json")
435
- else:
436
- span.set_attribute("gen_ai.content.prompt.mime_type", "text/plain")
437
-
438
- # Capture output/response content
439
- if hasattr(data, 'response') and data.response is not None:
440
- content = str(data.response)
441
- span.set_attribute(SemanticConvention.GEN_AI_CONTENT_COMPLETION, content)
442
- # Set MIME type based on content structure
443
- if content.startswith('{') or content.startswith('['):
444
- span.set_attribute("gen_ai.content.completion.mime_type", "application/json")
378
+ span_id = getattr(span_data, "span_id", "")
379
+ span_key = f"{trace_id}:{span_id}"
380
+
381
+ span = self.active_spans.get(span_key)
382
+ if span:
383
+ # Set final status
384
+ if hasattr(span_data, "error") and span_data.error:
385
+ span.set_status(Status(StatusCode.ERROR, str(span_data.error)))
445
386
  else:
446
- span.set_attribute("gen_ai.content.completion.mime_type", "text/plain")
387
+ span.set_status(Status(StatusCode.OK))
388
+
389
+ # End span
390
+ span.end()
391
+
392
+ # Cleanup
393
+ if span_key in self.active_spans:
394
+ del self.active_spans[span_key]
395
+ if span in self.span_stack:
396
+ self.span_stack.remove(span)
447
397
 
448
- except Exception:
449
- pass # Ignore export errors
398
+ except Exception as e: # pylint: disable=broad-exception-caught
399
+ handle_exception(span if "span" in locals() else None, e)
450
400
 
451
- def _capture_detailed_token_usage(self, span: Any, data: Any) -> None:
452
- """Capture detailed token usage information (inspired by OpenInference)"""
401
+ def force_flush(self):
402
+ """Force flush all pending spans."""
453
403
  try:
454
- if hasattr(data, 'usage'):
455
- usage = data.usage
456
-
457
- # Standard token usage
458
- if hasattr(usage, 'input_tokens'):
459
- span.set_attribute(SemanticConvention.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens)
460
- if hasattr(usage, 'output_tokens'):
461
- span.set_attribute(SemanticConvention.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens)
462
-
463
- # Enhanced token details (when available)
464
- if hasattr(usage, 'input_tokens_details'):
465
- details = usage.input_tokens_details
466
- if hasattr(details, 'cached_tokens'):
467
- span.set_attribute("gen_ai.usage.input_tokens.cached", details.cached_tokens)
468
- if hasattr(details, 'reasoning_tokens'):
469
- span.set_attribute("gen_ai.usage.input_tokens.reasoning", details.reasoning_tokens)
470
-
471
- if hasattr(usage, 'output_tokens_details'):
472
- details = usage.output_tokens_details
473
- if hasattr(details, 'reasoning_tokens'):
474
- span.set_attribute("gen_ai.usage.output_tokens.reasoning", details.reasoning_tokens)
475
-
476
- except Exception:
477
- pass # Ignore export errors
478
-
479
- def _capture_model_parameters(self, span: Any, data: Any) -> None:
480
- """Capture model invocation parameters as JSON (new feature from OpenInference)"""
404
+ # End any remaining spans
405
+ for span in list(self.active_spans.values()):
406
+ span.end()
407
+
408
+ self.active_spans.clear()
409
+ self.span_stack.clear()
410
+
411
+ except Exception as e: # pylint: disable=broad-exception-caught
412
+ handle_exception(None, e)
413
+
414
+ def shutdown(self):
415
+ """Shutdown the processor."""
416
+ self.force_flush()
417
+
418
+ def _extract_model_info(self, span_data) -> Dict[str, Any]:
419
+ """Extract model information from span data."""
420
+ model_info = {}
421
+
422
+ if hasattr(span_data, "model"):
423
+ model_info["model"] = span_data.model
424
+ if hasattr(span_data, "model_name"):
425
+ model_info["model"] = span_data.model_name
426
+
427
+ return model_info
428
+
429
+ def _calculate_cost(
430
+ self, model: str, prompt_tokens: int, completion_tokens: int
431
+ ) -> float:
432
+ """Calculate cost based on token usage."""
433
+ try:
434
+ return get_chat_model_cost(
435
+ model, self.pricing_info, prompt_tokens, completion_tokens
436
+ )
437
+ except Exception: # pylint: disable=broad-exception-caught
438
+ return 0.0
439
+
440
+ # Abstract method implementations required by OpenAI Agents framework
441
+ def on_trace_start(self, trace):
442
+ """Called when a trace starts - required by OpenAI Agents framework"""
481
443
  try:
482
- # Look for model configuration parameters
483
- params = {}
484
-
485
- # Common parameter attributes
486
- param_attrs = ['temperature', 'max_tokens', 'top_p', 'frequency_penalty', 'presence_penalty']
487
- for attr in param_attrs:
488
- if hasattr(data, attr):
489
- params[attr] = getattr(data, attr)
490
-
491
- # Try nested config objects
492
- if hasattr(data, 'config'):
493
- config = data.config
494
- for attr in param_attrs:
495
- if hasattr(config, attr):
496
- params[attr] = getattr(config, attr)
497
-
498
- # Try response object if available
499
- if hasattr(data, 'response') and hasattr(data.response, 'model_dump'):
500
- try:
501
- response_dict = data.response.model_dump()
502
- if response_dict and isinstance(response_dict, dict):
503
- # Extract model parameters from response
504
- if 'model' in response_dict:
505
- params['model'] = response_dict['model']
506
- if 'usage' in response_dict:
507
- params['usage'] = response_dict['usage']
508
- except Exception:
509
- pass
510
-
511
- # Set as JSON if we found any parameters
512
- if params:
513
- span.set_attribute("gen_ai.request.parameters", json.dumps(params))
514
-
515
- except Exception:
516
- pass # Ignore export errors
517
-
518
- def _process_span_completion(self, span: Any, agent_span: "Span[Any]") -> None:
519
- """Process span completion with enhanced business intelligence"""
520
- data = agent_span.span_data
521
-
522
- # Process response data if available
523
- self._process_response_data(span, data)
524
-
525
- # Extract and set token usage for business intelligence
526
- self._extract_token_usage(span, data)
527
-
528
- def _extract_token_usage(self, span: Any, data: Any) -> None:
529
- """Extract token usage and calculate costs (OpenLIT's business intelligence)"""
444
+ self.start_trace(
445
+ getattr(trace, "trace_id", "unknown"),
446
+ getattr(trace, "name", "workflow"),
447
+ )
448
+ except Exception: # pylint: disable=broad-exception-caught
449
+ pass
450
+
451
+ def on_trace_end(self, trace):
452
+ """Called when a trace ends - required by OpenAI Agents framework"""
453
+ try:
454
+ self.end_trace(getattr(trace, "trace_id", "unknown"))
455
+ except Exception: # pylint: disable=broad-exception-caught
456
+ pass
457
+
458
+ def on_span_start(self, span):
459
+ """Called when a span starts - required by OpenAI Agents framework"""
460
+ try:
461
+ trace_id = getattr(span, "trace_id", "unknown")
462
+ self.span_start(span, trace_id)
463
+ except Exception: # pylint: disable=broad-exception-caught
464
+ pass
465
+
466
+ def on_span_end(self, span):
467
+ """Called when a span ends - required by OpenAI Agents framework"""
530
468
  try:
531
- # Try to extract token usage from various possible locations
532
- input_tokens = 0
533
- output_tokens = 0
534
-
535
- # Check direct usage attributes
536
- if hasattr(data, 'usage'):
537
- usage = data.usage
538
- input_tokens = getattr(usage, 'input_tokens', 0) or getattr(usage, 'prompt_tokens', 0)
539
- output_tokens = getattr(usage, 'output_tokens', 0) or getattr(usage, 'completion_tokens', 0)
540
-
541
- # Check response object
542
- elif hasattr(data, 'response') and hasattr(data.response, 'usage'):
543
- usage = data.response.usage
544
- input_tokens = getattr(usage, 'input_tokens', 0) or getattr(usage, 'prompt_tokens', 0)
545
- output_tokens = getattr(usage, 'output_tokens', 0) or getattr(usage, 'completion_tokens', 0)
546
-
547
- # Set token attributes
548
- span.set_attribute(SemanticConvention.GEN_AI_USAGE_INPUT_TOKENS, input_tokens)
549
- span.set_attribute(SemanticConvention.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens)
550
- span.set_attribute(SemanticConvention.GEN_AI_CLIENT_TOKEN_USAGE, input_tokens + output_tokens)
551
-
552
- # Calculate cost (OpenLIT's business intelligence advantage)
553
- model = getattr(data, 'model', 'gpt-4o')
554
- cost = get_chat_model_cost(model, self._pricing_info, input_tokens, output_tokens)
555
- span.set_attribute(SemanticConvention.GEN_AI_USAGE_COST, cost)
556
-
557
- except Exception:
558
- pass # Ignore errors in token usage extraction
559
-
560
- def _process_response_data(self, span: Any, data: Any) -> None:
561
- """Process response data with content capture"""
562
- if self._capture_message_content:
563
- self._capture_input_output(span, data)
564
-
565
- def _process_trace_completion(self, span: Any, trace: "Trace") -> None:
566
- """Process trace completion with business intelligence aggregation"""
567
- # Add trace-level metadata
568
- span.set_attribute(SemanticConvention.GEN_AI_OPERATION_NAME, "workflow")
569
-
570
- # Calculate total duration
571
- if trace.trace_id in self._span_start_times:
572
- start_time = self._span_start_times[trace.trace_id]
573
- duration = time.time() - start_time
574
- span.set_attribute(SemanticConvention.GEN_AI_CLIENT_OPERATION_DURATION, duration)
575
-
576
- def _parse_timestamp(self, timestamp: Any) -> float:
577
- """Parse timestamp from various formats"""
578
- if isinstance(timestamp, (int, float)):
579
- return float(timestamp)
580
- elif isinstance(timestamp, str):
581
- try:
582
- # Try parsing ISO format
583
- dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
584
- return dt.timestamp()
585
- except ValueError:
586
- return time.time()
587
- else:
588
- return time.time()
589
-
590
- def _as_utc_nano(self, timestamp: float) -> int:
591
- """Convert timestamp to UTC nanoseconds for OpenTelemetry"""
592
- return int(timestamp * 1_000_000_000)
593
-
594
- def force_flush(self) -> bool:
595
- """Force flush any pending spans (required by TracingProcessor)"""
596
- return True
597
-
598
- def shutdown(self) -> bool:
599
- """Shutdown the processor (required by TracingProcessor)"""
600
- return True
469
+ trace_id = getattr(span, "trace_id", "unknown")
470
+ self.span_end(span, trace_id)
471
+ except Exception: # pylint: disable=broad-exception-caught
472
+ pass