kalibr 1.2.6__py3-none-any.whl → 1.2.9__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.
@@ -3,8 +3,11 @@ Anthropic SDK Instrumentation
3
3
 
4
4
  Monkey-patches the Anthropic SDK to automatically emit OpenTelemetry spans
5
5
  for all message API calls.
6
+
7
+ Thread-safe singleton pattern using double-checked locking.
6
8
  """
7
9
 
10
+ import threading
8
11
  import time
9
12
  from functools import wraps
10
13
  from typing import Any, Dict, Optional
@@ -15,50 +18,34 @@ from .base import BaseCostAdapter, BaseInstrumentation
15
18
 
16
19
 
17
20
  class AnthropicCostAdapter(BaseCostAdapter):
18
- """Cost calculation adapter for Anthropic models"""
19
-
20
- # Pricing per 1K tokens (USD) - Updated November 2025
21
- PRICING = {
22
- # Claude 4 models
23
- "claude-4-opus": {"input": 0.015, "output": 0.075},
24
- "claude-4-sonnet": {"input": 0.003, "output": 0.015},
25
- # Claude 3 models (Sonnet 4 is actually Claude 3.7)
26
- "claude-sonnet-4": {"input": 0.003, "output": 0.015},
27
- "claude-3-7-sonnet": {"input": 0.003, "output": 0.015},
28
- "claude-3-5-sonnet": {"input": 0.003, "output": 0.015},
29
- "claude-3-opus": {"input": 0.015, "output": 0.075},
30
- "claude-3-sonnet": {"input": 0.003, "output": 0.015},
31
- "claude-3-haiku": {"input": 0.00025, "output": 0.00125},
32
- # Claude 2 models
33
- "claude-2.1": {"input": 0.008, "output": 0.024},
34
- "claude-2.0": {"input": 0.008, "output": 0.024},
35
- "claude-instant-1.2": {"input": 0.0008, "output": 0.0024},
36
- }
37
-
38
- def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
39
- """Calculate cost in USD for an Anthropic API call"""
40
- # Normalize model name
41
- base_model = model.lower()
21
+ """Cost calculation adapter for Anthropic models.
22
+
23
+ Uses centralized pricing from kalibr.pricing module.
24
+ """
42
25
 
43
- # Try exact match first
44
- pricing = self.get_pricing(base_model)
26
+ def get_vendor_name(self) -> str:
27
+ """Return vendor name for Anthropic."""
28
+ return "anthropic"
45
29
 
46
- # Try fuzzy matching for versioned models
47
- if not pricing:
48
- for known_model in self.PRICING.keys():
49
- if known_model in base_model or base_model in known_model:
50
- pricing = self.PRICING[known_model]
51
- break
52
-
53
- if not pricing:
54
- # Default to Claude 3 Sonnet pricing if unknown
55
- pricing = {"input": 0.003, "output": 0.015}
30
+ def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
31
+ """Calculate cost in USD for an Anthropic API call.
32
+
33
+ Args:
34
+ model: Model identifier (e.g., "claude-3-opus", "claude-3-5-sonnet-20240620")
35
+ usage: Token usage dict with input_tokens and output_tokens
36
+
37
+ Returns:
38
+ Cost in USD (rounded to 6 decimal places)
39
+ """
40
+ # Get pricing from centralized module (handles normalization)
41
+ pricing = self.get_pricing_for_model(model)
56
42
 
57
43
  input_tokens = usage.get("input_tokens", 0)
58
44
  output_tokens = usage.get("output_tokens", 0)
59
45
 
60
- input_cost = (input_tokens / 1000) * pricing["input"]
61
- output_cost = (output_tokens / 1000) * pricing["output"]
46
+ # Calculate cost (pricing is per 1M tokens)
47
+ input_cost = (input_tokens / 1_000_000) * pricing["input"]
48
+ output_cost = (output_tokens / 1_000_000) * pricing["output"]
62
49
 
63
50
  return round(input_cost + output_cost, 6)
64
51
 
@@ -262,13 +249,20 @@ class AnthropicInstrumentation(BaseInstrumentation):
262
249
 
263
250
  # Singleton instance
264
251
  _anthropic_instrumentation = None
252
+ _anthropic_lock = threading.Lock()
265
253
 
266
254
 
267
255
  def get_instrumentation() -> AnthropicInstrumentation:
268
- """Get or create the Anthropic instrumentation singleton"""
256
+ """Get or create the Anthropic instrumentation singleton.
257
+
258
+ Thread-safe singleton pattern using double-checked locking.
259
+ """
269
260
  global _anthropic_instrumentation
270
261
  if _anthropic_instrumentation is None:
271
- _anthropic_instrumentation = AnthropicInstrumentation()
262
+ with _anthropic_lock:
263
+ # Double-check inside lock to prevent race condition
264
+ if _anthropic_instrumentation is None:
265
+ _anthropic_instrumentation = AnthropicInstrumentation()
272
266
  return _anthropic_instrumentation
273
267
 
274
268
 
@@ -3,6 +3,8 @@ Base instrumentation class for LLM SDKs
3
3
 
4
4
  Provides common functionality for monkey-patching LLM SDKs and
5
5
  emitting OpenTelemetry-compatible spans.
6
+
7
+ Note: Cost adapters now use centralized pricing from kalibr.pricing module.
6
8
  """
