kalibr 1.2.5__py3-none-any.whl → 1.2.8__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 +139 -53
- kalibr/simple_tracer.py +16 -15
- kalibr/trace_capsule.py +19 -12
- kalibr/utils.py +2 -2
- {kalibr-1.2.5.dist-info → kalibr-1.2.8.dist-info}/METADATA +115 -15
- {kalibr-1.2.5.dist-info → kalibr-1.2.8.dist-info}/RECORD +24 -23
- {kalibr-1.2.5.dist-info → kalibr-1.2.8.dist-info}/LICENSE +0 -0
- {kalibr-1.2.5.dist-info → kalibr-1.2.8.dist-info}/WHEEL +0 -0
- {kalibr-1.2.5.dist-info → kalibr-1.2.8.dist-info}/entry_points.txt +0 -0
- {kalibr-1.2.5.dist-info → kalibr-1.2.8.dist-info}/top_level.txt +0 -0
kalibr/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Kalibr SDK v1.2.
|
|
1
|
+
"""Kalibr SDK v1.2.7 - LLM Observability & Tracing Framework
|
|
2
2
|
|
|
3
3
|
Features:
|
|
4
4
|
- **Auto-Instrumentation**: Zero-config tracing of OpenAI, Anthropic, Google SDK calls
|
|
@@ -36,7 +36,7 @@ CLI Usage:
|
|
|
36
36
|
kalibr version # Show version
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
-
__version__ = "1.2.
|
|
39
|
+
__version__ = "1.2.7"
|
|
40
40
|
|
|
41
41
|
# Auto-instrument LLM SDKs on import (can be disabled via env var)
|
|
42
42
|
import os
|
kalibr/cli/capsule_cmd.py
CHANGED
|
@@ -23,7 +23,7 @@ def capsule(
|
|
|
23
23
|
None,
|
|
24
24
|
"--api-url",
|
|
25
25
|
"-u",
|
|
26
|
-
help="Kalibr API base URL (default: from env KALIBR_API_URL or https://
|
|
26
|
+
help="Kalibr API base URL (default: from env KALIBR_API_URL or https://kalibr-backend.fly.dev)",
|
|
27
27
|
envvar="KALIBR_API_URL",
|
|
28
28
|
),
|
|
29
29
|
output: Optional[Path] = typer.Option(
|
|
@@ -63,10 +63,10 @@ def capsule(
|
|
|
63
63
|
kalibr capsule abc-123-def --export --output capsule.json
|
|
64
64
|
|
|
65
65
|
# Specify custom API URL
|
|
66
|
-
kalibr capsule abc-123-def -u https://
|
|
66
|
+
kalibr capsule abc-123-def -u https://kalibr-backend.fly.dev
|
|
67
67
|
"""
|
|
68
68
|
# Determine API base URL
|
|
69
|
-
base_url = api_url or "https://
|
|
69
|
+
base_url = api_url or "https://kalibr-backend.fly.dev"
|
|
70
70
|
base_url = base_url.rstrip("/")
|
|
71
71
|
|
|
72
72
|
# Build endpoint URL
|
kalibr/cli/run.py
CHANGED
|
@@ -47,7 +47,7 @@ def run(
|
|
|
47
47
|
kalibr run weather.py --runtime fly.io
|
|
48
48
|
|
|
49
49
|
# Custom backend
|
|
50
|
-
kalibr run weather.py --backend-url https://
|
|
50
|
+
kalibr run weather.py --backend-url https://kalibr-backend.fly.dev
|
|
51
51
|
"""
|
|
52
52
|
# Validate file exists
|
|
53
53
|
agent_path = Path(file_path).resolve()
|
|
@@ -56,7 +56,7 @@ def run(
|
|
|
56
56
|
raise typer.Exit(1)
|
|
57
57
|
|
|
58
58
|
# Configure backend
|
|
59
|
-
backend = backend_url or os.getenv("KALIBR_BACKEND_URL", "https://
|
|
59
|
+
backend = backend_url or os.getenv("KALIBR_BACKEND_URL", "https://kalibr-backend.fly.dev")
|
|
60
60
|
api_key = os.getenv("KALIBR_API_KEY")
|
|
61
61
|
if not api_key:
|
|
62
62
|
console.print("[yellow]⚠️ KALIBR_API_KEY not set. Set it for trace authentication.[/yellow]")
|
kalibr/client.py
CHANGED
|
@@ -70,7 +70,7 @@ class KalibrClient:
|
|
|
70
70
|
|
|
71
71
|
self.api_key = api_key or env_config.get("auth_token", "")
|
|
72
72
|
self.endpoint = endpoint or env_config.get(
|
|
73
|
-
"api_endpoint", "https://
|
|
73
|
+
"api_endpoint", "https://kalibr-backend.fly.dev/api/v1/traces"
|
|
74
74
|
)
|
|
75
75
|
self.tenant_id = tenant_id or env_config.get("tenant_id", "default")
|
|
76
76
|
self.environment = environment or env_config.get("environment", "prod")
|
kalibr/collector.py
CHANGED
|
@@ -3,14 +3,21 @@ OpenTelemetry Collector Setup
|
|
|
3
3
|
|
|
4
4
|
Configures OpenTelemetry tracer provider with multiple exporters:
|
|
5
5
|
1. OTLP exporter for sending to OpenTelemetry collectors
|
|
6
|
-
2.
|
|
6
|
+
2. Kalibr HTTP exporter for sending to Kalibr backend
|
|
7
|
+
3. File exporter for local JSONL fallback
|
|
8
|
+
|
|
9
|
+
Thread-safe singleton pattern for collector setup.
|
|
7
10
|
"""
|
|
8
11
|
|
|
9
12
|
import json
|
|
10
13
|
import os
|
|
14
|
+
import threading
|
|
15
|
+
from datetime import datetime, timezone
|
|
11
16
|
from pathlib import Path
|
|
12
17
|
from typing import Optional
|
|
13
18
|
|
|
19
|
+
import requests
|
|
20
|
+
|
|
14
21
|
from opentelemetry import trace
|
|
15
22
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
16
23
|
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
|
@@ -21,6 +28,7 @@ from opentelemetry.sdk.trace.export import (
|
|
|
21
28
|
SpanExporter,
|
|
22
29
|
SpanExportResult,
|
|
23
30
|
)
|
|
31
|
+
from opentelemetry.trace import StatusCode
|
|
24
32
|
|
|
25
33
|
try:
|
|
26
34
|
from opentelemetry.sdk.trace import ReadableSpan
|
|
@@ -81,8 +89,156 @@ class FileSpanExporter(SpanExporter):
|
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
|
|
92
|
+
class KalibrHTTPSpanExporter(SpanExporter):
|
|
93
|
+
"""Export spans to Kalibr backend via HTTP POST"""
|
|
94
|
+
|
|
95
|
+
DEFAULT_URL = "https://kalibr-backend.fly.dev/api/ingest"
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
url: Optional[str] = None,
|
|
100
|
+
api_key: Optional[str] = None,
|
|
101
|
+
tenant_id: Optional[str] = None,
|
|
102
|
+
):
|
|
103
|
+
"""Initialize the Kalibr HTTP exporter.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
url: Kalibr collector URL (default: from KALIBR_COLLECTOR_URL env var)
|
|
107
|
+
api_key: API key (default: from KALIBR_API_KEY env var)
|
|
108
|
+
tenant_id: Tenant ID (default: from KALIBR_TENANT_ID env var)
|
|
109
|
+
"""
|
|
110
|
+
self.url = url or os.getenv("KALIBR_COLLECTOR_URL", self.DEFAULT_URL)
|
|
111
|
+
self.api_key = api_key or os.getenv("KALIBR_API_KEY")
|
|
112
|
+
self.tenant_id = tenant_id or os.getenv("KALIBR_TENANT_ID", "default")
|
|
113
|
+
self.environment = os.getenv("KALIBR_ENVIRONMENT", "production")
|
|
114
|
+
|
|
115
|
+
def export(self, spans) -> SpanExportResult:
|
|
116
|
+
"""Export spans to Kalibr backend"""
|
|
117
|
+
if not self.api_key:
|
|
118
|
+
print("[Kalibr SDK] ⚠️ KALIBR_API_KEY not set, spans will not be sent to backend")
|
|
119
|
+
return SpanExportResult.SUCCESS
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
events = [self._convert_span(span) for span in spans]
|
|
123
|
+
|
|
124
|
+
headers = {
|
|
125
|
+
"X-API-Key": self.api_key,
|
|
126
|
+
"X-Tenant-ID": self.tenant_id,
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
payload = {"events": events}
|
|
131
|
+
|
|
132
|
+
response = requests.post(
|
|
133
|
+
self.url,
|
|
134
|
+
headers=headers,
|
|
135
|
+
json=payload,
|
|
136
|
+
timeout=30,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if not response.ok:
|
|
140
|
+
print(
|
|
141
|
+
f"[Kalibr SDK] ❌ Backend rejected spans: {response.status_code} - {response.text}"
|
|
142
|
+
)
|
|
143
|
+
return SpanExportResult.FAILURE
|
|
144
|
+
|
|
145
|
+
return SpanExportResult.SUCCESS
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f"[Kalibr SDK] ❌ Failed to export spans to backend: {e}")
|
|
149
|
+
return SpanExportResult.FAILURE
|
|
150
|
+
|
|
151
|
+
def shutdown(self):
|
|
152
|
+
"""Shutdown the exporter"""
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
def _nanos_to_iso(self, nanos: int) -> str:
|
|
156
|
+
"""Convert nanoseconds since epoch to ISO format timestamp"""
|
|
157
|
+
if nanos is None:
|
|
158
|
+
return datetime.now(timezone.utc).isoformat()
|
|
159
|
+
seconds = nanos / 1_000_000_000
|
|
160
|
+
dt = datetime.fromtimestamp(seconds, tz=timezone.utc)
|
|
161
|
+
return dt.isoformat()
|
|
162
|
+
|
|
163
|
+
def _get_attr(self, span, *keys, default=None):
|
|
164
|
+
"""Get attribute value from span, trying multiple keys"""
|
|
165
|
+
attrs = dict(span.attributes) if span.attributes else {}
|
|
166
|
+
for key in keys:
|
|
167
|
+
if key in attrs:
|
|
168
|
+
return attrs[key]
|
|
169
|
+
return default
|
|
170
|
+
|
|
171
|
+
def _convert_span(self, span) -> dict:
|
|
172
|
+
"""Convert OTel span to Kalibr event format"""
|
|
173
|
+
|
|
174
|
+
# Calculate duration from span times (nanoseconds to milliseconds)
|
|
175
|
+
duration_ms = 0
|
|
176
|
+
if span.start_time and span.end_time:
|
|
177
|
+
duration_ms = int((span.end_time - span.start_time) / 1_000_000)
|
|
178
|
+
|
|
179
|
+
# Determine status
|
|
180
|
+
is_error = (
|
|
181
|
+
hasattr(span.status, "status_code") and span.status.status_code == StatusCode.ERROR
|
|
182
|
+
)
|
|
183
|
+
status = "error" if is_error else "success"
|
|
184
|
+
|
|
185
|
+
# Extract provider and model
|
|
186
|
+
provider = self._get_attr(span, "llm.vendor", "llm.system", "gen_ai.system", default="")
|
|
187
|
+
model_id = self._get_attr(
|
|
188
|
+
span, "llm.request.model", "llm.response.model", "gen_ai.request.model", default=""
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Extract token counts
|
|
192
|
+
input_tokens = self._get_attr(
|
|
193
|
+
span, "llm.usage.prompt_tokens", "gen_ai.usage.prompt_tokens", default=0
|
|
194
|
+
)
|
|
195
|
+
output_tokens = self._get_attr(
|
|
196
|
+
span, "llm.usage.completion_tokens", "gen_ai.usage.completion_tokens", default=0
|
|
197
|
+
)
|
|
198
|
+
total_tokens = self._get_attr(
|
|
199
|
+
span, "llm.usage.total_tokens", "gen_ai.usage.total_tokens", default=0
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# If total_tokens not provided, calculate it
|
|
203
|
+
if not total_tokens and (input_tokens or output_tokens):
|
|
204
|
+
total_tokens = (input_tokens or 0) + (output_tokens or 0)
|
|
205
|
+
|
|
206
|
+
# Build event payload
|
|
207
|
+
event = {
|
|
208
|
+
"schema_version": "1.0",
|
|
209
|
+
"trace_id": format(span.context.trace_id, "032x"),
|
|
210
|
+
"span_id": format(span.context.span_id, "016x"),
|
|
211
|
+
"parent_id": format(span.parent.span_id, "016x") if span.parent else None,
|
|
212
|
+
"tenant_id": self.tenant_id,
|
|
213
|
+
"provider": provider,
|
|
214
|
+
"model_id": model_id,
|
|
215
|
+
"model_name": model_id,
|
|
216
|
+
"operation": span.name,
|
|
217
|
+
"endpoint": span.name,
|
|
218
|
+
"input_tokens": input_tokens or 0,
|
|
219
|
+
"output_tokens": output_tokens or 0,
|
|
220
|
+
"total_tokens": total_tokens or 0,
|
|
221
|
+
"cost_usd": self._get_attr(span, "llm.cost_usd", "gen_ai.usage.cost", default=0.0),
|
|
222
|
+
"latency_ms": self._get_attr(span, "llm.latency_ms", default=duration_ms),
|
|
223
|
+
"duration_ms": duration_ms,
|
|
224
|
+
"status": status,
|
|
225
|
+
"error_type": self._get_attr(span, "error.type", default=None) if is_error else None,
|
|
226
|
+
"error_message": (
|
|
227
|
+
self._get_attr(span, "error.message", default=None) if is_error else None
|
|
228
|
+
),
|
|
229
|
+
"timestamp": self._nanos_to_iso(span.end_time),
|
|
230
|
+
"ts_start": self._nanos_to_iso(span.start_time),
|
|
231
|
+
"ts_end": self._nanos_to_iso(span.end_time),
|
|
232
|
+
"goal": self._get_attr(span, "kalibr.goal", default=""),
|
|
233
|
+
"environment": self.environment,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return event
|
|
237
|
+
|
|
238
|
+
|
|
84
239
|
_tracer_provider: Optional[TracerProvider] = None
|
|
85
240
|
_is_configured = False
|
|
241
|
+
_collector_lock = threading.Lock()
|
|
86
242
|
|
|
87
243
|
|
|
88
244
|
def setup_collector(
|
|
@@ -94,6 +250,8 @@ def setup_collector(
|
|
|
94
250
|
"""
|
|
95
251
|
Setup OpenTelemetry collector with multiple exporters
|
|
96
252
|
|
|
253
|
+
Thread-safe: Uses double-checked locking to ensure single initialization.
|
|
254
|
+
|
|
97
255
|
Args:
|
|
98
256
|
service_name: Service name for the tracer provider
|
|
99
257
|
otlp_endpoint: OTLP collector endpoint (e.g., "http://localhost:4317")
|
|
@@ -106,50 +264,67 @@ def setup_collector(
|
|
|
106
264
|
"""
|
|
107
265
|
global _tracer_provider, _is_configured
|
|
108
266
|
|
|
267
|
+
# First check without lock (fast path)
|
|
109
268
|
if _is_configured and _tracer_provider:
|
|
110
269
|
return _tracer_provider
|
|
111
270
|
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
153
328
|
|
|
154
329
|
|
|
155
330
|
def get_tracer_provider() -> Optional[TracerProvider]:
|
|
@@ -163,11 +338,15 @@ def is_configured() -> bool:
|
|
|
163
338
|
|
|
164
339
|
|
|
165
340
|
def shutdown_collector():
|
|
166
|
-
"""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
|
+
"""
|
|
167
345
|
global _tracer_provider, _is_configured
|
|
168
346
|
|
|
169
|
-
|
|
170
|
-
_tracer_provider
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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")
|
kalibr/cost_adapter.py
CHANGED
|
@@ -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."""
|