kalibr 1.1.3a0__py3-none-any.whl → 1.4.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.
@@ -60,6 +60,72 @@ def _get_provider_from_model(model: str) -> str:
60
60
  return "openai"
61
61
 
62
62
 
63
+ def _extract_model_from_agent(agent) -> tuple[str, str]:
64
+ """Extract model name and provider from agent's LLM config.
65
+
66
+ Args:
67
+ agent: CrewAI agent instance
68
+
69
+ Returns:
70
+ Tuple of (model_name, provider)
71
+ """
72
+ model_name = "unknown"
73
+ provider = "openai"
74
+
75
+ if not hasattr(agent, "llm"):
76
+ return model_name, provider
77
+
78
+ llm = agent.llm
79
+
80
+ # Case 1: LLM is a string like "openai/gpt-4o-mini" or "gpt-4"
81
+ if isinstance(llm, str):
82
+ if "/" in llm:
83
+ parts = llm.split("/", 1)
84
+ provider = parts[0]
85
+ model_name = parts[1]
86
+ else:
87
+ model_name = llm
88
+ provider = _get_provider_from_model(llm)
89
+ return model_name, provider
90
+
91
+ # Case 2: LLM has model or model_name attribute
92
+ if hasattr(llm, "model"):
93
+ model_name = str(llm.model)
94
+ elif hasattr(llm, "model_name"):
95
+ model_name = str(llm.model_name)
96
+
97
+ # Parse provider from model string if it contains "/"
98
+ if "/" in model_name:
99
+ parts = model_name.split("/", 1)
100
+ provider = parts[0]
101
+ model_name = parts[1]
102
+ else:
103
+ provider = _get_provider_from_model(model_name)
104
+
105
+ return model_name, provider
106
+
107
+
108
+ def _calculate_cost(provider: str, model: str, input_tokens: int, output_tokens: int) -> float:
109
+ """Calculate cost using CostAdapterFactory.
110
+
111
+ Args:
112
+ provider: Provider name (openai, anthropic, etc.)
113
+ model: Model name
114
+ input_tokens: Number of input tokens
115
+ output_tokens: Number of output tokens
116
+
117
+ Returns:
118
+ Cost in USD
119
+ """
120
+ if CostAdapterFactory is None:
121
+ return 0.0
122
+
123
+ try:
124
+ return CostAdapterFactory.compute_cost(provider, model, input_tokens, output_tokens)
125
+ except Exception:
126
+ return 0.0
127
+
128
+
63
129
  class EventBatcher:
64
130
  """Shared event batching for callbacks."""
65
131
 
@@ -198,6 +264,7 @@ class KalibrAgentCallback:
198
264
  service: Service name
199
265
  workflow_id: Workflow identifier
200
266
  metadata: Additional metadata for all events
267
+ agent: Optional agent reference for model extraction
201
268
 
202
269
  Usage:
203
270
  from kalibr_crewai import KalibrAgentCallback
@@ -210,6 +277,7 @@ class KalibrAgentCallback:
210
277
  goal="Find information",
211
278
  step_callback=callback,
212
279
  )
280
+ callback.set_agent(agent) # Set agent reference for model extraction
213
281
  """
214
282
 
215
283
  def __init__(
@@ -221,6 +289,7 @@ class KalibrAgentCallback:
221
289
  service: Optional[str] = None,
222
290
  workflow_id: Optional[str] = None,
223
291
  metadata: Optional[Dict[str, Any]] = None,
292
+ agent: Optional[Any] = None,
224
293
  ):
225
294
  self.api_key = api_key or os.getenv("KALIBR_API_KEY", "")
226
295
  self.endpoint = endpoint or os.getenv(
@@ -232,6 +301,7 @@ class KalibrAgentCallback:
232
301
  self.service = service or os.getenv("KALIBR_SERVICE", "crewai-app")
233
302
  self.workflow_id = workflow_id or os.getenv("KALIBR_WORKFLOW_ID", "default-workflow")
234
303
  self.default_metadata = metadata or {}
304
+ self._agent = agent
235
305
 
236
306
  # Get shared batcher
237
307
  self._batcher = EventBatcher.get_instance(
@@ -244,6 +314,14 @@ class KalibrAgentCallback:
244
314
  self._agent_span_id: Optional[str] = None
245
315
  self._step_count: int = 0
246
316
 
317
+ def set_agent(self, agent: Any) -> None:
318
+ """Set the agent reference for model extraction.
319
+
320
+ Args:
321
+ agent: CrewAI agent instance
322
+ """
323
+ self._agent = agent
324
+
247
325
  def __call__(self, step_output: Any) -> None:
248
326
  """Called after each agent step.