7
9
 
8
10
  import time
@@ -13,6 +15,8 @@ from typing import Any, Dict, Optional
13
15
  from opentelemetry import trace
14
16
  from opentelemetry.trace import SpanKind, Status, StatusCode
15
17
 
18
+ from kalibr.pricing import get_pricing
19
+
16
20
 
17
21
  class BaseInstrumentation(ABC):
18
22
  """Base class for LLM SDK instrumentation"""
@@ -76,9 +80,11 @@ class BaseInstrumentation(ABC):
76
80
 
77
81
 
78
82
  class BaseCostAdapter(ABC):
79
- """Base class for cost calculation adapters"""
80
-
81
- PRICING: Dict[str, Dict[str, float]] = {}
83
+ """Base class for cost calculation adapters.
84
+
85
+ Uses centralized pricing from kalibr.pricing module.
86
+ All subclasses must implement get_vendor_name() to specify their vendor.
87
+ """
82
88
 
83
89
  @abstractmethod
84
90
  def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
@@ -87,22 +93,34 @@ class BaseCostAdapter(ABC):
87
93
 
88
94
  Args:
89
95
  model: Model identifier (e.g., "gpt-4")
90
- usage: Token usage dictionary with prompt_tokens, completion_tokens
96
+ usage: Token usage dictionary with prompt_tokens, completion_tokens,
97
+ input_tokens, or output_tokens
91
98
 
92
99
  Returns:
93
100
  Cost in USD (rounded to 6 decimal places)
94
101
  """
95
102
  pass
96
103
 
97
- def get_pricing(self, model: str) -> Optional[Dict[str, float]]:
104
+ @abstractmethod
105
+ def get_vendor_name(self) -> str:
106
+ """
107
+ Get the vendor name for this adapter.
108
+
109
+ Returns:
110
+ Vendor name (e.g., "openai", "anthropic", "google")
111
+ """
112
+ pass
113
+
114
+ def get_pricing_for_model(self, model: str) -> Dict[str, float]:
98
115
  """
99
- Get pricing for a specific model
116
+ Get pricing for a specific model using centralized pricing.
100
117
 
101
118
  Args:
102
119
  model: Model identifier
103
120
 
104
121
  Returns:
105
- Dictionary with "input" and "output" prices per 1K tokens,
106
- or None if model not found
122
+ Dictionary with "input" and "output" prices per 1M tokens
107
123
  """
108
- return self.PRICING.get(model)
124
+ vendor = self.get_vendor_name()
125
+ pricing, _ = get_pricing(vendor, model)
126
+ return pricing
@@ -3,8 +3,11 @@ Google Generative AI SDK Instrumentation
3
3
 
4
4
  Monkey-patches the Google Generative AI SDK to automatically emit OpenTelemetry spans
5
5
  for all content generation API calls.
6
+
7
+ Thread-safe singleton pattern using double-checked locking.
6
8
  """
7
9
 
10
+ import threading
8
11
  import time
9
12
  from functools import wraps
10
13
  from typing import Any, Dict, Optional
