kalibr 1.1.2a0__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/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
- # OpenAI pricing as of 2025 (per 1M tokens)
48
- # Source: https://openai.com/pricing
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
- # Normalize model name
78
- model_key = self._normalize_model_name(model_name)
79
-
80
- # Get pricing (default to gpt-4 if unknown)
81
- pricing = self.PRICING.get(model_key, self.PRICING["gpt-4"])
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
- # Anthropic pricing as of 2025 (per 1M tokens)
117
- # Source: https://www.anthropic.com/pricing
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
- # Normalize model name
143
- model_key = self._normalize_model_name(model_name)
144
-
145
- # Get pricing (default to opus if unknown)
146
- pricing = self.PRICING.get(model_key, self.PRICING["claude-3-opus"])
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
- # Pricing per 1K tokens (USD) - Updated November 2025
21
- PRICING = {
22
- # Claude 4 models
23
- "claude-4-opus": {"input": 0.015, "output": 0.075},
24
- "claude-4-sonnet": {"input": 0.003, "output": 0.015},
25
- # Claude 3 models (Sonnet 4 is actually Claude 3.7)
26
- "claude-sonnet-4": {"input": 0.003, "output": 0.015},
27
- "claude-3-7-sonnet": {"input": 0.003, "output": 0.015},
28
- "claude-3-5-sonnet": {"input": 0.003, "output": 0.015},
29
- "claude-3-opus": {"input": 0.015, "output": 0.075},
30
- "claude-3-sonnet": {"input": 0.003, "output": 0.015},
31
- "claude-3-haiku": {"input": 0.00025, "output": 0.00125},
32
- # Claude 2 models
33
- "claude-2.1": {"input": 0.008, "output": 0.024},
34
- "claude-2.0": {"input": 0.008, "output": 0.024},
35
- "claude-instant-1.2": {"input": 0.0008, "output": 0.0024},
36
- }
37
-
38
- def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
39
- """Calculate cost in USD for an Anthropic API call"""
40
- # Normalize model name
41
- base_model = model.lower()
21
+ """Cost calculation adapter for Anthropic models.
22
+
23
+ Uses centralized pricing from kalibr.pricing module.
24
+ """
42
25
 
43
- # Try exact match first
44
- pricing = self.get_pricing(base_model)
26
+ def get_vendor_name(self) -> str:
27
+ """Return vendor name for Anthropic."""
28
+ return "anthropic"
45
29
 
46
- # Try fuzzy matching for versioned models
47
- if not pricing:
48
- for known_model in self.PRICING.keys():
49
- if known_model in base_model or base_model in known_model:
50
- pricing = self.PRICING[known_model]
51
- break
52
-
53
- if not pricing:
54
- # Default to Claude 3 Sonnet pricing if unknown
55
- pricing = {"input": 0.003, "output": 0.015}
30
+ def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
31
+ """Calculate cost in USD for an Anthropic API call.
32
+
33
+ Args:
34
+ model: Model identifier (e.g., "claude-3-opus", "claude-3-5-sonnet-20240620")
35
+ usage: Token usage dict with input_tokens and output_tokens
36
+
37
+ Returns:
38
+ Cost in USD (rounded to 6 decimal places)
39
+ """
40
+ # Get pricing from centralized module (handles normalization)
41
+ pricing = self.get_pricing_for_model(model)
56
42
 
57
43
  input_tokens = usage.get("input_tokens", 0)
58
44
  output_tokens = usage.get("output_tokens", 0)
59
45
 
60
- input_cost = (input_tokens / 1000) * pricing["input"]
61
- output_cost = (output_tokens / 1000) * pricing["output"]
46
+ # Calculate cost (pricing is per 1M tokens)
47
+ input_cost = (input_tokens / 1_000_000) * pricing["input"]
48
+ output_cost = (output_tokens / 1_000_000) * pricing["output"]
62
49
 
63
50
  return round(input_cost + output_cost, 6)
64
51
 
@@ -262,13 +249,20 @@ class AnthropicInstrumentation(BaseInstrumentation):
262
249
 
263
250
  # Singleton instance
264
251
  _anthropic_instrumentation = None
252
+ _anthropic_lock = threading.Lock()
265
253
 
266
254
 
267
255
  def get_instrumentation() -> AnthropicInstrumentation:
