kalibr 1.1.3a0__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kalibr/__init__.py +41 -3
- kalibr/cli/capsule_cmd.py +3 -3
- kalibr/cli/main.py +3 -3
- kalibr/cli/run.py +2 -2
- kalibr/client.py +1 -1
- kalibr/collector.py +227 -48
- kalibr/context.py +42 -0
- 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 +662 -0
- kalibr/middleware/auto_tracer.py +1 -1
- kalibr/pricing.py +245 -0
- kalibr/router.py +499 -0
- kalibr/simple_tracer.py +16 -15
- kalibr/trace_capsule.py +19 -12
- kalibr/utils.py +2 -2
- kalibr-1.3.0.dist-info/LICENSE +190 -0
- kalibr-1.3.0.dist-info/METADATA +296 -0
- kalibr-1.3.0.dist-info/RECORD +52 -0
- {kalibr-1.1.3a0.dist-info → kalibr-1.3.0.dist-info}/WHEEL +1 -1
- kalibr_crewai/__init__.py +1 -1
- kalibr_crewai/callbacks.py +122 -14
- kalibr_crewai/instrumentor.py +196 -33
- kalibr_langchain/__init__.py +4 -2
- kalibr_langchain/callback.py +26 -0
- kalibr_langchain/chat_model.py +103 -0
- kalibr_openai_agents/__init__.py +1 -1
- kalibr-1.1.3a0.dist-info/METADATA +0 -236
- kalibr-1.1.3a0.dist-info/RECORD +0 -48
- kalibr-1.1.3a0.dist-info/licenses/LICENSE +0 -21
- {kalibr-1.1.3a0.dist-info → kalibr-1.3.0.dist-info}/entry_points.txt +0 -0
- {kalibr-1.1.3a0.dist-info → kalibr-1.3.0.dist-info}/top_level.txt +0 -0
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."""
|
|
@@ -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
|
|