@@ -15,49 +18,34 @@ from .base import BaseCostAdapter, BaseInstrumentation
15
18
 
16
19
 
17
20
  class GoogleCostAdapter(BaseCostAdapter):
18
- """Cost calculation adapter for Google Generative AI models"""
19
-
20
- # Pricing per 1K tokens (USD) - Updated November 2025
21
- PRICING = {
22
- # Gemini 2.5 models
23
- "gemini-2.5-pro": {"input": 0.00125, "output": 0.005},
24
- "gemini-2.5-flash": {"input": 0.000075, "output": 0.0003},
25
- # Gemini 2.0 models
26
- "gemini-2.0-flash": {"input": 0.000075, "output": 0.0003},
27
- "gemini-2.0-flash-thinking": {"input": 0.000075, "output": 0.0003},
28
- # Gemini 1.5 models
29
- "gemini-1.5-pro": {"input": 0.00125, "output": 0.005},
30
- "gemini-1.5-flash": {"input": 0.000075, "output": 0.0003},
31
- "gemini-1.5-flash-8b": {"input": 0.0000375, "output": 0.00015},
32
- # Gemini 1.0 models
33
- "gemini-1.0-pro": {"input": 0.0005, "output": 0.0015},
34
- "gemini-pro": {"input": 0.0005, "output": 0.0015}, # Alias
35
- }
36
-
37
- def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
38
- """Calculate cost in USD for a Google Generative AI API call"""
39
- # Normalize model name
40
- base_model = model.lower()
21
+ """Cost calculation adapter for Google Generative AI models.
22
+
23
+ Uses centralized pricing from kalibr.pricing module.
24
+ """
41
25
 
42
- # Try exact match first
43
- pricing = self.get_pricing(base_model)
26
+ def get_vendor_name(self) -> str:
27
+ """Return vendor name for Google."""
28
+ return "google"
44
29
 
45
- # Try fuzzy matching for versioned models
46
- if not pricing:
47
- for known_model in self.PRICING.keys():
48
- if known_model in base_model or base_model in known_model:
49
- pricing = self.PRICING[known_model]
50
- break
51
-
52
- if not pricing:
53
- # Default to Gemini 1.5 Pro pricing if unknown
54
- pricing = {"input": 0.00125, "output": 0.005}
30
+ def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
31
+ """Calculate cost in USD for a Google Generative AI API call.
32
+
33
+ Args:
34
+ model: Model identifier (e.g., "gemini-1.5-pro", "gemini-2.0-flash")
35
+ usage: Token usage dict with prompt_tokens and completion_tokens
36
+
37
+ Returns:
38
+ Cost in USD (rounded to 6 decimal places)
39
+ """
40
+ # Get pricing from centralized module (handles normalization)
41
+ pricing = self.get_pricing_for_model(model)
55
42
 
56
43
  prompt_tokens = usage.get("prompt_tokens", 0)
57
44
  completion_tokens = usage.get("completion_tokens", 0)
58
45
 
59
- input_cost = (prompt_tokens / 1000) * pricing["input"]
60
- output_cost = (completion_tokens / 1000) * pricing["output"]
46
+ # Calculate cost (pricing is per 1M tokens)
47
+ input_cost = (prompt_tokens / 1_000_000) * pricing["input"]
48
+ output_cost = (completion_tokens / 1_000_000) * pricing["output"]
61
49
 
62
50
  return round(input_cost + output_cost, 6)
63
51
 
@@ -261,13 +249,20 @@ class GoogleInstrumentation(BaseInstrumentation):
261
249
 
262
250
  # Singleton instance
263
251
  _google_instrumentation = None
252
+ _google_lock = threading.Lock()
264
253
 
265
254
 
266
255
  def get_instrumentation() -> GoogleInstrumentation:
267
- """Get or create the Google instrumentation singleton"""
256
+ """Get or create the Google instrumentation singleton.
257
+
258
+ Thread-safe singleton pattern using double-checked locking.
259
+ """
268
260
  global _google_instrumentation
269
261
  if _google_instrumentation is None:
270
- _google_instrumentation = GoogleInstrumentation()
262
+ with _google_lock:
263
+ # Double-check inside lock to prevent race condition
264
+ if _google_instrumentation is None:
265
+ _google_instrumentation = GoogleInstrumentation()
271
266
  return _google_instrumentation