249
327
 
@@ -271,6 +349,12 @@ class KalibrAgentCallback:
271
349
 
272
350
  span_id = str(uuid.uuid4())
273
351
 
352
+ # Extract model from agent if available
353
+ model_name = "unknown"
354
+ provider = "openai"
355
+ if self._agent:
356
+ model_name, provider = _extract_model_from_agent(self._agent)
357
+
274
358
  # Extract step information
275
359
  step_type = "agent_step"
276
360
  operation = "agent_step"
@@ -307,8 +391,11 @@ class KalibrAgentCallback:
307
391
  output_text = str(step_output)
308
392
 
309
393
  # Count tokens
310
- input_tokens = _count_tokens(tool_input or "", "gpt-4")
311
- output_tokens = _count_tokens(output_text, "gpt-4")
394
+ input_tokens = _count_tokens(tool_input or "", model_name)
395
+ output_tokens = _count_tokens(output_text, model_name)
396
+
397
+ # Calculate cost using CostAdapterFactory
398
+ cost_usd = _calculate_cost(provider, model_name, input_tokens, output_tokens)
312
399
 
313
400
  # Build event
314
401
  event = {
@@ -318,9 +405,9 @@ class KalibrAgentCallback:
318
405
  "parent_span_id": self._agent_span_id,
319
406
  "tenant_id": self.tenant_id,
320
407
  "workflow_id": self.workflow_id,
321
- "provider": "crewai",
322
- "model_id": "agent",
323
- "model_name": "crewai-agent",
408
+ "provider": provider,
409
+ "model_id": model_name,
410
+ "model_name": model_name,
324
411
  "operation": operation,
325
412
  "endpoint": operation,
326
413
  "duration_ms": 0, # Step timing not available
@@ -328,8 +415,8 @@ class KalibrAgentCallback:
328
415
  "input_tokens": input_tokens,
329
416
  "output_tokens": output_tokens,
330
417
  "total_tokens": input_tokens + output_tokens,
331
- "cost_usd": 0.0, # Cost tracked at LLM level
332
- "total_cost_usd": 0.0,
418
+ "cost_usd": cost_usd,
419
+ "total_cost_usd": cost_usd,
333
420
  "status": status,
334
421
  "timestamp": now.isoformat(),
335
422
  "ts_start": now.isoformat(),
@@ -376,6 +463,7 @@ class KalibrTaskCallback:
376
463
  service: Service name
377
464
  workflow_id: Workflow identifier
378
465
  metadata: Additional metadata for all events
466
+ agent: Optional agent reference for model extraction
379
467
 
380
468
  Usage:
381
469
  from kalibr_crewai import KalibrTaskCallback
@@ -388,6 +476,7 @@ class KalibrTaskCallback:
388
476
  agent=my_agent,
389
477
  callback=callback,
390
478
  )
479
+ callback.set_agent(my_agent) # Set agent reference for model extraction
391
480
  """
392
481
 
393
482
  def __init__(
@@ -399,6 +488,7 @@ class KalibrTaskCallback:
399
488
  service: Optional[str] = None,
400
489
  workflow_id: Optional[str] = None,
401
490
  metadata: Optional[Dict[str, Any]] = None,
491
+ agent: Optional[Any] = None,
402
492
  ):
403
493
  self.api_key = api_key or os.getenv("KALIBR_API_KEY", "")
404
494
  self.endpoint = endpoint or os.getenv(
@@ -410,6 +500,7 @@ class KalibrTaskCallback:
410
500
  self.service = service or os.getenv("KALIBR_SERVICE", "crewai-app")
411
501
  self.workflow_id = workflow_id or os.getenv("KALIBR_WORKFLOW_ID", "default-workflow")
412
502
  self.default_metadata = metadata or {}
503
+ self._agent = agent
413
504
 
414
505
  # Get shared batcher
415
506
  self._batcher = EventBatcher.get_instance(
@@ -421,6 +512,14 @@ class KalibrTaskCallback:
421
512
  self._trace_id: Optional[str] = None
422
513
  self._crew_span_id: Optional[str] = None
423
514
 
515
+ def set_agent(self, agent: Any) -> None:
516
+ """Set the agent reference for model extraction.
517
+
518
+ Args:
519
+ agent: CrewAI agent instance
520
+ """
521
+ self._agent = agent
522
+
424
523
  def __call__(self, task_output: Any) -> None:
425
524
  """Called when task completes.
