lucidicai 2.0.2__py3-none-any.whl → 2.1.0__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 (35) hide show
  1. lucidicai/__init__.py +350 -899
  2. lucidicai/api/__init__.py +1 -0
  3. lucidicai/api/client.py +218 -0
  4. lucidicai/api/resources/__init__.py +1 -0
  5. lucidicai/api/resources/dataset.py +192 -0
  6. lucidicai/api/resources/event.py +88 -0
  7. lucidicai/api/resources/session.py +126 -0
  8. lucidicai/core/__init__.py +1 -0
  9. lucidicai/core/config.py +223 -0
  10. lucidicai/core/errors.py +60 -0
  11. lucidicai/core/types.py +35 -0
  12. lucidicai/sdk/__init__.py +1 -0
  13. lucidicai/sdk/context.py +144 -0
  14. lucidicai/sdk/decorators.py +187 -0
  15. lucidicai/sdk/error_boundary.py +299 -0
  16. lucidicai/sdk/event.py +122 -0
  17. lucidicai/sdk/event_builder.py +304 -0
  18. lucidicai/sdk/features/__init__.py +1 -0
  19. lucidicai/sdk/features/dataset.py +605 -0
  20. lucidicai/sdk/features/feature_flag.py +383 -0
  21. lucidicai/sdk/init.py +271 -0
  22. lucidicai/sdk/shutdown_manager.py +302 -0
  23. lucidicai/telemetry/context_bridge.py +82 -0
  24. lucidicai/telemetry/context_capture_processor.py +25 -9
  25. lucidicai/telemetry/litellm_bridge.py +18 -24
  26. lucidicai/telemetry/lucidic_exporter.py +51 -36
  27. lucidicai/telemetry/utils/model_pricing.py +278 -0
  28. lucidicai/utils/__init__.py +1 -0
  29. lucidicai/utils/images.py +337 -0
  30. lucidicai/utils/logger.py +168 -0
  31. lucidicai/utils/queue.py +393 -0
  32. {lucidicai-2.0.2.dist-info → lucidicai-2.1.0.dist-info}/METADATA +1 -1
  33. {lucidicai-2.0.2.dist-info → lucidicai-2.1.0.dist-info}/RECORD +35 -8
  34. {lucidicai-2.0.2.dist-info → lucidicai-2.1.0.dist-info}/WHEEL +0 -0
  35. {lucidicai-2.0.2.dist-info → lucidicai-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,302 @@
1
+ """Shutdown manager for graceful cleanup.
2
+
3
+ Coordinates shutdown across all active sessions, ensuring proper cleanup
4
+ on process exit. Inspired by TypeScript SDK's shutdown-manager.ts.
5
+ """
6
+ import atexit
7
+ import signal
8
+ import sys
9
+ import threading
10
+ import time
11
+ from typing import Dict, Optional, Set, Callable
12
+ from dataclasses import dataclass
13
+
14
+ from ..utils.logger import debug, info, warning, error, truncate_id
15
+
16
+
17
+ @dataclass
18
+ class SessionState:
19
+ """State information for an active session."""
20
+ session_id: str
21
+ http_client: Optional[object] = None
22
+ event_queue: Optional[object] = None
23
+ is_shutting_down: bool = False
24
+ auto_end: bool = True
25
+
26
+
27
+ class ShutdownManager:
28
+ """Singleton manager for coordinating shutdown across all active sessions.
29
+
30
+ Ensures process listeners are only registered once and all sessions
31
+ are properly ended on exit.
32
+ """
33
+
34
+ _instance: Optional['ShutdownManager'] = None
35
+ _lock = threading.Lock()
36
+
37
+ def __new__(cls):
38
+ if cls._instance is None:
39
+ with cls._lock:
40
+ if cls._instance is None:
41
+ cls._instance = super().__new__(cls)
42
+ cls._instance._initialized = False
43
+ return cls._instance
44
+
45
+ def __init__(self):
46
+ # only initialize once
47
+ if self._initialized:
48
+ return
49
+
50
+ self._initialized = True
51
+ self.active_sessions: Dict[str, SessionState] = {}
52
+ self.is_shutting_down = False
53
+ self.shutdown_complete = threading.Event()
54
+ self.listeners_registered = False
55
+ self._session_lock = threading.Lock()
56
+
57
+ debug("[ShutdownManager] Initialized")
58
+
59
+ def register_session(self, session_id: str, state: SessionState) -> None:
60
+ """Register a new active session.
61
+
62
+ Args:
63
+ session_id: Session identifier
64
+ state: Session state information
65
+ """
66
+ with self._session_lock:
67
+ debug(f"[ShutdownManager] Registering session {truncate_id(session_id)}, auto_end={state.auto_end}")
68
+ self.active_sessions[session_id] = state
69
+
70
+ # ensure listeners are registered
71
+ self._ensure_listeners_registered()
72
+
73
+ def unregister_session(self, session_id: str) -> None:
74
+ """Unregister a session after it ends.
75
+
76
+ Args:
77
+ session_id: Session identifier
78
+ """
79
+ with self._session_lock:
80
+ debug(f"[ShutdownManager] Unregistering session {truncate_id(session_id)}")
81
+ self.active_sessions.pop(session_id, None)
82
+
83
+ def get_active_session_count(self) -> int:
84
+ """Get count of active sessions."""
85
+ with self._session_lock:
86
+ return len(self.active_sessions)
87
+
88
+ def is_session_active(self, session_id: str) -> bool:
89
+ """Check if a session is active.
90
+
91
+ Args:
92
+ session_id: Session identifier
93
+
94
+ Returns:
95
+ True if session is active
96
+ """
97
+ with self._session_lock:
98
+ return session_id in self.active_sessions
99
+
100
+ def _ensure_listeners_registered(self) -> None:
101
+ """Register process exit listeners once."""
102
+ if self.listeners_registered:
103
+ return
104
+
105
+ self.listeners_registered = True
106
+ debug("[ShutdownManager] Registering global shutdown listeners (atexit, SIGINT, SIGTERM, uncaught exceptions)")
107
+
108
+ # register atexit handler for normal termination
109
+ atexit.register(self._handle_exit)
110
+
111
+ # register signal handlers for interrupts
112
+ signal.signal(signal.SIGINT, self._signal_handler)
113
+ signal.signal(signal.SIGTERM, self._signal_handler)
114
+
115
+ # register uncaught exception handler
116
+ sys.excepthook = self._exception_handler
117
+
118
+ def _signal_handler(self, signum, frame):
119
+ """Handle shutdown signals."""
120
+ info(f"[ShutdownManager] Received signal {signum}, initiating graceful shutdown")
121
+ self._handle_shutdown(f"signal_{signum}")
122
+ # exit after cleanup
123
+ sys.exit(0)
124
+
125
+ def _exception_handler(self, exc_type, exc_value, exc_traceback):
126
+ """Handle uncaught exceptions."""
127
+ # log the exception
128
+ error(f"[ShutdownManager] Uncaught exception: {exc_type.__name__}: {exc_value}")
129
+
130
+ # Create an error event for the uncaught exception
131
+ try:
132
+ from ..sdk.event import create_event
133
+ import traceback
134
+
135
+ error_message = f"{exc_type.__name__}: {exc_value}"
136
+ traceback_str = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
137
+
138
+ create_event(
139
+ type="error_traceback",
140
+ error=error_message,
141
+ traceback=traceback_str
142
+ )
143
+ debug(f"[ShutdownManager] Created error_traceback event for uncaught exception")
144
+ except Exception as e:
145
+ debug(f"[ShutdownManager] Failed to create error_traceback event: {e}")
146
+
147
+ # perform shutdown
148
+ self._handle_shutdown("uncaught_exception")
149
+
150
+ # call default handler
151
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
152
+
153
+ def _handle_exit(self):
154
+ """Handle normal process exit."""
155
+ debug("[ShutdownManager] Normal process exit triggered (atexit)")
156
+ self._handle_shutdown("atexit")
157
+
158
+ def _handle_shutdown(self, trigger: str) -> None:
159
+ """Coordinate shutdown of all sessions.
160
+
161
+ Args:
162
+ trigger: What triggered the shutdown
163
+ """
164
+ if self.is_shutting_down:
165
+ debug(f"[ShutdownManager] Already shutting down, ignoring {trigger}")
166
+ return
167
+
168
+ self.is_shutting_down = True
169
+
170
+ with self._session_lock:
171
+ session_count = len(self.active_sessions)
172
+ if session_count == 0:
173
+ debug("[ShutdownManager] No active sessions to clean up")
174
+ self.shutdown_complete.set()
175
+ return
176
+
177
+ info(f"[ShutdownManager] Shutdown initiated by {trigger}, ending {session_count} active session(s)")
178
+
179
+ # perform shutdown in separate thread to avoid deadlocks
180
+ import threading
181
+ shutdown_thread = threading.Thread(
182
+ target=self._perform_shutdown,
183
+ name="ShutdownThread"
184
+ )
185
+ shutdown_thread.daemon = True
186
+ shutdown_thread.start()
187
+
188
+ # wait for shutdown with timeout
189
+ if not self.shutdown_complete.wait(timeout=30):
190
+ warning("[ShutdownManager] Shutdown timeout after 30s")
191
+
192
+ def _perform_shutdown(self) -> None:
193
+ """Perform the actual shutdown of all sessions."""
194
+ debug("[ShutdownManager] _perform_shutdown thread started")
195
+ try:
196
+ sessions_to_end = []
197
+
198
+ with self._session_lock:
199
+ # collect sessions that need ending
200
+ for session_id, state in self.active_sessions.items():
201
+ if state.auto_end and not state.is_shutting_down:
202
+ state.is_shutting_down = True
203
+ sessions_to_end.append((session_id, state))
204
+
205
+ debug(f"[ShutdownManager] Found {len(sessions_to_end)} sessions to end")
206
+
207
+ # end all sessions
208
+ for session_id, state in sessions_to_end:
209
+ try:
210
+ debug(f"[ShutdownManager] Ending session {truncate_id(session_id)}")
211
+ self._end_session(session_id, state)
212
+ except Exception as e:
213
+ error(f"[ShutdownManager] Error ending session {truncate_id(session_id)}: {e}")
214
+
215
+ # Final telemetry shutdown after all sessions are ended
216
+ try:
217
+ from ..sdk.init import _sdk_state
218
+ if hasattr(_sdk_state, 'tracer_provider') and _sdk_state.tracer_provider:
219
+ debug("[ShutdownManager] Final OpenTelemetry shutdown")
220
+ try:
221
+ # Final flush and shutdown with longer timeout
222
+ _sdk_state.tracer_provider.force_flush(timeout_millis=5000)
223
+ _sdk_state.tracer_provider.shutdown()
224
+ debug("[ShutdownManager] OpenTelemetry shutdown complete")
225
+ except Exception as e:
226
+ error(f"[ShutdownManager] Error in final telemetry shutdown: {e}")
227
+ except ImportError:
228
+ pass # SDK not initialized
229
+
230
+ info("[ShutdownManager] Shutdown complete")
231
+
232
+ except Exception as e:
233
+ error(f"[ShutdownManager] Unexpected error in _perform_shutdown: {e}")
234
+ import traceback
235
+ error(f"[ShutdownManager] Traceback: {traceback.format_exc()}")
236
+ finally:
237
+ debug("[ShutdownManager] Setting shutdown_complete event")
238
+ self.shutdown_complete.set()
239
+
240
+ def _end_session(self, session_id: str, state: SessionState) -> None:
241
+ """End a single session with cleanup.
242
+
243
+ Args:
244
+ session_id: Session identifier
245
+ state: Session state
246
+ """
247
+ # Flush OpenTelemetry spans first (before event queue)
248
+ try:
249
+ # Get the global tracer provider if it exists
250
+ from ..sdk.init import _sdk_state
251
+ if hasattr(_sdk_state, 'tracer_provider') and _sdk_state.tracer_provider:
252
+ debug(f"[ShutdownManager] Flushing OpenTelemetry spans for session {truncate_id(session_id)}")
253
+ try:
254
+ # Force flush with 3 second timeout
255
+ _sdk_state.tracer_provider.force_flush(timeout_millis=3000)
256
+ except Exception as e:
257
+ error(f"[ShutdownManager] Error flushing spans: {e}")
258
+ except ImportError:
259
+ pass # SDK not initialized
260
+
261
+ # Skip event queue flush during shutdown to avoid hanging
262
+ # The queue worker is a daemon thread and will flush on its own
263
+ if state.event_queue:
264
+ debug(f"[ShutdownManager] Skipping event queue flush during shutdown for session {truncate_id(session_id)}")
265
+
266
+ # end session via API if http client present
267
+ if state.http_client and session_id:
268
+ try:
269
+ debug(f"[ShutdownManager] Ending session {truncate_id(session_id)} via API")
270
+ debug(f"[ShutdownManager] http_client type: {type(state.http_client)}, keys: {state.http_client.keys() if isinstance(state.http_client, dict) else 'not a dict'}")
271
+ # state.http_client is a resources dict with 'sessions' key
272
+ if isinstance(state.http_client, dict) and 'sessions' in state.http_client:
273
+ state.http_client['sessions'].end_session(
274
+ session_id,
275
+ is_successful=False,
276
+ session_eval_reason="Process shutdown"
277
+ )
278
+ debug(f"[ShutdownManager] Session {truncate_id(session_id)} ended via API")
279
+ else:
280
+ debug(f"[ShutdownManager] Cannot end session - http_client not properly configured")
281
+ except Exception as e:
282
+ error(f"[ShutdownManager] Error ending session via API: {e}")
283
+
284
+ # unregister the session
285
+ self.unregister_session(session_id)
286
+
287
+ def reset(self) -> None:
288
+ """Reset shutdown manager (for testing)."""
289
+ with self._session_lock:
290
+ self.active_sessions.clear()
291
+ self.is_shutting_down = False
292
+ self.shutdown_complete.clear()
293
+ # note: we don't reset listeners_registered as they persist
294
+
295
+
296
+ # global singleton instance
297
+ _shutdown_manager = ShutdownManager()
298
+
299
+
300
+ def get_shutdown_manager() -> ShutdownManager:
301
+ """Get the global shutdown manager instance."""
302
+ return _shutdown_manager
@@ -0,0 +1,82 @@
1
+ """Bridge between Lucidic context and OpenTelemetry context.
2
+
3
+ This module ensures that Lucidic's contextvars (session_id, parent_event_id)
4
+ are properly propagated through OpenTelemetry's context system, which is
5
+ necessary for instrumentors that create spans in different execution contexts.
6
+ """
7
+
8
+ from typing import Optional
9
+ from opentelemetry import baggage, context as otel_context
10
+ from opentelemetry.trace import set_span_in_context
11
+ from ..utils.logger import debug, verbose, truncate_id
12
+
13
+
14
+ def inject_lucidic_context() -> otel_context.Context:
15
+ """Inject Lucidic context into OpenTelemetry baggage.
16
+
17
+ This ensures that our context variables are available to any spans
18
+ created by OpenTelemetry instrumentors, even if they run in different
19
+ threads or async contexts.
20
+
21
+ Returns:
22
+ OpenTelemetry Context with Lucidic values in baggage
23
+ """
24
+ try:
25
+ from ..sdk.context import current_session_id, current_parent_event_id
26
+
27
+ ctx = otel_context.get_current()
28
+
29
+ # Get Lucidic context values
30
+ session_id = None
31
+ parent_event_id = None
32
+
33
+ try:
34
+ session_id = current_session_id.get(None)
35
+ except Exception:
36
+ pass
37
+
38
+ try:
39
+ parent_event_id = current_parent_event_id.get(None)
40
+ except Exception:
41
+ pass
42
+
43
+ # Inject into OpenTelemetry baggage
44
+ if session_id:
45
+ ctx = baggage.set_baggage("lucidic.session_id", session_id, context=ctx)
46
+ debug(f"[ContextBridge] Injected session_id {truncate_id(session_id)} into OTel baggage")
47
+
48
+ if parent_event_id:
49
+ ctx = baggage.set_baggage("lucidic.parent_event_id", parent_event_id, context=ctx)
50
+ debug(f"[ContextBridge] Injected parent_event_id {truncate_id(parent_event_id)} into OTel baggage")
51
+
52
+ return ctx
53
+
54
+ except Exception as e:
55
+ verbose(f"[ContextBridge] Failed to inject context: {e}")
56
+ return otel_context.get_current()
57
+
58
+
59
+ def extract_lucidic_context(ctx: Optional[otel_context.Context] = None) -> tuple[Optional[str], Optional[str]]:
60
+ """Extract Lucidic context from OpenTelemetry baggage.
61
+
62
+ Args:
63
+ ctx: OpenTelemetry context (uses current if not provided)
64
+
65
+ Returns:
66
+ Tuple of (session_id, parent_event_id)
67
+ """
68
+ if ctx is None:
69
+ ctx = otel_context.get_current()
70
+
71
+ try:
72
+ session_id = baggage.get_baggage("lucidic.session_id", context=ctx)
73
+ parent_event_id = baggage.get_baggage("lucidic.parent_event_id", context=ctx)
74
+
75
+ if session_id or parent_event_id:
76
+ debug(f"[ContextBridge] Extracted from OTel baggage - session: {truncate_id(session_id)}, parent: {truncate_id(parent_event_id)}")
77
+
78
+ return session_id, parent_event_id
79
+
80
+ except Exception as e:
81
+ verbose(f"[ContextBridge] Failed to extract context: {e}")
82
+ return None, None
@@ -8,13 +8,11 @@ processed asynchronously in different threads/contexts.
8
8
  This fixes the nesting issue for ALL providers (OpenAI, Anthropic, LangChain, etc.)
9
9
  """
10
10
 
11
- import logging
12
11
  from typing import Optional
13
12
  from opentelemetry.sdk.trace import SpanProcessor, ReadableSpan
14
13
  from opentelemetry.trace import Span
15
14
  from opentelemetry import context as otel_context
16
-
17
- logger = logging.getLogger("Lucidic")
15
+ from ..utils.logger import debug, verbose, truncate_id
18
16
 
19
17
 
20
18
  class ContextCaptureProcessor(SpanProcessor):
@@ -24,33 +22,51 @@ class ContextCaptureProcessor(SpanProcessor):
24
22
  """Called when a span is started - capture context here."""
25
23
  try:
26
24
  # Import here to avoid circular imports
27
- from lucidicai.context import current_session_id, current_parent_event_id
25
+ from lucidicai.sdk.context import current_session_id, current_parent_event_id
26
+ from .context_bridge import extract_lucidic_context
28
27
 
29
- # Capture session ID from context
28
+ # Try to get from contextvars first
30
29
  session_id = None
30
+ parent_event_id = None
31
+
31
32
  try:
32
33
  session_id = current_session_id.get(None)
33
34
  except Exception:
34
35
  pass
35
36
 
36
- # Capture parent event ID from context
37
- parent_event_id = None
38
37
  try:
39
38
  parent_event_id = current_parent_event_id.get(None)
40
39
  except Exception:
41
40
  pass
42
41
 
42
+ # If not found in contextvars, try OpenTelemetry baggage
43
+ # This handles cases where spans are created in different threads
44
+ if not session_id or not parent_event_id:
45
+ baggage_session, baggage_parent = extract_lucidic_context(parent_context)
46
+ if not session_id and baggage_session:
47
+ session_id = baggage_session
48
+ debug(f"[ContextCapture] Got session_id from OTel baggage for span {span.name}")
49
+ if not parent_event_id and baggage_parent:
50
+ parent_event_id = baggage_parent
51
+ debug(f"[ContextCapture] Got parent_event_id from OTel baggage for span {span.name}")
52
+
53
+ # Add debug logging to understand context propagation
54
+ debug(f"[ContextCapture] Processing span '{span.name}' - session: {truncate_id(session_id)}, parent: {truncate_id(parent_event_id)}")
55
+
43
56
  # Store in span attributes for later retrieval
44
57
  if session_id:
45
58
  span.set_attribute("lucidic.session_id", session_id)
59
+ debug(f"[ContextCapture] Set session_id attribute for span {span.name}")
46
60
 
47
61
  if parent_event_id:
48
62
  span.set_attribute("lucidic.parent_event_id", parent_event_id)
49
- logger.debug(f"[ContextCapture] Captured parent_event_id {parent_event_id[:8]}... for span {span.name}")
63
+ debug(f"[ContextCapture] Captured parent_event_id {truncate_id(parent_event_id)} for span {span.name}")
64
+ else:
65
+ debug(f"[ContextCapture] No parent_event_id available for span {span.name}")
50
66
 
51
67
  except Exception as e:
52
68
  # Never fail span creation due to context capture
53
- logger.debug(f"[ContextCapture] Failed to capture context: {e}")
69
+ verbose(f"[ContextCapture] Failed to capture context: {e}")
54
70
 
55
71
  def on_end(self, span: ReadableSpan) -> None:
56
72
  """Called when a span ends - no action needed."""
@@ -14,9 +14,10 @@ except ImportError:
14
14
  def __init__(self, **kwargs):
15
15
  pass
16
16
 
17
- from lucidicai.client import Client
18
- from lucidicai.model_pricing import calculate_cost
19
- from lucidicai.context import current_parent_event_id
17
+ from lucidicai.sdk.event import create_event
18
+ from lucidicai.sdk.init import get_session_id
19
+ from lucidicai.telemetry.utils.model_pricing import calculate_cost
20
+ from lucidicai.sdk.context import current_parent_event_id
20
21
 
21
22
  logger = logging.getLogger("Lucidic")
22
23
  DEBUG = os.getenv("LUCIDIC_DEBUG", "False") == "True"
@@ -76,16 +77,12 @@ class LucidicLiteLLMCallback(CustomLogger):
76
77
  def log_pre_api_call(self, model, messages, kwargs):
77
78
  """Called before the LLM API call"""
78
79
  try:
79
- client = Client()
80
- if not client.session:
80
+ session_id = get_session_id()
81
+ if not session_id:
81
82
  return
82
83
 
83
84
  # Extract description from messages
84
85
  description = self._format_messages(messages)
85
-
86
- # Apply masking if configured
87
- if hasattr(client, 'mask') and callable(client.mask):
88
- description = client.mask(description)
89
86
 
90
87
  # Store pre-call info for later use
91
88
  call_id = kwargs.get("litellm_call_id", str(time.time())) if kwargs else str(time.time())
@@ -109,8 +106,8 @@ class LucidicLiteLLMCallback(CustomLogger):
109
106
  self._register_callback(callback_id)
110
107
 
111
108
  try:
112
- client = Client()
113
- if not client.session:
109
+ session_id = get_session_id()
110
+ if not session_id:
114
111
  self._complete_callback(callback_id)
115
112
  return
116
113
 
@@ -129,10 +126,6 @@ class LucidicLiteLLMCallback(CustomLogger):
129
126
  # Extract response content
130
127
  result = self._extract_response_content(response_obj)
131
128
 
132
- # Apply masking to result if configured
133
- if hasattr(client, 'mask') and callable(client.mask):
134
- result = client.mask(result)
135
-
136
129
  # Calculate cost if usage info is available
137
130
  usage = self._extract_usage(response_obj)
138
131
  cost = None
@@ -142,7 +135,7 @@ class LucidicLiteLLMCallback(CustomLogger):
142
135
  # Extract any images from multimodal requests
143
136
  images = self._extract_images_from_messages(messages)
144
137
 
145
- # Create LLM_GENERATION typed event
138
+ # Get parent event ID from context
146
139
  parent_id = None
147
140
  try:
148
141
  parent_id = current_parent_event_id.get(None)
@@ -150,10 +143,11 @@ class LucidicLiteLLMCallback(CustomLogger):
150
143
  parent_id = None
151
144
 
152
145
  # occurred_at/duration from datetimes
153
- occ_dt = start_time if isinstance(start_time, datetime) else None
146
+ occ_dt = start_time.isoformat() if isinstance(start_time, datetime) else None
154
147
  duration_secs = (end_time - start_time).total_seconds() if isinstance(start_time, datetime) and isinstance(end_time, datetime) else None
155
148
 
156
- client.create_event(
149
+ # Create event with correct field names
150
+ create_event(
157
151
  type="llm_generation",
158
152
  provider=provider,
159
153
  model=model,
@@ -162,7 +156,7 @@ class LucidicLiteLLMCallback(CustomLogger):
162
156
  input_tokens=(usage or {}).get("prompt_tokens", 0),
163
157
  output_tokens=(usage or {}).get("completion_tokens", 0),
164
158
  cost=cost,
165
- parent_event_id=parent_id,
159
+ parent_event_id=parent_id, # This will be normalized by EventBuilder
166
160
  occurred_at=occ_dt,
167
161
  duration=duration_secs,
168
162
  )
@@ -185,8 +179,8 @@ class LucidicLiteLLMCallback(CustomLogger):
185
179
  self._register_callback(callback_id)
186
180
 
187
181
  try:
188
- client = Client()
189
- if not client.session:
182
+ session_id = get_session_id()
183
+ if not session_id:
190
184
  self._complete_callback(callback_id)
191
185
  return
192
186
 
@@ -211,14 +205,14 @@ class LucidicLiteLLMCallback(CustomLogger):
211
205
  parent_id = current_parent_event_id.get(None)
212
206
  except Exception:
213
207
  parent_id = None
214
- occ_dt = start_time if isinstance(start_time, datetime) else None
208
+ occ_dt = start_time.isoformat() if isinstance(start_time, datetime) else None
215
209
  duration_secs = (end_time - start_time).total_seconds() if isinstance(start_time, datetime) and isinstance(end_time, datetime) else None
216
210
 
217
- client.create_event(
211
+ create_event(
218
212
  type="error_traceback",
219
213
  error=error_msg,
220
214
  traceback="",
221
- parent_event_id=parent_id,
215
+ parent_event_id=parent_id, # This will be normalized by EventBuilder
222
216
  occurred_at=occ_dt,
223
217
  duration=duration_secs,
224
218
  metadata={"provider": provider, "litellm": True}