272
267
 
273
268
 
@@ -3,8 +3,11 @@ OpenAI SDK Instrumentation
3
3
 
4
4
  Monkey-patches the OpenAI SDK to automatically emit OpenTelemetry spans
5
5
  for all chat completion API calls.
6
+
7
+ Thread-safe singleton pattern using double-checked locking.
6
8
  """
7
9
 
10
+ import threading
8
11
  import time
9
12
  from functools import wraps
10
13
  from typing import Any, Dict, Optional
@@ -15,38 +18,34 @@ from .base import BaseCostAdapter, BaseInstrumentation
15
18
 
16
19
 
17
20
  class OpenAICostAdapter(BaseCostAdapter):
18
- """Cost calculation adapter for OpenAI models"""
19
-
20
- # Pricing per 1K tokens (USD) - Updated November 2025
21
- PRICING = {
22
- # GPT-5 models
23
- "gpt-5": {"input": 0.005, "output": 0.015},
24
- "gpt-5-turbo": {"input": 0.0025, "output": 0.0075},
25
- # GPT-4 models
26
- "gpt-4": {"input": 0.03, "output": 0.06},
27
- "gpt-4-turbo": {"input": 0.01, "output": 0.03},
28
- "gpt-4o": {"input": 0.0025, "output": 0.01},
29
- "gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
30
- # GPT-3.5 models
31
- "gpt-3.5-turbo": {"input": 0.0005, "output": 0.0015},
32
- "gpt-3.5-turbo-16k": {"input": 0.001, "output": 0.002},
33
- }
21
+ """Cost calculation adapter for OpenAI models.
22
+
23
+ Uses centralized pricing from kalibr.pricing module.
24
+ """
34
25
 
35
- def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
36
- """Calculate cost in USD for an OpenAI API call"""
37
- # Normalize model name (remove version suffixes)
38
- base_model = model.split("-2")[0] # Remove date suffixes like -20240101
26
+ def get_vendor_name(self) -> str:
27
+ """Return vendor name for OpenAI."""
28
+ return "openai"
39
29
 
40
- pricing = self.get_pricing(base_model)
41
- if not pricing:
42
- # Default to GPT-4 pricing if unknown
43
- pricing = {"input": 0.03, "output": 0.06}
30
+ def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
31
+ """Calculate cost in USD for an OpenAI API call.
32
+
33
+ Args:
34
+ model: Model identifier (e.g., "gpt-4o", "gpt-4o-2024-05-13")
35
+ usage: Token usage dict with prompt_tokens and completion_tokens
36
+
37
+ Returns:
38
+ Cost in USD (rounded to 6 decimal places)
39
+ """
40
+ # Get pricing from centralized module (handles normalization)
41
+ pricing = self.get_pricing_for_model(model)
44
42
 
45
43
  prompt_tokens = usage.get("prompt_tokens", 0)
46
44
  completion_tokens = usage.get("completion_tokens", 0)
47
45
 
48
- input_cost = (prompt_tokens / 1000) * pricing["input"]
49
- output_cost = (completion_tokens / 1000) * pricing["output"]
46
+ # Calculate cost (pricing is per 1M tokens)
47
+ input_cost = (prompt_tokens / 1_000_000) * pricing["input"]
48
+ output_cost = (completion_tokens / 1_000_000) * pricing["output"]
50
49
 
51
50
  return round(input_cost + output_cost, 6)
52
51
 
@@ -245,13 +244,20 @@ class OpenAIInstrumentation(BaseInstrumentation):
245
244
 
246
245
  # Singleton instance
247
246
  _openai_instrumentation = None
247
+ _openai_lock = threading.Lock()
248
248
 
249
249
 
250
250
  def get_instrumentation() -> OpenAIInstrumentation:
251
- """Get or create the OpenAI instrumentation singleton"""
251
+ """Get or create the OpenAI instrumentation singleton.
252
+
253
+ Thread-safe singleton pattern using double-checked locking.
254
+ """
252
255
  global _openai_instrumentation
253
256
  if _openai_instrumentation is None:
254
- _openai_instrumentation = OpenAIInstrumentation()
257
+ with _openai_lock:
258
+ # Double-check inside lock to prevent race condition
259
+ if _openai_instrumentation is None:
260
+ _openai_instrumentation = OpenAIInstrumentation()
255
261
  return _openai_instrumentation
256
262
 
257
263
 
@@ -3,18 +3,25 @@ Instrumentation Registry
3
3
 
4
4
  Handles auto-discovery and registration of LLM SDK instrumentations.
5
5
  Provides a central place to manage which SDKs are instrumented.
6
+
7
+ Thread-safe registry using locks to protect shared state.
6
8
  """