426
525
 
@@ -467,9 +566,18 @@ class KalibrTaskCallback:
467
566
  if hasattr(task_output, "agent"):
468
567
  agent_role = str(task_output.agent)
469
568
 
569
+ # Extract model from agent if available
570
+ model_name = "unknown"
571
+ provider = "openai"
572
+ if self._agent:
573
+ model_name, provider = _extract_model_from_agent(self._agent)
574
+
470
575
  # Token counting
471
- input_tokens = _count_tokens(description, "gpt-4")
472
- output_tokens = _count_tokens(raw_output, "gpt-4")
576
+ input_tokens = _count_tokens(description, model_name)
577
+ output_tokens = _count_tokens(raw_output, model_name)
578
+
579
+ # Calculate cost using CostAdapterFactory
580
+ cost_usd = _calculate_cost(provider, model_name, input_tokens, output_tokens)
473
581
 
474
582
  # Build operation name from description
475
583
  operation = "task_complete"
@@ -486,9 +594,9 @@ class KalibrTaskCallback:
486
594
  "parent_span_id": self._crew_span_id,
487
595
  "tenant_id": self.tenant_id,
488
596
  "workflow_id": self.workflow_id,
489
- "provider": "crewai",
490
- "model_id": "task",
491
- "model_name": agent_role,
597
+ "provider": provider,
598
+ "model_id": model_name,
599
+ "model_name": model_name,
492
600
  "operation": operation,
493
601
  "endpoint": "task_complete",
494
602
  "duration_ms": 0, # Task timing not available in callback
@@ -496,8 +604,8 @@ class KalibrTaskCallback:
496
604
  "input_tokens": input_tokens,
497
605
  "output_tokens": output_tokens,
498
606
  "total_tokens": input_tokens + output_tokens,
499
- "cost_usd": 0.0, # Cost tracked at LLM level
500
- "total_cost_usd": 0.0,
607
+ "cost_usd": cost_usd,
608
+ "total_cost_usd": cost_usd,
501
609
  "status": "success",
502
610
  "timestamp": now.isoformat(),
503
611
  "ts_start": now.isoformat(),
@@ -12,6 +12,8 @@ from datetime import datetime, timezone
12
12
  from functools import wraps
13
13
  from typing import Any, Callable, Dict, List, Optional
14
14
 
15
+ from opentelemetry import trace as otel_trace
16
+
15
17
  from .callbacks import EventBatcher, _count_tokens, _get_provider_from_model
16
18
 
17
19
  # Import Kalibr cost adapters
@@ -21,6 +23,72 @@ except ImportError:
21
23
  CostAdapterFactory = None
22
24
 
23
25
 