268
- """Get or create the Anthropic instrumentation singleton"""
256
+ """Get or create the Anthropic instrumentation singleton.
257
+
258
+ Thread-safe singleton pattern using double-checked locking.
259
+ """
269
260
  global _anthropic_instrumentation
270
261
  if _anthropic_instrumentation is None:
271
- _anthropic_instrumentation = AnthropicInstrumentation()
262
+ with _anthropic_lock:
263
+ # Double-check inside lock to prevent race condition
264
+ if _anthropic_instrumentation is None:
265
+ _anthropic_instrumentation = AnthropicInstrumentation()
272
266
  return _anthropic_instrumentation
273
267
 
274
268
 
@@ -3,6 +3,8 @@ Base instrumentation class for LLM SDKs
3
3
 
4
4
  Provides common functionality for monkey-patching LLM SDKs and
5
5
  emitting OpenTelemetry-compatible spans.
6
+
7
+ Note: Cost adapters now use centralized pricing from kalibr.pricing module.
6
8
  """
7
9
 
8
10
  import time
@@ -13,6 +15,8 @@ from typing import Any, Dict, Optional
13
15
  from opentelemetry import trace
14
16
  from opentelemetry.trace import SpanKind, Status, StatusCode
15
17
 
18
+ from kalibr.pricing import get_pricing
19
+
16
20
 
17
21
  class BaseInstrumentation(ABC):
18
22
  """Base class for LLM SDK instrumentation"""
@@ -76,9 +80,11 @@ class BaseInstrumentation(ABC):
76
80
 
77
81
 
78
82
  class BaseCostAdapter(ABC):
79
- """Base class for cost calculation adapters"""
80
-
81
- PRICING: Dict[str, Dict[str, float]] = {}
83
+ """Base class for cost calculation adapters.
84
+
85
+ Uses centralized pricing from kalibr.pricing module.
86
+ All subclasses must implement get_vendor_name() to specify their vendor.
87
+ """
82
88
 
83
89
  @abstractmethod
84
90
  def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
@@ -87,22 +93,34 @@ class BaseCostAdapter(ABC):
87
93
 
88
94
  Args:
89
95
  model: Model identifier (e.g., "gpt-4")
90
- usage: Token usage dictionary with prompt_tokens, completion_tokens
96
+ usage: Token usage dictionary with prompt_tokens, completion_tokens,
97
+ input_tokens, or output_tokens
91
98
 
92
99
  Returns:
93
100
  Cost in USD (rounded to 6 decimal places)
94
101
  """
95
102
  pass
96
103
 
97
- def get_pricing(self, model: str) -> Optional[Dict[str, float]]:
104
+ @abstractmethod
105
+ def get_vendor_name(self) -> str:
106
+ """
107
+ Get the vendor name for this adapter.
108
+
109
+ Returns:
110
+ Vendor name (e.g., "openai", "anthropic", "google")
111
+ """
112
+ pass
113
+
114
+ def get_pricing_for_model(self, model: str) -> Dict[str, float]:
98
115
  """
99
- Get pricing for a specific model
116
+ Get pricing for a specific model using centralized pricing.
100
117
 
101
118
  Args:
102
119
  model: Model identifier
103
120
 
104
121
  Returns:
105
- Dictionary with "input" and "output" prices per 1K tokens,
106
- or None if model not found
122
+ Dictionary with "input" and "output" prices per 1M tokens
107
123
  """
108
- return self.PRICING.get(model)
124
+ vendor = self.get_vendor_name()
125
+ pricing, _ = get_pricing(vendor, model)
126
+ return pricing
@@ -3,8 +3,11 @@ Google Generative AI SDK Instrumentation
3
3
 
4
4
  Monkey-patches the Google Generative AI SDK to automatically emit OpenTelemetry spans
5
5
  for all content generation API calls.