7
9
 
8
10
  import os
11
+ import threading
9
12
  from typing import Dict, List, Set
10
13
 
11
14
  # Track which providers have been instrumented
12
15
  _instrumented_providers: Set[str] = set()
16
+ # Lock to protect concurrent access to the registry
17
+ _registry_lock = threading.Lock()
13
18
 
14
19
 
15
20
  def auto_instrument(providers: List[str] = None) -> Dict[str, bool]:
16
21
  """
17
22
  Auto-discover and instrument LLM SDKs
23
+
24
+ Thread-safe: Uses internal lock to protect registry state.
18
25
 
19
26
  Args:
20
27
  providers: List of provider names to instrument.
@@ -35,10 +42,11 @@ def auto_instrument(providers: List[str] = None) -> Dict[str, bool]:
35
42
  for provider in providers:
36
43
  provider_lower = provider.lower()
37
44
 
38
- # Skip if already instrumented
39
- if provider_lower in _instrumented_providers:
40
- results[provider_lower] = True
41
- continue
45
+ # Check if already instrumented (thread-safe read)
46
+ with _registry_lock:
47
+ if provider_lower in _instrumented_providers:
48
+ results[provider_lower] = True
49
+ continue
42
50
 
43
51
  try:
44
52
  if provider_lower == "openai":
@@ -47,7 +55,8 @@ def auto_instrument(providers: List[str] = None) -> Dict[str, bool]:
47
55
  success = openai_instr.instrument()
48
56
  results[provider_lower] = success
49
57
  if success:
50
- _instrumented_providers.add(provider_lower)
58
+ with _registry_lock:
59
+ _instrumented_providers.add(provider_lower)
51
60
  print(f"✅ Instrumented OpenAI SDK")
52
61
 
53
62
  elif provider_lower == "anthropic":
@@ -56,7 +65,8 @@ def auto_instrument(providers: List[str] = None) -> Dict[str, bool]:
56
65
  success = anthropic_instr.instrument()
57
66
  results[provider_lower] = success
58
67
  if success:
59
- _instrumented_providers.add(provider_lower)
68
+ with _registry_lock:
69
+ _instrumented_providers.add(provider_lower)
60
70
  print(f"✅ Instrumented Anthropic SDK")
61
71
 
62
72
  elif provider_lower == "google":
@@ -65,7 +75,8 @@ def auto_instrument(providers: List[str] = None) -> Dict[str, bool]:
65
75
  success = google_instr.instrument()
66
76
  results[provider_lower] = success
67
77
  if success:
68
- _instrumented_providers.add(provider_lower)
78
+ with _registry_lock:
79
+ _instrumented_providers.add(provider_lower)
69
80
  print(f"✅ Instrumented Google Generative AI SDK")
70
81
 
71
82
  else:
@@ -85,6 +96,8 @@ def auto_instrument(providers: List[str] = None) -> Dict[str, bool]:
85
96
  def uninstrument_all() -> Dict[str, bool]:
86
97
  """
87
98
  Remove instrumentation from all previously instrumented SDKs
99
+
100
+ Thread-safe: Uses internal lock to protect registry state.
88
101
 
89
102
  Returns:
90
103
  Dictionary mapping provider names to uninstrumentation success status
@@ -92,7 +105,10 @@ def uninstrument_all() -> Dict[str, bool]:
92
105
  global _instrumented_providers
93
106
 
94
107
  results = {}
95
- providers_to_uninstrument = list(_instrumented_providers)
108
+
109
+ # Get snapshot of providers to uninstrument (thread-safe)
110
+ with _registry_lock:
111
+ providers_to_uninstrument = list(_instrumented_providers)
96
112
 
