kalibr 1.2.7__tar.gz → 1.2.9__tar.gz
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-1.2.7 → kalibr-1.2.9}/PKG-INFO +58 -1
- {kalibr-1.2.7 → kalibr-1.2.9}/README.md +57 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/collector.py +74 -58
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/cost_adapter.py +36 -104
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/instrumentation/anthropic_instr.py +34 -40
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/instrumentation/base.py +27 -9
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/instrumentation/google_instr.py +34 -39
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/instrumentation/openai_instr.py +34 -28
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/instrumentation/registry.py +38 -13
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/intelligence.py +26 -16
- kalibr-1.2.9/kalibr/pricing.py +245 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/router.py +126 -50
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/simple_tracer.py +15 -14
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/trace_capsule.py +19 -12
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr.egg-info/PKG-INFO +58 -1
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr.egg-info/SOURCES.txt +6 -1
- {kalibr-1.2.7 → kalibr-1.2.9}/pyproject.toml +1 -1
- kalibr-1.2.9/tests/test_cost_adapter.py +218 -0
- kalibr-1.2.9/tests/test_http_client_leak.py +298 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/tests/test_instrumentation.py +94 -6
- kalibr-1.2.9/tests/test_pricing.py +268 -0
- kalibr-1.2.9/tests/test_thread_safety.py +501 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/LICENSE +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/__init__.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/__main__.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/capsule_middleware.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/cli/__init__.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/cli/capsule_cmd.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/cli/deploy_cmd.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/cli/main.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/cli/run.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/cli/serve.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/client.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/context.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/decorators.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/instrumentation/__init__.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/kalibr.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/kalibr_app.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/middleware/__init__.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/middleware/auto_tracer.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/models.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/redaction.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/schemas.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/tokens.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/trace_models.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/tracer.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/types.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr/utils.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr.egg-info/dependency_links.txt +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr.egg-info/entry_points.txt +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr.egg-info/requires.txt +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr.egg-info/top_level.txt +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr_crewai/__init__.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr_crewai/callbacks.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr_crewai/instrumentor.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr_langchain/__init__.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr_langchain/async_callback.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr_langchain/callback.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr_langchain/chat_model.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr_openai_agents/__init__.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/kalibr_openai_agents/processor.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/setup.cfg +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/tests/test_capsule_builder.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/tests/test_intelligence.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/tests/test_langchain_routing.py +0 -0
- {kalibr-1.2.7 → kalibr-1.2.9}/tests/test_router.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: kalibr
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.9
|
|
4
4
|
Summary: Adaptive routing for AI agents. Learns which models work best and routes automatically.
|
|
5
5
|
Author-email: Kalibr Team <support@kalibr.systems>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -123,6 +123,63 @@ paths = [
|
|
|
123
123
|
]
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
## Advanced Path Configuration
|
|
127
|
+
|
|
128
|
+
### Routing Between Parameters
|
|
129
|
+
|
|
130
|
+
Kalibr can route between different parameter configurations of the same model:
|
|
131
|
+
```python
|
|
132
|
+
from kalibr import Router
|
|
133
|
+
|
|
134
|
+
router = Router(
|
|
135
|
+
goal="creative_writing",
|
|
136
|
+
paths=[
|
|
137
|
+
{"model": "gpt-4o", "params": {"temperature": 0.3}},
|
|
138
|
+
{"model": "gpt-4o", "params": {"temperature": 0.9}},
|
|
139
|
+
{"model": "claude-sonnet-4-20250514", "params": {"temperature": 0.7}}
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
response = router.completion(messages=[...])
|
|
144
|
+
router.report(success=True)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Each unique `(model, params)` combination is tracked separately. Kalibr learns which configuration works best for your specific goal.
|
|
148
|
+
|
|
149
|
+
### Routing Between Tools
|
|
150
|
+
```python
|
|
151
|
+
router = Router(
|
|
152
|
+
goal="research_task",
|
|
153
|
+
paths=[
|
|
154
|
+
{"model": "gpt-4o", "tools": ["web_search"]},
|
|
155
|
+
{"model": "gpt-4o", "tools": ["code_interpreter"]},
|
|
156
|
+
{"model": "claude-sonnet-4-20250514"}
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### When to Use get_policy() Instead of Router
|
|
162
|
+
|
|
163
|
+
For most use cases, use `Router`. It handles provider dispatching and response conversion automatically.
|
|
164
|
+
|
|
165
|
+
Use `get_policy()` for advanced scenarios:
|
|
166
|
+
- Integrating with frameworks like LangChain that wrap LLM calls
|
|
167
|
+
- Custom retry logic or provider-specific features
|
|
168
|
+
- Building tools that need fine-grained control
|
|
169
|
+
```python
|
|
170
|
+
from kalibr import get_policy, report_outcome
|
|
171
|
+
|
|
172
|
+
policy = get_policy(goal="summarize")
|
|
173
|
+
model = policy["recommended_model"]
|
|
174
|
+
|
|
175
|
+
# You call the provider yourself
|
|
176
|
+
if model.startswith("gpt"):
|
|
177
|
+
client = OpenAI()
|
|
178
|
+
response = client.chat.completions.create(model=model, messages=[...])
|
|
179
|
+
|
|
180
|
+
report_outcome(trace_id=trace_id, goal="summarize", success=True)
|
|
181
|
+
```
|
|
182
|
+
|
|
126
183
|
## Outcome Reporting
|
|
127
184
|
|
|
128
185
|
### Automatic (with success_when)
|
|
@@ -56,6 +56,63 @@ paths = [
|
|
|
56
56
|
]
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
## Advanced Path Configuration
|
|
60
|
+
|
|
61
|
+
### Routing Between Parameters
|
|
62
|
+
|
|
63
|
+
Kalibr can route between different parameter configurations of the same model:
|
|
64
|
+
```python
|
|
65
|
+
from kalibr import Router
|
|
66
|
+
|
|
67
|
+
router = Router(
|
|
68
|
+
goal="creative_writing",
|
|
69
|
+
paths=[
|
|
70
|
+
{"model": "gpt-4o", "params": {"temperature": 0.3}},
|
|
71
|
+
{"model": "gpt-4o", "params": {"temperature": 0.9}},
|
|
72
|
+
{"model": "claude-sonnet-4-20250514", "params": {"temperature": 0.7}}
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
response = router.completion(messages=[...])
|
|
77
|
+
router.report(success=True)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Each unique `(model, params)` combination is tracked separately. Kalibr learns which configuration works best for your specific goal.
|
|
81
|
+
|
|
82
|
+
### Routing Between Tools
|
|
83
|
+
```python
|
|
84
|
+
router = Router(
|
|
85
|
+
goal="research_task",
|
|
86
|
+
paths=[
|
|
87
|
+
{"model": "gpt-4o", "tools": ["web_search"]},
|
|
88
|
+
{"model": "gpt-4o", "tools": ["code_interpreter"]},
|
|
89
|
+
{"model": "claude-sonnet-4-20250514"}
|
|
90
|
+
]
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### When to Use get_policy() Instead of Router
|
|
95
|
+
|
|
96
|
+
For most use cases, use `Router`. It handles provider dispatching and response conversion automatically.
|
|
97
|
+
|
|
98
|
+
Use `get_policy()` for advanced scenarios:
|
|
99
|
+
- Integrating with frameworks like LangChain that wrap LLM calls
|
|
100
|
+
- Custom retry logic or provider-specific features
|
|
101
|
+
- Building tools that need fine-grained control
|
|
102
|
+
```python
|
|
103
|
+
from kalibr import get_policy, report_outcome
|
|
104
|
+
|
|
105
|
+
policy = get_policy(goal="summarize")
|
|
106
|
+
model = policy["recommended_model"]
|
|
107
|
+
|
|
108
|
+
# You call the provider yourself
|
|
109
|
+
if model.startswith("gpt"):
|
|
110
|
+
client = OpenAI()
|
|
111
|
+
response = client.chat.completions.create(model=model, messages=[...])
|
|
112
|
+
|
|
113
|
+
report_outcome(trace_id=trace_id, goal="summarize", success=True)
|
|
114
|
+
```
|
|
115
|
+
|
|
59
116
|
## Outcome Reporting
|
|
60
117
|
|
|
61
118
|
### Automatic (with success_when)
|
|
@@ -5,10 +5,13 @@ Configures OpenTelemetry tracer provider with multiple exporters:
|
|
|
5
5
|
1. OTLP exporter for sending to OpenTelemetry collectors
|
|
6
6
|
2. Kalibr HTTP exporter for sending to Kalibr backend
|
|
7
7
|
3. File exporter for local JSONL fallback
|
|
8
|
+
|
|
9
|
+
Thread-safe singleton pattern for collector setup.
|
|
8
10
|
"""
|
|
9
11
|
|
|
10
12
|
import json
|
|
11
13
|
import os
|
|
14
|
+
import threading
|
|
12
15
|
from datetime import datetime, timezone
|
|
13
16
|
from pathlib import Path
|
|
14
17
|
from typing import Optional
|
|
@@ -167,7 +170,6 @@ class KalibrHTTPSpanExporter(SpanExporter):
|
|
|
167
170
|
|
|
168
171
|
def _convert_span(self, span) -> dict:
|
|
169
172
|
"""Convert OTel span to Kalibr event format"""
|
|
170
|
-
attrs = dict(span.attributes) if span.attributes else {}
|
|
171
173
|
|
|
172
174
|
# Calculate duration from span times (nanoseconds to milliseconds)
|
|
173
175
|
duration_ms = 0
|
|
@@ -236,6 +238,7 @@ class KalibrHTTPSpanExporter(SpanExporter):
|
|
|
236
238
|
|
|
237
239
|
_tracer_provider: Optional[TracerProvider] = None
|
|
238
240
|
_is_configured = False
|
|
241
|
+
_collector_lock = threading.Lock()
|
|
239
242
|
|
|
240
243
|
|
|
241
244
|
def setup_collector(
|
|
@@ -247,6 +250,8 @@ def setup_collector(
|
|
|
247
250
|
"""
|
|
248
251
|
Setup OpenTelemetry collector with multiple exporters
|
|
249
252
|
|
|
253
|
+
Thread-safe: Uses double-checked locking to ensure single initialization.
|
|
254
|
+
|
|
250
255
|
Args:
|
|
251
256
|
service_name: Service name for the tracer provider
|
|
252
257
|
otlp_endpoint: OTLP collector endpoint (e.g., "http://localhost:4317")
|
|
@@ -259,60 +264,67 @@ def setup_collector(
|
|
|
259
264
|
"""
|
|
260
265
|
global _tracer_provider, _is_configured
|
|
261
266
|
|
|
267
|
+
# First check without lock (fast path)
|
|
262
268
|
if _is_configured and _tracer_provider:
|
|
263
269
|
return _tracer_provider
|
|
264
270
|
|
|
265
|
-
#
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
271
|
+
# Acquire lock for initialization
|
|
272
|
+
with _collector_lock:
|
|
273
|
+
# Double-check inside lock
|
|
274
|
+
if _is_configured and _tracer_provider:
|
|
275
|
+
return _tracer_provider
|
|
276
|
+
|
|
277
|
+
# Create resource with service name
|
|
278
|
+
resource = Resource(attributes={SERVICE_NAME: service_name})
|
|
279
|
+
|
|
280
|
+
# Create tracer provider
|
|
281
|
+
provider = TracerProvider(resource=resource)
|
|
282
|
+
|
|
283
|
+
# Add OTLP exporter if endpoint is configured
|
|
284
|
+
otlp_endpoint = otlp_endpoint or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
285
|
+
if otlp_endpoint:
|
|
286
|
+
try:
|
|
287
|
+
otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint)
|
|
288
|
+
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
|
|
289
|
+
print(f"✅ OTLP exporter configured: {otlp_endpoint}")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
print(f"⚠️ Failed to configure OTLP exporter: {e}")
|
|
292
|
+
|
|
293
|
+
# Add Kalibr HTTP exporter if API key is configured
|
|
294
|
+
kalibr_api_key = os.getenv("KALIBR_API_KEY")
|
|
295
|
+
if kalibr_api_key:
|
|
296
|
+
try:
|
|
297
|
+
kalibr_exporter = KalibrHTTPSpanExporter()
|
|
298
|
+
provider.add_span_processor(BatchSpanProcessor(kalibr_exporter))
|
|
299
|
+
print(f"✅ Kalibr backend exporter configured: {kalibr_exporter.url}")
|
|
300
|
+
except Exception as e:
|
|
301
|
+
print(f"⚠️ Failed to configure Kalibr backend exporter: {e}")
|
|
302
|
+
|
|
303
|
+
# Add file exporter for local fallback
|
|
304
|
+
if file_export:
|
|
305
|
+
try:
|
|
306
|
+
file_exporter = FileSpanExporter("/tmp/kalibr_otel_spans.jsonl")
|
|
307
|
+
provider.add_span_processor(BatchSpanProcessor(file_exporter))
|
|
308
|
+
print("✅ File exporter configured: /tmp/kalibr_otel_spans.jsonl")
|
|
309
|
+
except Exception as e:
|
|
310
|
+
print(f"⚠️ Failed to configure file exporter: {e}")
|
|
311
|
+
|
|
312
|
+
# Add console exporter for debugging
|
|
313
|
+
if console_export:
|
|
314
|
+
try:
|
|
315
|
+
console_exporter = ConsoleSpanExporter()
|
|
316
|
+
provider.add_span_processor(BatchSpanProcessor(console_exporter))
|
|
317
|
+
print("✅ Console exporter configured")
|
|
318
|
+
except Exception as e:
|
|
319
|
+
print(f"⚠️ Failed to configure console exporter: {e}")
|
|
320
|
+
|
|
321
|
+
# Set as global tracer provider
|
|
322
|
+
trace.set_tracer_provider(provider)
|
|
323
|
+
|
|
324
|
+
_tracer_provider = provider
|
|
325
|
+
_is_configured = True
|
|
326
|
+
|
|
327
|
+
return provider
|
|
316
328
|
|
|
317
329
|
|
|
318
330
|
def get_tracer_provider() -> Optional[TracerProvider]:
|
|
@@ -326,11 +338,15 @@ def is_configured() -> bool:
|
|
|
326
338
|
|
|
327
339
|
|
|
328
340
|
def shutdown_collector():
|
|
329
|
-
"""Shutdown the tracer provider and flush all spans
|
|
341
|
+
"""Shutdown the tracer provider and flush all spans.
|
|
342
|
+
|
|
343
|
+
Thread-safe: Uses lock to protect shutdown operation.
|
|
344
|
+
"""
|
|
330
345
|
global _tracer_provider, _is_configured
|
|
331
346
|
|
|
332
|
-
|
|
333
|
-
_tracer_provider
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
347
|
+
with _collector_lock:
|
|
348
|
+
if _tracer_provider:
|
|
349
|
+
_tracer_provider.shutdown()
|
|
350
|
+
_tracer_provider = None
|
|
351
|
+
_is_configured = False
|
|
352
|
+
print("✅ Tracer provider shutdown")
|
|
@@ -10,6 +10,8 @@ Supports:
|
|
|
10
10
|
- OpenAI (GPT-4, GPT-3.5, etc.)
|
|
11
11
|
- Anthropic (Claude models)
|
|
12
12
|
- Extensible for other vendors
|
|
13
|
+
|
|
14
|
+
Note: All adapters now use centralized pricing from kalibr.pricing module.
|
|
13
15
|
"""
|
|
14
16
|
|
|
15
17
|
import json
|
|
@@ -17,6 +19,8 @@ import os
|
|
|
17
19
|
from abc import ABC, abstractmethod
|
|
18
20
|
from typing import Dict, Optional
|
|
19
21
|
|
|
22
|
+
from kalibr.pricing import get_pricing, normalize_model_name
|
|
23
|
+
|
|
20
24
|
|
|
21
25
|
class BaseCostAdapter(ABC):
|
|
22
26
|
"""Base class for vendor cost adapters."""
|
|
@@ -42,43 +46,27 @@ class BaseCostAdapter(ABC):
|
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
class OpenAICostAdapter(BaseCostAdapter):
|
|
45
|
-
"""Cost adapter for OpenAI models.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
PRICING = {
|
|
50
|
-
"gpt-4": {
|
|
51
|
-
"input": 30.00, # $30/1M input tokens
|
|
52
|
-
"output": 60.00, # $60/1M output tokens
|
|
53
|
-
},
|
|
54
|
-
"gpt-4-turbo": {
|
|
55
|
-
"input": 10.00,
|
|
56
|
-
"output": 30.00,
|
|
57
|
-
},
|
|
58
|
-
"gpt-4o": {
|
|
59
|
-
"input": 2.50,
|
|
60
|
-
"output": 10.00,
|
|
61
|
-
},
|
|
62
|
-
"gpt-3.5-turbo": {
|
|
63
|
-
"input": 0.50,
|
|
64
|
-
"output": 1.50,
|
|
65
|
-
},
|
|
66
|
-
"gpt-4o-mini": {
|
|
67
|
-
"input": 0.15,
|
|
68
|
-
"output": 0.60,
|
|
69
|
-
},
|
|
70
|
-
}
|
|
49
|
+
"""Cost adapter for OpenAI models.
|
|
50
|
+
|
|
51
|
+
Uses centralized pricing from kalibr.pricing module.
|
|
52
|
+
"""
|
|
71
53
|
|
|
72
54
|
def get_vendor_name(self) -> str:
|
|
73
55
|
return "openai"
|
|
74
56
|
|
|
75
57
|
def compute_cost(self, model_name: str, tokens_in: int, tokens_out: int) -> float:
|
|
76
|
-
"""Compute cost for OpenAI models.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
58
|
+
"""Compute cost for OpenAI models.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
model_name: Model identifier (e.g., "gpt-4o", "gpt-4")
|
|
62
|
+
tokens_in: Input token count
|
|
63
|
+
tokens_out: Output token count
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Cost in USD (rounded to 6 decimal places)
|
|
67
|
+
"""
|
|
68
|
+
# Get pricing from centralized module
|
|
69
|
+
pricing, _ = get_pricing("openai", model_name)
|
|
82
70
|
|
|
83
71
|
# Calculate cost (pricing is per 1M tokens)
|
|
84
72
|
input_cost = (tokens_in / 1_000_000) * pricing["input"]
|
|
@@ -86,64 +74,29 @@ class OpenAICostAdapter(BaseCostAdapter):
|
|
|
86
74
|
|
|
87
75
|
return round(input_cost + output_cost, 6)
|
|
88
76
|
|
|
89
|
-
def _normalize_model_name(self, model_name: str) -> str:
|
|
90
|
-
"""Normalize model name to match pricing table."""
|
|
91
|
-
model_lower = model_name.lower()
|
|
92
|
-
|
|
93
|
-
# Direct matches
|
|
94
|
-
if model_lower in self.PRICING:
|
|
95
|
-
return model_lower
|
|
96
|
-
|
|
97
|
-
# Fuzzy matches
|
|
98
|
-
if "gpt-4o-mini" in model_lower:
|
|
99
|
-
return "gpt-4o-mini"
|
|
100
|
-
elif "gpt-4o" in model_lower:
|
|
101
|
-
return "gpt-4o"
|
|
102
|
-
elif "gpt-4-turbo" in model_lower:
|
|
103
|
-
return "gpt-4-turbo"
|
|
104
|
-
elif "gpt-4" in model_lower:
|
|
105
|
-
return "gpt-4"
|
|
106
|
-
elif "gpt-3.5" in model_lower:
|
|
107
|
-
return "gpt-3.5-turbo"
|
|
108
|
-
|
|
109
|
-
# Default to gpt-4 for unknown models
|
|
110
|
-
return "gpt-4"
|
|
111
|
-
|
|
112
77
|
|
|
113
78
|
class AnthropicCostAdapter(BaseCostAdapter):
|
|
114
|
-
"""Cost adapter for Anthropic Claude models.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
PRICING = {
|
|
119
|
-
"claude-3-opus": {
|
|
120
|
-
"input": 15.00,
|
|
121
|
-
"output": 75.00,
|
|
122
|
-
},
|
|
123
|
-
"claude-3-sonnet": {
|
|
124
|
-
"input": 3.00,
|
|
125
|
-
"output": 15.00,
|
|
126
|
-
},
|
|
127
|
-
"claude-3-haiku": {
|
|
128
|
-
"input": 0.25,
|
|
129
|
-
"output": 1.25,
|
|
130
|
-
},
|
|
131
|
-
"claude-3.5-sonnet": {
|
|
132
|
-
"input": 3.00,
|
|
133
|
-
"output": 15.00,
|
|
134
|
-
},
|
|
135
|
-
}
|
|
79
|
+
"""Cost adapter for Anthropic Claude models.
|
|
80
|
+
|
|
81
|
+
Uses centralized pricing from kalibr.pricing module.
|
|
82
|
+
"""
|
|
136
83
|
|
|
137
84
|
def get_vendor_name(self) -> str:
|
|
138
85
|
return "anthropic"
|
|
139
86
|
|
|
140
87
|
def compute_cost(self, model_name: str, tokens_in: int, tokens_out: int) -> float:
|
|
141
|
-
"""Compute cost for Anthropic models.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
88
|
+
"""Compute cost for Anthropic models.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
model_name: Model identifier (e.g., "claude-3-opus", "claude-3-5-sonnet")
|
|
92
|
+
tokens_in: Input token count
|
|
93
|
+
tokens_out: Output token count
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Cost in USD (rounded to 6 decimal places)
|
|
97
|
+
"""
|
|
98
|
+
# Get pricing from centralized module
|
|
99
|
+
pricing, _ = get_pricing("anthropic", model_name)
|
|
147
100
|
|
|
148
101
|
# Calculate cost (pricing is per 1M tokens)
|
|
149
102
|
input_cost = (tokens_in / 1_000_000) * pricing["input"]
|
|
@@ -151,27 +104,6 @@ class AnthropicCostAdapter(BaseCostAdapter):
|
|
|
151
104
|
|
|
152
105
|
return round(input_cost + output_cost, 6)
|
|
153
106
|
|
|
154
|
-
def _normalize_model_name(self, model_name: str) -> str:
|
|
155
|
-
"""Normalize model name to match pricing table."""
|
|
156
|
-
model_lower = model_name.lower()
|
|
157
|
-
|
|
158
|
-
# Direct matches
|
|
159
|
-
if model_lower in self.PRICING:
|
|
160
|
-
return model_lower
|
|
161
|
-
|
|
162
|
-
# Fuzzy matches
|
|
163
|
-
if "claude-3.5-sonnet" in model_lower or "claude-3-5-sonnet" in model_lower:
|
|
164
|
-
return "claude-3.5-sonnet"
|
|
165
|
-
elif "claude-3-opus" in model_lower:
|
|
166
|
-
return "claude-3-opus"
|
|
167
|
-
elif "claude-3-sonnet" in model_lower:
|
|
168
|
-
return "claude-3-sonnet"
|
|
169
|
-
elif "claude-3-haiku" in model_lower:
|
|
170
|
-
return "claude-3-haiku"
|
|
171
|
-
|
|
172
|
-
# Default to opus for unknown models
|
|
173
|
-
return "claude-3-opus"
|
|
174
|
-
|
|
175
107
|
|
|
176
108
|
class CostAdapterFactory:
|
|
177
109
|
"""Factory to get appropriate cost adapter for a vendor."""
|
|
@@ -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
|
|