26
+ def _extract_model_from_agent(agent) -> tuple[str, str]:
27
+ """Extract model name and provider from agent's LLM config.
28
+
29
+ Args:
30
+ agent: CrewAI agent instance
31
+
32
+ Returns:
33
+ Tuple of (model_name, provider)
34
+ """
35
+ model_name = "unknown"
36
+ provider = "openai"
37
+
38
+ if not hasattr(agent, "llm"):
39
+ return model_name, provider
40
+
41
+ llm = agent.llm
42
+
43
+ # Case 1: LLM is a string like "openai/gpt-4o-mini" or "gpt-4"
44
+ if isinstance(llm, str):
45
+ if "/" in llm:
46
+ parts = llm.split("/", 1)
47
+ provider = parts[0]
48
+ model_name = parts[1]
49
+ else:
50
+ model_name = llm
51
+ provider = _get_provider_from_model(llm)
52
+ return model_name, provider
53
+
54
+ # Case 2: LLM has model or model_name attribute
55
+ if hasattr(llm, "model"):
56
+ model_name = str(llm.model)
57
+ elif hasattr(llm, "model_name"):
58
+ model_name = str(llm.model_name)
59
+
60
+ # Parse provider from model string if it contains "/"
61
+ if "/" in model_name:
62
+ parts = model_name.split("/", 1)
63
+ provider = parts[0]
64
+ model_name = parts[1]
65
+ else:
66
+ provider = _get_provider_from_model(model_name)
67
+
68
+ return model_name, provider
69
+
70
+
71
+ def _calculate_cost(provider: str, model: str, input_tokens: int, output_tokens: int) -> float:
72
+ """Calculate cost using CostAdapterFactory.
73
+
74
+ Args:
75
+ provider: Provider name (openai, anthropic, etc.)
76
+ model: Model name
77
+ input_tokens: Number of input tokens
78
+ output_tokens: Number of output tokens
79
+
80
+ Returns:
81
+ Cost in USD
82
+ """
83
+ if CostAdapterFactory is None:
84
+ return 0.0
85
+
86
+ try:
87
+ return CostAdapterFactory.compute_cost(provider, model, input_tokens, output_tokens)
88
+ except Exception:
89
+ return 0.0
90
+
91
+
24
92
  class KalibrCrewAIInstrumentor:
25
93
  """Auto-instrumentation for CrewAI.
26
94
 
@@ -84,6 +152,10 @@ class KalibrCrewAIInstrumentor:
84
152
  # Instrumentation state
85
153
  self._is_instrumented = False
86
154
 
155
+ # Accumulated metrics for crew-level aggregation
156
+ self._accumulated_tokens = {"input": 0, "output": 0}
157
+ self._accumulated_cost = 0.0
158
+
87
159
  def instrument(self) -> bool:
88
160
  """Instrument CrewAI classes.
89
161
 
@@ -170,11 +242,22 @@ class KalibrCrewAIInstrumentor:
170
242
  start_time = time.time()
171
243
  ts_start = datetime.now(timezone.utc)
172
244
 
245
+ # Reset accumulators before crew execution
246
+ instrumentor._accumulated_tokens = {"input": 0, "output": 0}
247
+ instrumentor._accumulated_cost = 0.0
248
+
173
249
  # Capture crew info
174
250
  crew_name = getattr(crew_self, "name", None) or "unnamed_crew"
175
- agent_count = len(getattr(crew_self, "agents", []))
251
+ agents = getattr(crew_self, "agents", [])
252
+ agent_count = len(agents)
176
253
  task_count = len(getattr(crew_self, "tasks", []))
177
254
 
255
+ # Extract model from first agent if available
256
+ model_name = "unknown"
257
+ provider = "crewai"
258
+ if agents:
259
+ model_name, provider = _extract_model_from_agent(agents[0])
260
+
178
261
  status = "success"
179
262
  error_type = None
180
263
  error_message = None
@@ -194,12 +277,33 @@ class KalibrCrewAIInstrumentor:
194
277
  duration_ms = int((time.time() - start_time) * 1000)
195
278
  ts_end = datetime.now(timezone.utc)
196
279
 
280
+ # Enrich CrewAI's OTel span with Kalibr telemetry
281
+ try:
282
+ current_span = otel_trace.get_current_span()
283
+ if current_span and current_span.is_recording():
284
+ current_span.set_attribute("kalibr.cost_usd", instrumentor._accumulated_cost)
285
+ current_span.set_attribute("kalibr.input_tokens", instrumentor._accumulated_tokens["input"])
286
+ current_span.set_attribute("kalibr.output_tokens", instrumentor._accumulated_tokens["output"])
287
+ current_span.set_attribute("kalibr.total_tokens", instrumentor._accumulated_tokens["input"] + instrumentor._accumulated_tokens["output"])
288
+ current_span.set_attribute("kalibr.model_id", model_name)
289
+ current_span.set_attribute("kalibr.provider", provider)
290
+ current_span.set_attribute("kalibr.duration_ms", duration_ms)
291
+ current_span.set_attribute("kalibr.tenant_id", instrumentor.tenant_id)
292
+ except Exception:
293
+ pass # Don't fail if span enrichment fails
294
+
197
295
  # Build output info