97
113
  for provider in providers_to_uninstrument:
98
114
  try:
@@ -102,7 +118,8 @@ def uninstrument_all() -> Dict[str, bool]:
102
118
  success = openai_instr.uninstrument()
103
119
  results[provider] = success
104
120
  if success:
105
- _instrumented_providers.discard(provider)
121
+ with _registry_lock:
122
+ _instrumented_providers.discard(provider)
106
123
  print(f"✅ Uninstrumented OpenAI SDK")
107
124
 
108
125
  elif provider == "anthropic":
@@ -111,7 +128,8 @@ def uninstrument_all() -> Dict[str, bool]:
111
128
  success = anthropic_instr.uninstrument()
112
129
  results[provider] = success
113
130
  if success:
114
- _instrumented_providers.discard(provider)
131
+ with _registry_lock:
132
+ _instrumented_providers.discard(provider)
115
133
  print(f"✅ Uninstrumented Anthropic SDK")
116
134
 
117
135
  elif provider == "google":
@@ -120,7 +138,8 @@ def uninstrument_all() -> Dict[str, bool]:
120
138
  success = google_instr.uninstrument()
121
139
  results[provider] = success
122
140
  if success:
123
- _instrumented_providers.discard(provider)
141
+ with _registry_lock:
142
+ _instrumented_providers.discard(provider)
124
143
  print(f"✅ Uninstrumented Google Generative AI SDK")
125
144
 
126
145
  except Exception as e:
@@ -133,16 +152,21 @@ def uninstrument_all() -> Dict[str, bool]:
133
152
  def get_instrumented_providers() -> List[str]:
134
153
  """
135
154
  Get list of currently instrumented providers
155
+
156
+ Thread-safe: Returns a snapshot of the current state.
136
157
 
137
158
  Returns:
138
159
  List of provider names that are currently instrumented
139
160
  """
140
- return list(_instrumented_providers)
161
+ with _registry_lock:
162
+ return list(_instrumented_providers)
141
163
 
142
164
 
143
165
  def is_instrumented(provider: str) -> bool:
144
166
  """
145
167
  Check if a specific provider is instrumented
168
+
169
+ Thread-safe: Protected by internal lock.
146
170
 
147
171
  Args:
148
172
  provider: Provider name to check
@@ -150,4 +174,5 @@ def is_instrumented(provider: str) -> bool:
150
174
  Returns:
151
175
  True if provider is instrumented, False otherwise
152
176
  """
153
- return provider.lower() in _instrumented_providers
177
+ with _registry_lock:
178
+ return provider.lower() in _instrumented_providers
kalibr/intelligence.py CHANGED
@@ -33,6 +33,7 @@ Example - Path registration and intelligent routing:
33
33
  from __future__ import annotations
34
34
 
35
35
  import os
36
+ import threading
36
37
  from typing import Any, Optional
37
38
 
38
39
  import httpx
@@ -153,6 +154,7 @@ class KalibrIntelligence:
153
154
  metadata: dict | None = None,
154
155
  tool_id: str | None = None,
155
156
  execution_params: dict | None = None,
157
+ model_id: str | None = None,
156
158
  ) -> dict[str, Any]:
157
159
  """Report execution outcome for a goal.
158
160
 
@@ -202,6 +204,7 @@ class KalibrIntelligence:
202
204
  "metadata": metadata,
203
205
  "tool_id": tool_id,
204
206
  "execution_params": execution_params,
207
+ "model_id": model_id,
205
208
  },
206
209
  )
207
210
  return response.json()
@@ -507,13 +510,20 @@ class KalibrIntelligence:
507
510
 
508
511
  # Module-level singleton for convenience functions
509
512
  _intelligence_client: KalibrIntelligence | None = None
513
+ _client_lock = threading.Lock()
510
514
 
511
515
 
512
516
  def _get_intelligence_client() -> KalibrIntelligence:
513
- """Get or create the singleton intelligence client."""
517
+ """Get or create the singleton intelligence client.
518
+
519
+ Thread-safe singleton pattern using double-checked locking.
520
+ """
514
521
  global _intelligence_client
515
522
  if _intelligence_client is None:
516
- _intelligence_client = KalibrIntelligence()
523
+ with _client_lock:
524
+ # Double-check inside lock to prevent race condition
525
+ if _intelligence_client is None:
526
+ _intelligence_client = KalibrIntelligence()
517
527
  return _intelligence_client
518
528
 
519
529
 
@@ -537,11 +547,11 @@ def get_policy(goal: str, tenant_id: str | None = None, **kwargs) -> dict[str, A
537
547
  policy = get_policy(goal="book_meeting")
538
548
  model = policy["recommended_model"]
539
549
  """