6
+
7
+ Thread-safe singleton pattern using double-checked locking.
6
8
  """
7
9
 
10
+ import threading
8
11
  import time
9
12
  from functools import wraps
10
13
  from typing import Any, Dict, Optional
@@ -15,49 +18,34 @@ from .base import BaseCostAdapter, BaseInstrumentation
15
18
 
16
19
 
17
20
  class GoogleCostAdapter(BaseCostAdapter):
18
- """Cost calculation adapter for Google Generative AI models"""
19
-
20
- # Pricing per 1K tokens (USD) - Updated November 2025
21
- PRICING = {
22
- # Gemini 2.5 models
23
- "gemini-2.5-pro": {"input": 0.00125, "output": 0.005},
24
- "gemini-2.5-flash": {"input": 0.000075, "output": 0.0003},
25
- # Gemini 2.0 models
26
- "gemini-2.0-flash": {"input": 0.000075, "output": 0.0003},
27
- "gemini-2.0-flash-thinking": {"input": 0.000075, "output": 0.0003},
28
- # Gemini 1.5 models
29
- "gemini-1.5-pro": {"input": 0.00125, "output": 0.005},
30
- "gemini-1.5-flash": {"input": 0.000075, "output": 0.0003},
31
- "gemini-1.5-flash-8b": {"input": 0.0000375, "output": 0.00015},
32
- # Gemini 1.0 models
33
- "gemini-1.0-pro": {"input": 0.0005, "output": 0.0015},
34
- "gemini-pro": {"input": 0.0005, "output": 0.0015}, # Alias
35
- }
36
-
37
- def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
38
- """Calculate cost in USD for a Google Generative AI API call"""
39
- # Normalize model name
40
- base_model = model.lower()
21
+ """Cost calculation adapter for Google Generative AI models.
22
+
23
+ Uses centralized pricing from kalibr.pricing module.
24
+ """
41
25
 
42
- # Try exact match first
43
- pricing = self.get_pricing(base_model)
26
+ def get_vendor_name(self) -> str:
27
+ """Return vendor name for Google."""
28
+ return "google"
44
29
 
45
- # Try fuzzy matching for versioned models
46
- if not pricing:
47
- for known_model in self.PRICING.keys():
48
- if known_model in base_model or base_model in known_model:
49
- pricing = self.PRICING[known_model]
50
- break
51
-
52
- if not pricing:
53
- # Default to Gemini 1.5 Pro pricing if unknown
54
- pricing = {"input": 0.00125, "output": 0.005}
30
+ def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
31
+ """Calculate cost in USD for a Google Generative AI API call.
32
+
33
+ Args:
34
+ model: Model identifier (e.g., "gemini-1.5-pro", "gemini-2.0-flash")
35
+ usage: Token usage dict with prompt_tokens and completion_tokens
36
+
37
+ Returns:
38
+ Cost in USD (rounded to 6 decimal places)
39
+ """
40
+ # Get pricing from centralized module (handles normalization)
41
+ pricing = self.get_pricing_for_model(model)
55
42
 
56
43
  prompt_tokens = usage.get("prompt_tokens", 0)
57
44
  completion_tokens = usage.get("completion_tokens", 0)
58
45
 
59
- input_cost = (prompt_tokens / 1000) * pricing["input"]
60
- output_cost = (completion_tokens / 1000) * pricing["output"]
46
+ # Calculate cost (pricing is per 1M tokens)
47
+ input_cost = (prompt_tokens / 1_000_000) * pricing["input"]
48
+ output_cost = (completion_tokens / 1_000_000) * pricing["output"]
61
49
 
62
50
  return round(input_cost + output_cost, 6)
63
51
 
@@ -261,13 +249,20 @@ class GoogleInstrumentation(BaseInstrumentation):
261
249
 
262
250
  # Singleton instance
263
251
  _google_instrumentation = None
252
+ _google_lock = threading.Lock()
264
253
 
265
254
 
266
255
  def get_instrumentation() -> GoogleInstrumentation:
267
- """Get or create the Google instrumentation singleton"""
256
+ """Get or create the Google instrumentation singleton.
257
+
258
+ Thread-safe singleton pattern using double-checked locking.
259
+ """
268
260
  global _google_instrumentation
269
261
  if _google_instrumentation is None:
270
- _google_instrumentation = GoogleInstrumentation()
262
+ with _google_lock:
263
+ # Double-check inside lock to prevent race condition
264
+ if _google_instrumentation is None:
265
+ _google_instrumentation = GoogleInstrumentation()
271
266
  return _google_instrumentation
272
267
 
273
268
 
@@ -3,8 +3,11 @@ OpenAI SDK Instrumentation
3
3
 
4
4
  Monkey-patches the OpenAI SDK to automatically emit OpenTelemetry spans
5
5
  for all chat completion API calls.
6
+
7
+ Thread-safe singleton pattern using double-checked locking.
6
8
  """