198
296
  output_preview = None
199
297
  if instrumentor.capture_output and result is not None:
200
298
  output_preview = str(result)[:500]
201
299
 
202
- # Create event
300
+ # Get accumulated metrics from child agent/task executions
301
+ input_tokens = instrumentor._accumulated_tokens["input"]
302
+ output_tokens = instrumentor._accumulated_tokens["output"]
303
+ total_tokens = input_tokens + output_tokens
304
+ cost_usd = instrumentor._accumulated_cost
305
+
306
+ # Create event with aggregated metrics
203
307
  event = {
204
308
  "schema_version": "1.0",
205
309
  "trace_id": trace_id,
@@ -207,18 +311,18 @@ class KalibrCrewAIInstrumentor:
207
311
  "parent_span_id": None,
208
312
  "tenant_id": instrumentor.tenant_id,
209
313
  "workflow_id": instrumentor.workflow_id,
210
- "provider": "crewai",
211
- "model_id": "crew",
212
- "model_name": crew_name,
314
+ "provider": provider,
315
+ "model_id": model_name,
316
+ "model_name": model_name,
213
317
  "operation": f"crew:{crew_name}",
214
318
  "endpoint": "crew.kickoff",
215
319
  "duration_ms": duration_ms,
216
320
  "latency_ms": duration_ms,
217
- "input_tokens": 0,
218
- "output_tokens": 0,
219
- "total_tokens": 0,
220
- "cost_usd": 0.0,
221
- "total_cost_usd": 0.0,
321
+ "input_tokens": input_tokens,
322
+ "output_tokens": output_tokens,
323
+ "total_tokens": total_tokens,
324
+ "cost_usd": cost_usd,
325
+ "total_cost_usd": cost_usd,
222
326
  "status": status,
223
327
  "error_type": error_type,
224
328
  "error_message": error_message,
@@ -255,10 +359,21 @@ class KalibrCrewAIInstrumentor:
255
359
  start_time = time.time()
256
360
  ts_start = datetime.now(timezone.utc)
257
361
 
362
+ # Reset accumulators before crew execution
363
+ instrumentor._accumulated_tokens = {"input": 0, "output": 0}
364
+ instrumentor._accumulated_cost = 0.0
365
+
258
366
  crew_name = getattr(crew_self, "name", None) or "unnamed_crew"
259
- agent_count = len(getattr(crew_self, "agents", []))
367
+ agents = getattr(crew_self, "agents", [])
368
+ agent_count = len(agents)
260
369
  task_count = len(getattr(crew_self, "tasks", []))
261
370
 
371
+ # Extract model from first agent if available
372
+ model_name = "unknown"
373
+ provider = "crewai"
374
+ if agents:
375
+ model_name, provider = _extract_model_from_agent(agents[0])
376
+
262
377
  status = "success"
263
378
  error_type = None
264
379
  error_message = None
@@ -278,10 +393,32 @@ class KalibrCrewAIInstrumentor:
278
393
  duration_ms = int((time.time() - start_time) * 1000)
279
394
  ts_end = datetime.now(timezone.utc)
280
395
 
396
+ # Enrich CrewAI's OTel span with Kalibr telemetry
397
+ try:
398
+ current_span = otel_trace.get_current_span()
399
+ if current_span and current_span.is_recording():
400
+ current_span.set_attribute("kalibr.cost_usd", instrumentor._accumulated_cost)
401
+ current_span.set_attribute("kalibr.input_tokens", instrumentor._accumulated_tokens["input"])
402
+ current_span.set_attribute("kalibr.output_tokens", instrumentor._accumulated_tokens["output"])
403
+ current_span.set_attribute("kalibr.total_tokens", instrumentor._accumulated_tokens["input"] + instrumentor._accumulated_tokens["output"])
404
+ current_span.set_attribute("kalibr.model_id", model_name)
405
+ current_span.set_attribute("kalibr.provider", provider)
406
+ current_span.set_attribute("kalibr.duration_ms", duration_ms)
407
+ current_span.set_attribute("kalibr.tenant_id", instrumentor.tenant_id)
408
+ except Exception:
409
+ pass # Don't fail if span enrichment fails
410
+
281
411
  output_preview = None
282
412
  if instrumentor.capture_output and result is not None:
283
413
  output_preview = str(result)[:500]
284
414
 
415
+ # Get accumulated metrics from child agent/task executions
416
+ input_tokens = instrumentor._accumulated_tokens["input"]
417
+ output_tokens = instrumentor._accumulated_tokens["output"]
418
+ total_tokens = input_tokens + output_tokens
419
+ cost_usd = instrumentor._accumulated_cost
420
+
421
+ # Create event with aggregated metrics
285
422
  event = {
286
423
  "schema_version": "1.0",
287
424
  "trace_id": trace_id,
@@ -289,18 +426,18 @@ class KalibrCrewAIInstrumentor:
289
426
  "parent_span_id": None,
290
427
  "tenant_id": instrumentor.tenant_id,
291
428
  "workflow_id": instrumentor.workflow_id,
292
- "provider": "crewai",
293
- "model_id": "crew",
294
- "model_name": crew_name,
429
+ "provider": provider,
430
+ "model_id": model_name,
431
+ "model_name": model_name,
295
432
  "operation": f"crew:{crew_name}",
296
433
  "endpoint": "crew.kickoff_async",
297
434
  "duration_ms": duration_ms,
298
435
  "latency_ms": duration_ms,
299
- "input_tokens": 0,
300
- "output_tokens": 0,
301
- "total_tokens": 0,
302
- "cost_usd": 0.0,
303
- "total_cost_usd": 0.0,
436
+ "input_tokens": input_tokens,
437
+ "output_tokens": output_tokens,
438
+ "total_tokens": total_tokens,
439
+ "cost_usd": cost_usd,
440
+ "total_cost_usd": cost_usd,
304
441
  "status": status,
305
442
  "error_type": error_type,
306
443
  "error_message": error_message,
@@ -341,6 +478,9 @@ class KalibrCrewAIInstrumentor:
341
478
  role = getattr(agent_self, "role", "unknown")
342
479
  goal = getattr(agent_self, "goal", "")
343
480
 
481
+ # Extract model from agent's LLM config
482
+ model_name, provider = _extract_model_from_agent(agent_self)
483
+
344
484
  # Get task info
345
485
  task_description = ""
346
486
  if hasattr(task, "description"):
@@ -370,8 +510,16 @@ class KalibrCrewAIInstrumentor:
370
510
  output_preview = str(result)[:500]
371
511
 
372
512
  # Token estimation
373
- input_tokens = _count_tokens(task_description + goal, "gpt-4")
374
- output_tokens = _count_tokens(output_preview or "", "gpt-4")
513
+ input_tokens = _count_tokens(task_description + goal, model_name)
514
+ output_tokens = _count_tokens(output_preview or "", model_name)
515
+
516
+ # Calculate cost using CostAdapterFactory
517
+ cost_usd = _calculate_cost(provider, model_name, input_tokens, output_tokens)
518
+
519
+ # Accumulate metrics for crew-level aggregation
520
+ instrumentor._accumulated_tokens["input"] += input_tokens
521
+ instrumentor._accumulated_tokens["output"] += output_tokens
522
+ instrumentor._accumulated_cost += cost_usd
375
523
 
376
524
  event = {
377
525
  "schema_version": "1.0",
@@ -380,9 +528,9 @@ class KalibrCrewAIInstrumentor:
380
528
  "parent_span_id": None,
381
529
  "tenant_id": instrumentor.tenant_id,
382
530
  "workflow_id": instrumentor.workflow_id,
383
- "provider": "crewai",
384
- "model_id": "agent",
385
- "model_name": role,
531
+ "provider": provider,
532
+ "model_id": model_name,
533
+ "model_name": model_name,
386
534
  "operation": f"agent:{role}",
387
535
  "endpoint": "agent.execute_task",
388
536
  "duration_ms": duration_ms,
@@ -390,8 +538,8 @@ class KalibrCrewAIInstrumentor:
390
538
  "input_tokens": input_tokens,
391
539
  "output_tokens": output_tokens,
392
540
  "total_tokens": input_tokens + output_tokens,
393
- "cost_usd": 0.0,
394
- "total_cost_usd": 0.0,
541
+ "cost_usd": cost_usd,
542
+ "total_cost_usd": cost_usd,
395
543
  "status": status,
396
544
  "error_type": error_type,
397
545
  "error_message": error_message,
@@ -430,6 +578,13 @@ class KalibrCrewAIInstrumentor:
430
578
  description = getattr(task_self, "description", "")
431
579
  expected_output = getattr(task_self, "expected_output", "")
432
580
 
581
+ # Try to extract model from task's agent
582
+ model_name = "unknown"
583
+ provider = "openai"
584
+ agent = getattr(task_self, "agent", None)
585
+ if agent:
586
+ model_name, provider = _extract_model_from_agent(agent)
587
+
433
588
  status = "success"
434
589
  error_type = None
435
590
  error_message = None
@@ -456,8 +611,16 @@ class KalibrCrewAIInstrumentor:
456
611
  else:
457
612
  output_preview = str(result)[:500]
458
613
 
459
- input_tokens = _count_tokens(description, "gpt-4")
460
- output_tokens = _count_tokens(output_preview or "", "gpt-4")
614
+ input_tokens = _count_tokens(description, model_name)
615
+ output_tokens = _count_tokens(output_preview or "", model_name)
616
+
617
+ # Calculate cost using CostAdapterFactory
618
+ cost_usd = _calculate_cost(provider, model_name, input_tokens, output_tokens)
619
+
620
+ # Accumulate metrics for crew-level aggregation
621
+ instrumentor._accumulated_tokens["input"] += input_tokens
622
+ instrumentor._accumulated_tokens["output"] += output_tokens
623
+ instrumentor._accumulated_cost += cost_usd
461
624
 
462
625
  event = {
463
626
  "schema_version": "1.0",
@@ -466,9 +629,9 @@ class KalibrCrewAIInstrumentor:
466
629
  "parent_span_id": None,
467
630
  "tenant_id": instrumentor.tenant_id,
468
631
  "workflow_id": instrumentor.workflow_id,
469
- "provider": "crewai",
470
- "model_id": "task",
471
- "model_name": "crewai-task",
632
+ "provider": provider,
633
+ "model_id": model_name,
634
+ "model_name": model_name,
472
635
  "operation": f"task:{description[:30]}..." if len(description) > 30 else f"task:{description}",
473
636
  "endpoint": "task.execute_sync",
474
637
  "duration_ms": duration_ms,
@@ -476,8 +639,8 @@ class KalibrCrewAIInstrumentor:
476
639
  "input_tokens": input_tokens,
477
640
  "output_tokens": output_tokens,
478
641
  "total_tokens": input_tokens + output_tokens,
479
- "cost_usd": 0.0,
480
- "total_cost_usd": 0.0,
642
+ "cost_usd": cost_usd,
643
+ "total_cost_usd": cost_usd,
481
644
  "status": status,
482
645
  "error_type": error_type,
483
646
  "error_message": error_message,
@@ -1,4 +1,4 @@
1
- """Kalibr LangChain Integration - Observability for LangChain applications.
1
+ """Kalibr LangChain Integration - Observability and Routing for LangChain applications.
2
2
 
3
3
  This package provides a callback handler that integrates LangChain with
4
4
  Kalibr's observability platform, capturing:
@@ -29,7 +29,7 @@ Usage:
29
29
 
30
30
  Environment Variables:
31
31
  KALIBR_API_KEY: API key for authentication
32
- KALIBR_ENDPOINT: Backend endpoint URL
32
+ KALIBR_COLLECTOR_URL: Backend endpoint URL
33
33
  KALIBR_TENANT_ID: Tenant identifier
34
34
  KALIBR_ENVIRONMENT: Environment (prod/staging/dev)
35
35
  KALIBR_SERVICE: Service name
@@ -39,9 +39,11 @@ __version__ = "0.1.0"
39
39
 
40
40
  from .callback import KalibrCallbackHandler
41
41
  from .async_callback import AsyncKalibrCallbackHandler
42
+ from .chat_model import KalibrChatModel
42
43
 
43
44
  __all__ = [
44
45
  "KalibrCallbackHandler",
45
46
  "AsyncKalibrCallbackHandler",
47
+ "KalibrChatModel",
46
48
  "__version__",
47
49
  ]