540
- client = _get_intelligence_client()
541
550
  if tenant_id:
542
- # Create a new client with the specified tenant_id
543
- client = KalibrIntelligence(tenant_id=tenant_id)
544
- return client.get_policy(goal, **kwargs)
551
+ # Use context manager to ensure client is properly closed
552
+ with KalibrIntelligence(tenant_id=tenant_id) as client:
553
+ return client.get_policy(goal, **kwargs)
554
+ return _get_intelligence_client().get_policy(goal, **kwargs)
545
555
 
546
556
 
547
557
  def report_outcome(trace_id: str, goal: str, success: bool, tenant_id: str | None = None, **kwargs) -> dict[str, Any]:
@@ -565,11 +575,11 @@ def report_outcome(trace_id: str, goal: str, success: bool, tenant_id: str | Non
565
575
 
566
576
  report_outcome(trace_id="abc123", goal="book_meeting", success=True)
567
577
  """
568
- client = _get_intelligence_client()
569
578
  if tenant_id:
570
- # Create a new client with the specified tenant_id
571
- client = KalibrIntelligence(tenant_id=tenant_id)
572
- return client.report_outcome(trace_id, goal, success, **kwargs)
579
+ # Use context manager to ensure client is properly closed
580
+ with KalibrIntelligence(tenant_id=tenant_id) as client:
581
+ return client.report_outcome(trace_id, goal, success, **kwargs)
582
+ return _get_intelligence_client().report_outcome(trace_id, goal, success, **kwargs)
573
583
 
574
584
 
575
585
  def get_recommendation(task_type: str, **kwargs) -> dict[str, Any]:
@@ -614,10 +624,11 @@ def register_path(
614
624
  tool_id="calendar_tool"
615
625
  )
616
626
  """
617
- client = _get_intelligence_client()
618
627
  if tenant_id:
619
- client = KalibrIntelligence(tenant_id=tenant_id)
620
- return client.register_path(goal, model_id, tool_id, params, risk_level)
628
+ # Use context manager to ensure client is properly closed
629
+ with KalibrIntelligence(tenant_id=tenant_id) as client:
630
+ return client.register_path(goal, model_id, tool_id, params, risk_level)
631
+ return _get_intelligence_client().register_path(goal, model_id, tool_id, params, risk_level)
621
632
 
622
633
 
623
634
  def decide(
@@ -644,7 +655,8 @@ def decide(
644
655
  decision = decide(goal="book_meeting")
645
656
  model = decision["model_id"]
646
657
  """
647
- client = _get_intelligence_client()
648
658
  if tenant_id:
649
- client = KalibrIntelligence(tenant_id=tenant_id)
650
- return client.decide(goal, task_risk_level)
659
+ # Use context manager to ensure client is properly closed
660
+ with KalibrIntelligence(tenant_id=tenant_id) as client:
661
+ return client.decide(goal, task_risk_level)
662
+ return _get_intelligence_client().decide(goal, task_risk_level)
@@ -54,7 +54,7 @@ class AutoTracerMiddleware(BaseHTTPMiddleware):
54
54
 
55
55
  # Collector config
56
56
  self.collector_url = collector_url or os.getenv(
57
- "KALIBR_COLLECTOR_URL", "https://api.kalibr.systems/api/ingest"
57
+ "KALIBR_COLLECTOR_URL", "https://kalibr-backend.fly.dev/api/ingest"
58
58
  )
59
59
  self.api_key = api_key or os.getenv("KALIBR_API_KEY", "")
60
60
  self.tenant_id = tenant_id or os.getenv("KALIBR_TENANT_ID", "default")