7
9
 
10
+ import threading
8
11
  import time
9
12
  from functools import wraps
10
13
  from typing import Any, Dict, Optional
@@ -15,38 +18,34 @@ from .base import BaseCostAdapter, BaseInstrumentation
15
18
 
16
19
 
17
20
  class OpenAICostAdapter(BaseCostAdapter):
18
- """Cost calculation adapter for OpenAI models"""
19
-
20
- # Pricing per 1K tokens (USD) - Updated November 2025
21
- PRICING = {
22
- # GPT-5 models
23
- "gpt-5": {"input": 0.005, "output": 0.015},
24
- "gpt-5-turbo": {"input": 0.0025, "output": 0.0075},
25
- # GPT-4 models
26
- "gpt-4": {"input": 0.03, "output": 0.06},
27
- "gpt-4-turbo": {"input": 0.01, "output": 0.03},
28
- "gpt-4o": {"input": 0.0025, "output": 0.01},
29
- "gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
30
- # GPT-3.5 models
31
- "gpt-3.5-turbo": {"input": 0.0005, "output": 0.0015},
32
- "gpt-3.5-turbo-16k": {"input": 0.001, "output": 0.002},
33
- }
21
+ """Cost calculation adapter for OpenAI models.
22
+
23
+ Uses centralized pricing from kalibr.pricing module.
24
+ """
34
25
 
35
- def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
36
- """Calculate cost in USD for an OpenAI API call"""
37
- # Normalize model name (remove version suffixes)
38
- base_model = model.split("-2")[0] # Remove date suffixes like -20240101
26
+ def get_vendor_name(self) -> str:
27
+ """Return vendor name for OpenAI."""
28
+ return "openai"
39
29
 
40
- pricing = self.get_pricing(base_model)
41
- if not pricing:
42
- # Default to GPT-4 pricing if unknown
43
- pricing = {"input": 0.03, "output": 0.06}
30
+ def calculate_cost(self, model: str, usage: Dict[str, int]) -> float:
31
+ """Calculate cost in USD for an OpenAI API call.
32
+
33
+ Args:
34
+ model: Model identifier (e.g., "gpt-4o", "gpt-4o-2024-05-13")
35
+ usage: Token usage dict with prompt_tokens and completion_tokens
36
+
37
+ Returns:
38
+ Cost in USD (rounded to 6 decimal places)
39
+ """
40
+ # Get pricing from centralized module (handles normalization)
41
+ pricing = self.get_pricing_for_model(model)
44
42
 
45
43
  prompt_tokens = usage.get("prompt_tokens", 0)
46
44
  completion_tokens = usage.get("completion_tokens", 0)
47
45
 
48
- input_cost = (prompt_tokens / 1000) * pricing["input"]
49
- output_cost = (completion_tokens / 1000) * pricing["output"]
46
+ # Calculate cost (pricing is per 1M tokens)
47
+ input_cost = (prompt_tokens / 1_000_000) * pricing["input"]
48
+ output_cost = (completion_tokens / 1_000_000) * pricing["output"]
50
49
 
51
50
  return round(input_cost + output_cost, 6)
52
51
 
@@ -245,13 +244,20 @@ class OpenAIInstrumentation(BaseInstrumentation):
245
244
 
246
245
  # Singleton instance
247
246
  _openai_instrumentation = None
247
+ _openai_lock = threading.Lock()
248
248
 
249
249
 
250
250
  def get_instrumentation() -> OpenAIInstrumentation:
251
- """Get or create the OpenAI instrumentation singleton"""
251
+ """Get or create the OpenAI instrumentation singleton.
252
+
253
+ Thread-safe singleton pattern using double-checked locking.
254
+ """
252
255
  global _openai_instrumentation
253
256
  if _openai_instrumentation is None:
254
- _openai_instrumentation = OpenAIInstrumentation()
257
+ with _openai_lock:
258
+ # Double-check inside lock to prevent race condition
259
+ if _openai_instrumentation is None:
260
+ _openai_instrumentation = OpenAIInstrumentation()
255
261
  return _openai_instrumentation
256
262
 
257
263