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.
- kalibr/__init__.py +2 -2
- kalibr/cli/capsule_cmd.py +3 -3
- kalibr/cli/run.py +2 -2
- kalibr/client.py +1 -1
- kalibr/collector.py +227 -48
- kalibr/cost_adapter.py +36 -104
- kalibr/instrumentation/anthropic_instr.py +34 -40
- kalibr/instrumentation/base.py +27 -9
- kalibr/instrumentation/google_instr.py +34 -39
- kalibr/instrumentation/openai_instr.py +34 -28
- kalibr/instrumentation/registry.py +38 -13
- kalibr/intelligence.py +28 -16
- kalibr/middleware/auto_tracer.py +1 -1
- kalibr/pricing.py +245 -0
- kalibr/router.py +144 -34
- kalibr/simple_tracer.py +16 -15
- kalibr/trace_capsule.py +19 -12
- kalibr/utils.py +2 -2
- {kalibr-1.2.6.dist-info → kalibr-1.2.9.dist-info}/METADATA +114 -14
- {kalibr-1.2.6.dist-info → kalibr-1.2.9.dist-info}/RECORD +24 -23
- {kalibr-1.2.6.dist-info → kalibr-1.2.9.dist-info}/LICENSE +0 -0
- {kalibr-1.2.6.dist-info → kalibr-1.2.9.dist-info}/WHEEL +0 -0
- {kalibr-1.2.6.dist-info → kalibr-1.2.9.dist-info}/entry_points.txt +0 -0
- {kalibr-1.2.6.dist-info → kalibr-1.2.9.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
44
|
-
|
|
26
|
+
def get_vendor_name(self) -> str:
|
|
27
|
+
"""Return vendor name for Anthropic."""
|
|
28
|
+
return "anthropic"
|
|
45
29
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
|
kalibr/instrumentation/base.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
or None if model not found
|
|
122
|
+
Dictionary with "input" and "output" prices per 1M tokens
|
|
107
123
|
"""
|
|
108
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
43
|
-
|
|
26
|
+
def get_vendor_name(self) -> str:
|
|
27
|
+
"""Return vendor name for Google."""
|
|
28
|
+
return "google"
|
|
44
29
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
36
|
-
"""
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
#
|
|
571
|
-
|
|
572
|
-
|
|
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
|
|
620
|
-
|
|
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
|
|
650
|
-
|
|
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)
|
kalibr/middleware/auto_tracer.py
CHANGED
|
@@ -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://
|
|
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")
|