isa-model 0.3.5__py3-none-any.whl → 0.3.6__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.
- isa_model/__init__.py +30 -1
- isa_model/client.py +770 -0
- isa_model/core/config/__init__.py +16 -0
- isa_model/core/config/config_manager.py +514 -0
- isa_model/core/config.py +426 -0
- isa_model/core/models/model_billing_tracker.py +476 -0
- isa_model/core/models/model_manager.py +399 -0
- isa_model/core/{storage/supabase_storage.py → models/model_repo.py} +72 -73
- isa_model/core/pricing_manager.py +426 -0
- isa_model/core/services/__init__.py +19 -0
- isa_model/core/services/intelligent_model_selector.py +547 -0
- isa_model/core/types.py +291 -0
- isa_model/deployment/__init__.py +2 -0
- isa_model/deployment/cloud/modal/isa_vision_doc_service.py +157 -3
- isa_model/deployment/cloud/modal/isa_vision_table_service.py +532 -0
- isa_model/deployment/cloud/modal/isa_vision_ui_service.py +104 -3
- isa_model/deployment/cloud/modal/register_models.py +321 -0
- isa_model/deployment/runtime/deployed_service.py +338 -0
- isa_model/deployment/services/__init__.py +9 -0
- isa_model/deployment/services/auto_deploy_vision_service.py +537 -0
- isa_model/deployment/services/model_service.py +332 -0
- isa_model/deployment/services/service_monitor.py +356 -0
- isa_model/deployment/services/service_registry.py +527 -0
- isa_model/eval/__init__.py +80 -44
- isa_model/eval/config/__init__.py +10 -0
- isa_model/eval/config/evaluation_config.py +108 -0
- isa_model/eval/evaluators/__init__.py +18 -0
- isa_model/eval/evaluators/base_evaluator.py +503 -0
- isa_model/eval/evaluators/llm_evaluator.py +472 -0
- isa_model/eval/factory.py +417 -709
- isa_model/eval/infrastructure/__init__.py +24 -0
- isa_model/eval/infrastructure/experiment_tracker.py +466 -0
- isa_model/eval/metrics.py +191 -21
- isa_model/inference/ai_factory.py +181 -605
- isa_model/inference/services/audio/base_stt_service.py +65 -1
- isa_model/inference/services/audio/base_tts_service.py +75 -1
- isa_model/inference/services/audio/openai_stt_service.py +189 -151
- isa_model/inference/services/audio/openai_tts_service.py +12 -10
- isa_model/inference/services/audio/replicate_tts_service.py +61 -56
- isa_model/inference/services/base_service.py +55 -17
- isa_model/inference/services/embedding/base_embed_service.py +65 -1
- isa_model/inference/services/embedding/ollama_embed_service.py +103 -43
- isa_model/inference/services/embedding/openai_embed_service.py +8 -10
- isa_model/inference/services/helpers/stacked_config.py +148 -0
- isa_model/inference/services/img/__init__.py +18 -0
- isa_model/inference/services/{vision → img}/base_image_gen_service.py +80 -1
- isa_model/inference/services/{stacked → img}/flux_professional_service.py +25 -1
- isa_model/inference/services/{stacked → img/helpers}/base_stacked_service.py +40 -35
- isa_model/inference/services/{vision → img}/replicate_image_gen_service.py +44 -31
- isa_model/inference/services/llm/__init__.py +3 -3
- isa_model/inference/services/llm/base_llm_service.py +492 -40
- isa_model/inference/services/llm/helpers/llm_prompts.py +258 -0
- isa_model/inference/services/llm/helpers/llm_utils.py +280 -0
- isa_model/inference/services/llm/ollama_llm_service.py +51 -17
- isa_model/inference/services/llm/openai_llm_service.py +70 -19
- isa_model/inference/services/llm/yyds_llm_service.py +24 -23
- isa_model/inference/services/vision/__init__.py +38 -4
- isa_model/inference/services/vision/base_vision_service.py +218 -117
- isa_model/inference/services/vision/{isA_vision_service.py → disabled/isA_vision_service.py} +98 -0
- isa_model/inference/services/{stacked → vision}/doc_analysis_service.py +1 -1
- isa_model/inference/services/vision/helpers/base_stacked_service.py +274 -0
- isa_model/inference/services/vision/helpers/image_utils.py +272 -3
- isa_model/inference/services/vision/helpers/vision_prompts.py +297 -0
- isa_model/inference/services/vision/openai_vision_service.py +104 -307
- isa_model/inference/services/vision/replicate_vision_service.py +140 -325
- isa_model/inference/services/{stacked → vision}/ui_analysis_service.py +2 -498
- isa_model/scripts/register_models.py +370 -0
- isa_model/scripts/register_models_with_embeddings.py +510 -0
- isa_model/serving/api/fastapi_server.py +6 -1
- isa_model/serving/api/routes/unified.py +202 -0
- {isa_model-0.3.5.dist-info → isa_model-0.3.6.dist-info}/METADATA +4 -1
- {isa_model-0.3.5.dist-info → isa_model-0.3.6.dist-info}/RECORD +77 -53
- isa_model/config/__init__.py +0 -9
- isa_model/config/config_manager.py +0 -213
- isa_model/core/model_manager.py +0 -213
- isa_model/core/model_registry.py +0 -375
- isa_model/core/vision_models_init.py +0 -116
- isa_model/inference/billing_tracker.py +0 -406
- isa_model/inference/services/llm/triton_llm_service.py +0 -481
- isa_model/inference/services/stacked/__init__.py +0 -26
- isa_model/inference/services/stacked/config.py +0 -426
- isa_model/inference/services/vision/ollama_vision_service.py +0 -194
- /isa_model/core/{model_storage.py → models/model_storage.py} +0 -0
- /isa_model/inference/services/{vision → embedding}/helpers/text_splitter.py +0 -0
- /isa_model/inference/services/llm/{llm_adapter.py → helpers/llm_adapter.py} +0 -0
- {isa_model-0.3.5.dist-info → isa_model-0.3.6.dist-info}/WHEEL +0 -0
- {isa_model-0.3.5.dist-info → isa_model-0.3.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,476 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
"""
|
5
|
+
Model Billing Tracker - Core billing and usage tracking for model lifecycle management
|
6
|
+
|
7
|
+
This module tracks model usage, costs, and billing across all lifecycle stages:
|
8
|
+
- Training costs
|
9
|
+
- Evaluation costs
|
10
|
+
- Deployment costs
|
11
|
+
- Inference costs
|
12
|
+
|
13
|
+
Integrates with ModelRegistry to store billing data in Supabase.
|
14
|
+
"""
|
15
|
+
|
16
|
+
from typing import Dict, List, Optional, Any, Union
|
17
|
+
from datetime import datetime, timezone
|
18
|
+
from dataclasses import dataclass, asdict
|
19
|
+
import json
|
20
|
+
import logging
|
21
|
+
from pathlib import Path
|
22
|
+
from enum import Enum
|
23
|
+
import os
|
24
|
+
|
25
|
+
logger = logging.getLogger(__name__)
|
26
|
+
|
27
|
+
class ModelOperationType(Enum):
|
28
|
+
"""Types of model operations that incur costs"""
|
29
|
+
TRAINING = "training"
|
30
|
+
EVALUATION = "evaluation"
|
31
|
+
DEPLOYMENT = "deployment"
|
32
|
+
INFERENCE = "inference"
|
33
|
+
STORAGE = "storage"
|
34
|
+
|
35
|
+
class ServiceType(Enum):
|
36
|
+
"""Types of AI services"""
|
37
|
+
LLM = "llm"
|
38
|
+
EMBEDDING = "embedding"
|
39
|
+
VISION = "vision"
|
40
|
+
IMAGE_GENERATION = "image_generation"
|
41
|
+
AUDIO_STT = "audio_stt"
|
42
|
+
AUDIO_TTS = "audio_tts"
|
43
|
+
|
44
|
+
@dataclass
|
45
|
+
class ModelUsageRecord:
|
46
|
+
"""Record of model usage across its lifecycle"""
|
47
|
+
timestamp: str
|
48
|
+
model_id: str
|
49
|
+
operation_type: str # ModelOperationType
|
50
|
+
provider: str
|
51
|
+
service_type: str # ServiceType
|
52
|
+
operation: str # Specific operation (e.g., 'chat', 'train', 'deploy')
|
53
|
+
input_tokens: Optional[int] = None
|
54
|
+
output_tokens: Optional[int] = None
|
55
|
+
total_tokens: Optional[int] = None
|
56
|
+
input_units: Optional[float] = None # For non-token based services
|
57
|
+
output_units: Optional[float] = None
|
58
|
+
cost_usd: Optional[float] = None
|
59
|
+
metadata: Optional[Dict[str, Any]] = None
|
60
|
+
|
61
|
+
def to_dict(self) -> Dict[str, Any]:
|
62
|
+
"""Convert to dictionary"""
|
63
|
+
return asdict(self)
|
64
|
+
|
65
|
+
@classmethod
|
66
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'ModelUsageRecord':
|
67
|
+
"""Create from dictionary, filtering out database-specific fields"""
|
68
|
+
# Filter out database fields that aren't part of the ModelUsageRecord
|
69
|
+
filtered_data = {
|
70
|
+
k: v for k, v in data.items()
|
71
|
+
if k in ['timestamp', 'model_id', 'operation_type', 'provider', 'service_type',
|
72
|
+
'operation', 'input_tokens', 'output_tokens', 'total_tokens',
|
73
|
+
'input_units', 'output_units', 'cost_usd', 'metadata']
|
74
|
+
}
|
75
|
+
return cls(**filtered_data)
|
76
|
+
|
77
|
+
class ModelBillingTracker:
|
78
|
+
"""
|
79
|
+
Core billing tracker for model lifecycle management
|
80
|
+
|
81
|
+
Integrates with ModelRegistry to store billing data in Supabase.
|
82
|
+
Provides unified cost tracking across training, evaluation, deployment, and inference.
|
83
|
+
"""
|
84
|
+
|
85
|
+
def __init__(self, model_registry=None, storage_path: Optional[str] = None):
|
86
|
+
"""
|
87
|
+
Initialize model billing tracker
|
88
|
+
|
89
|
+
Args:
|
90
|
+
model_registry: ModelRegistry instance for database storage
|
91
|
+
storage_path: Fallback local storage path
|
92
|
+
"""
|
93
|
+
self.model_registry = model_registry
|
94
|
+
|
95
|
+
# Fallback to local storage if no registry provided
|
96
|
+
if storage_path is None:
|
97
|
+
project_root = Path(__file__).parent.parent.parent.parent # Go up one more level to reach project root
|
98
|
+
self.storage_path = project_root / "model_billing_data.json"
|
99
|
+
else:
|
100
|
+
self.storage_path = Path(storage_path)
|
101
|
+
|
102
|
+
self.usage_records: List[ModelUsageRecord] = []
|
103
|
+
self.session_start = datetime.now(timezone.utc).isoformat()
|
104
|
+
|
105
|
+
# Load existing data
|
106
|
+
self._load_data()
|
107
|
+
|
108
|
+
def _load_data(self):
|
109
|
+
"""Load existing billing data from registry or local storage"""
|
110
|
+
try:
|
111
|
+
if self.model_registry and hasattr(self.model_registry, 'supabase'):
|
112
|
+
# Load from Supabase
|
113
|
+
self._load_from_supabase()
|
114
|
+
else:
|
115
|
+
# Load from local storage
|
116
|
+
self._load_from_local()
|
117
|
+
except Exception as e:
|
118
|
+
logger.warning(f"Could not load billing data: {e}")
|
119
|
+
self.usage_records = []
|
120
|
+
|
121
|
+
def _load_from_supabase(self):
|
122
|
+
"""Load billing data from Supabase"""
|
123
|
+
try:
|
124
|
+
if not self.model_registry or not hasattr(self.model_registry, 'supabase'):
|
125
|
+
logger.warning("No Supabase client available for billing data loading")
|
126
|
+
self.usage_records = []
|
127
|
+
return
|
128
|
+
|
129
|
+
# Query model_usage table for recent usage records (last 30 days)
|
130
|
+
from datetime import datetime, timedelta
|
131
|
+
thirty_days_ago = (datetime.now() - timedelta(days=30)).isoformat()
|
132
|
+
|
133
|
+
result = self.model_registry.supabase.table('model_usage').select('*').gte('timestamp', thirty_days_ago).order('timestamp', desc=True).execute()
|
134
|
+
|
135
|
+
if result.data:
|
136
|
+
self.usage_records = [
|
137
|
+
ModelUsageRecord.from_dict(record)
|
138
|
+
for record in result.data
|
139
|
+
]
|
140
|
+
logger.info(f"Loaded {len(self.usage_records)} billing records from Supabase")
|
141
|
+
else:
|
142
|
+
self.usage_records = []
|
143
|
+
logger.info("No billing records found in Supabase")
|
144
|
+
|
145
|
+
except Exception as e:
|
146
|
+
logger.error(f"Failed to load billing data from Supabase: {e}")
|
147
|
+
# Fallback to empty records
|
148
|
+
self.usage_records = []
|
149
|
+
|
150
|
+
def _load_from_local(self):
|
151
|
+
"""Load billing data from local JSON file"""
|
152
|
+
if self.storage_path.exists():
|
153
|
+
with open(self.storage_path, 'r') as f:
|
154
|
+
data = json.load(f)
|
155
|
+
self.usage_records = [
|
156
|
+
ModelUsageRecord.from_dict(record)
|
157
|
+
for record in data.get('usage_records', [])
|
158
|
+
]
|
159
|
+
logger.info(f"Loaded {len(self.usage_records)} billing records from local storage")
|
160
|
+
|
161
|
+
def _save_data(self):
|
162
|
+
"""Save billing data to registry or local storage"""
|
163
|
+
try:
|
164
|
+
if self.model_registry and hasattr(self.model_registry, 'supabase'):
|
165
|
+
self._save_to_supabase()
|
166
|
+
else:
|
167
|
+
self._save_to_local()
|
168
|
+
except Exception as e:
|
169
|
+
logger.error(f"Could not save billing data: {e}")
|
170
|
+
|
171
|
+
def _save_to_supabase(self):
|
172
|
+
"""Save billing data to Supabase"""
|
173
|
+
try:
|
174
|
+
if not self.model_registry or not hasattr(self.model_registry, 'supabase'):
|
175
|
+
logger.warning("No Supabase client available for billing data saving")
|
176
|
+
return
|
177
|
+
|
178
|
+
if not self.usage_records:
|
179
|
+
logger.debug("No usage records to save")
|
180
|
+
return
|
181
|
+
|
182
|
+
# Convert usage records to dict format for Supabase
|
183
|
+
records_to_save = []
|
184
|
+
for record in self.usage_records:
|
185
|
+
record_dict = record.to_dict()
|
186
|
+
# Ensure all required fields are present and properly formatted
|
187
|
+
record_dict['created_at'] = record_dict.get('timestamp')
|
188
|
+
records_to_save.append(record_dict)
|
189
|
+
|
190
|
+
# Insert records into model_usage table (upsert to handle duplicates)
|
191
|
+
result = self.model_registry.supabase.table('model_usage').upsert(
|
192
|
+
records_to_save,
|
193
|
+
on_conflict='timestamp,model_id,operation' # Avoid duplicates based on these fields
|
194
|
+
).execute()
|
195
|
+
|
196
|
+
if result.data:
|
197
|
+
logger.info(f"Successfully saved {len(result.data)} billing records to Supabase")
|
198
|
+
else:
|
199
|
+
logger.warning("No records were saved to Supabase")
|
200
|
+
|
201
|
+
except Exception as e:
|
202
|
+
logger.error(f"Failed to save billing data to Supabase: {e}")
|
203
|
+
# Fallback to local storage on Supabase failure
|
204
|
+
logger.info("Falling back to local storage for billing data")
|
205
|
+
self._save_to_local()
|
206
|
+
|
207
|
+
def _save_to_local(self):
|
208
|
+
"""Save billing data to local JSON file"""
|
209
|
+
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
|
210
|
+
|
211
|
+
data = {
|
212
|
+
"session_start": self.session_start,
|
213
|
+
"last_updated": datetime.now(timezone.utc).isoformat(),
|
214
|
+
"usage_records": [record.to_dict() for record in self.usage_records]
|
215
|
+
}
|
216
|
+
|
217
|
+
with open(self.storage_path, 'w') as f:
|
218
|
+
json.dump(data, f, indent=2)
|
219
|
+
|
220
|
+
def track_model_usage(
|
221
|
+
self,
|
222
|
+
model_id: str,
|
223
|
+
operation_type: Union[str, ModelOperationType],
|
224
|
+
provider: str,
|
225
|
+
service_type: Union[str, ServiceType],
|
226
|
+
operation: str,
|
227
|
+
input_tokens: Optional[int] = None,
|
228
|
+
output_tokens: Optional[int] = None,
|
229
|
+
input_units: Optional[float] = None,
|
230
|
+
output_units: Optional[float] = None,
|
231
|
+
cost_usd: Optional[float] = None,
|
232
|
+
metadata: Optional[Dict[str, Any]] = None
|
233
|
+
) -> ModelUsageRecord:
|
234
|
+
"""
|
235
|
+
Track model usage across its lifecycle
|
236
|
+
|
237
|
+
Args:
|
238
|
+
model_id: Unique model identifier
|
239
|
+
operation_type: Type of operation (training, evaluation, deployment, inference)
|
240
|
+
provider: Provider name (openai, replicate, etc.)
|
241
|
+
service_type: Type of service
|
242
|
+
operation: Specific operation performed
|
243
|
+
input_tokens: Number of input tokens
|
244
|
+
output_tokens: Number of output tokens
|
245
|
+
input_units: Input units for non-token services
|
246
|
+
output_units: Output units for non-token services
|
247
|
+
cost_usd: Cost in USD for this operation
|
248
|
+
metadata: Additional metadata
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
ModelUsageRecord object
|
252
|
+
"""
|
253
|
+
# Convert enums to strings
|
254
|
+
if isinstance(operation_type, ModelOperationType):
|
255
|
+
operation_type = operation_type.value
|
256
|
+
if isinstance(service_type, ServiceType):
|
257
|
+
service_type = service_type.value
|
258
|
+
|
259
|
+
# Calculate total tokens
|
260
|
+
total_tokens = None
|
261
|
+
if input_tokens is not None or output_tokens is not None:
|
262
|
+
total_tokens = (input_tokens or 0) + (output_tokens or 0)
|
263
|
+
|
264
|
+
# Use provided cost_usd or calculate it
|
265
|
+
if cost_usd is None:
|
266
|
+
cost_usd = self._calculate_cost(
|
267
|
+
provider, model_id, operation_type,
|
268
|
+
input_tokens, output_tokens, input_units, output_units
|
269
|
+
)
|
270
|
+
|
271
|
+
# Create usage record
|
272
|
+
record = ModelUsageRecord(
|
273
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
274
|
+
model_id=model_id,
|
275
|
+
operation_type=operation_type,
|
276
|
+
provider=provider,
|
277
|
+
service_type=service_type,
|
278
|
+
operation=operation,
|
279
|
+
input_tokens=input_tokens,
|
280
|
+
output_tokens=output_tokens,
|
281
|
+
total_tokens=total_tokens,
|
282
|
+
input_units=input_units,
|
283
|
+
output_units=output_units,
|
284
|
+
cost_usd=cost_usd,
|
285
|
+
metadata=metadata or {}
|
286
|
+
)
|
287
|
+
|
288
|
+
# Add to records and save
|
289
|
+
self.usage_records.append(record)
|
290
|
+
self._save_data()
|
291
|
+
|
292
|
+
logger.info(f"Tracked model usage: {model_id} - {operation_type} - ${cost_usd:.6f}")
|
293
|
+
return record
|
294
|
+
|
295
|
+
def _calculate_cost(
|
296
|
+
self,
|
297
|
+
provider: str,
|
298
|
+
model_id: str,
|
299
|
+
operation_type: str,
|
300
|
+
input_tokens: Optional[int] = None,
|
301
|
+
output_tokens: Optional[int] = None,
|
302
|
+
input_units: Optional[float] = None,
|
303
|
+
output_units: Optional[float] = None
|
304
|
+
) -> float:
|
305
|
+
"""Calculate cost for model usage"""
|
306
|
+
try:
|
307
|
+
# Import here to avoid circular imports
|
308
|
+
from .model_manager import ModelManager
|
309
|
+
|
310
|
+
# Get model info to determine provider model name
|
311
|
+
if self.model_registry:
|
312
|
+
model_info = self.model_registry.get_model_info(model_id)
|
313
|
+
if model_info and model_info.get('metadata'):
|
314
|
+
provider_model_name = model_info['metadata'].get('provider_model_name')
|
315
|
+
if provider_model_name:
|
316
|
+
# Use ModelManager pricing
|
317
|
+
pricing = ModelManager.MODEL_PRICING.get(provider, {}).get(provider_model_name)
|
318
|
+
if pricing:
|
319
|
+
cost = 0.0
|
320
|
+
if input_tokens is not None and "input" in pricing:
|
321
|
+
cost += (input_tokens / 1000000) * pricing["input"]
|
322
|
+
if output_tokens is not None and "output" in pricing:
|
323
|
+
cost += (output_tokens / 1000000) * pricing["output"]
|
324
|
+
return cost
|
325
|
+
|
326
|
+
# Fallback to default pricing if model not found
|
327
|
+
return 0.0
|
328
|
+
|
329
|
+
except Exception as e:
|
330
|
+
logger.error(f"Error calculating cost for model {model_id}: {e}")
|
331
|
+
return 0.0
|
332
|
+
|
333
|
+
def get_model_usage_summary(self, model_id: str) -> Dict[str, Any]:
|
334
|
+
"""Get usage summary for a specific model"""
|
335
|
+
model_records = [
|
336
|
+
record for record in self.usage_records
|
337
|
+
if record.model_id == model_id
|
338
|
+
]
|
339
|
+
|
340
|
+
return self._generate_summary(model_records, f"Model {model_id} Usage")
|
341
|
+
|
342
|
+
def get_operation_summary(self, operation_type: Union[str, ModelOperationType]) -> Dict[str, Any]:
|
343
|
+
"""Get usage summary for a specific operation type"""
|
344
|
+
if isinstance(operation_type, ModelOperationType):
|
345
|
+
operation_type = operation_type.value
|
346
|
+
|
347
|
+
operation_records = [
|
348
|
+
record for record in self.usage_records
|
349
|
+
if record.operation_type == operation_type
|
350
|
+
]
|
351
|
+
|
352
|
+
return self._generate_summary(operation_records, f"{operation_type.title()} Operations")
|
353
|
+
|
354
|
+
def get_provider_summary(self, provider: str) -> Dict[str, Any]:
|
355
|
+
"""Get usage summary for a specific provider"""
|
356
|
+
provider_records = [
|
357
|
+
record for record in self.usage_records
|
358
|
+
if record.provider == provider
|
359
|
+
]
|
360
|
+
|
361
|
+
return self._generate_summary(provider_records, f"{provider.title()} Usage")
|
362
|
+
|
363
|
+
def _generate_summary(self, records: List[ModelUsageRecord], title: str) -> Dict[str, Any]:
|
364
|
+
"""Generate usage summary from records"""
|
365
|
+
if not records:
|
366
|
+
return {
|
367
|
+
"title": title,
|
368
|
+
"total_cost": 0.0,
|
369
|
+
"total_requests": 0,
|
370
|
+
"operations": {},
|
371
|
+
"models": {},
|
372
|
+
"providers": {}
|
373
|
+
}
|
374
|
+
|
375
|
+
total_cost = sum(record.cost_usd or 0 for record in records)
|
376
|
+
total_requests = len(records)
|
377
|
+
|
378
|
+
# Group by operation type
|
379
|
+
operations = {}
|
380
|
+
for record in records:
|
381
|
+
if record.operation_type not in operations:
|
382
|
+
operations[record.operation_type] = {
|
383
|
+
"cost": 0.0,
|
384
|
+
"requests": 0
|
385
|
+
}
|
386
|
+
operations[record.operation_type]["cost"] += record.cost_usd or 0
|
387
|
+
operations[record.operation_type]["requests"] += 1
|
388
|
+
|
389
|
+
# Group by model
|
390
|
+
models = {}
|
391
|
+
for record in records:
|
392
|
+
if record.model_id not in models:
|
393
|
+
models[record.model_id] = {
|
394
|
+
"cost": 0.0,
|
395
|
+
"requests": 0,
|
396
|
+
"total_tokens": 0
|
397
|
+
}
|
398
|
+
models[record.model_id]["cost"] += record.cost_usd or 0
|
399
|
+
models[record.model_id]["requests"] += 1
|
400
|
+
if record.total_tokens:
|
401
|
+
models[record.model_id]["total_tokens"] += record.total_tokens
|
402
|
+
|
403
|
+
# Group by provider
|
404
|
+
providers = {}
|
405
|
+
for record in records:
|
406
|
+
if record.provider not in providers:
|
407
|
+
providers[record.provider] = {
|
408
|
+
"cost": 0.0,
|
409
|
+
"requests": 0
|
410
|
+
}
|
411
|
+
providers[record.provider]["cost"] += record.cost_usd or 0
|
412
|
+
providers[record.provider]["requests"] += 1
|
413
|
+
|
414
|
+
return {
|
415
|
+
"title": title,
|
416
|
+
"total_cost": round(total_cost, 6),
|
417
|
+
"total_requests": total_requests,
|
418
|
+
"operations": operations,
|
419
|
+
"models": models,
|
420
|
+
"providers": providers,
|
421
|
+
"period": {
|
422
|
+
"start": records[0].timestamp if records else None,
|
423
|
+
"end": records[-1].timestamp if records else None
|
424
|
+
}
|
425
|
+
}
|
426
|
+
|
427
|
+
def print_model_summary(self, model_id: str):
|
428
|
+
"""Print usage summary for a specific model"""
|
429
|
+
summary = self.get_model_usage_summary(model_id)
|
430
|
+
|
431
|
+
print(f"\n🤖 {summary['title']} Summary")
|
432
|
+
print("=" * 50)
|
433
|
+
print(f"💵 Total Cost: ${summary['total_cost']:.6f}")
|
434
|
+
print(f"📊 Total Operations: {summary['total_requests']}")
|
435
|
+
|
436
|
+
if summary['operations']:
|
437
|
+
print("\n📈 By Operation Type:")
|
438
|
+
for operation, data in summary['operations'].items():
|
439
|
+
print(f" {operation}: ${data['cost']:.6f} ({data['requests']} operations)")
|
440
|
+
|
441
|
+
if summary['providers']:
|
442
|
+
print("\n🔧 By Provider:")
|
443
|
+
for provider, data in summary['providers'].items():
|
444
|
+
print(f" {provider}: ${data['cost']:.6f} ({data['requests']} requests)")
|
445
|
+
|
446
|
+
# Global model billing tracker instance
|
447
|
+
_global_model_tracker: Optional[ModelBillingTracker] = None
|
448
|
+
|
449
|
+
def get_model_billing_tracker() -> ModelBillingTracker:
|
450
|
+
"""Get the global model billing tracker instance"""
|
451
|
+
global _global_model_tracker
|
452
|
+
if _global_model_tracker is None:
|
453
|
+
# Try to get ModelRegistry instance
|
454
|
+
try:
|
455
|
+
from .model_repo import ModelRegistry
|
456
|
+
registry = ModelRegistry()
|
457
|
+
_global_model_tracker = ModelBillingTracker(model_registry=registry)
|
458
|
+
except Exception:
|
459
|
+
_global_model_tracker = ModelBillingTracker()
|
460
|
+
return _global_model_tracker
|
461
|
+
|
462
|
+
def track_model_usage(**kwargs) -> ModelUsageRecord:
|
463
|
+
"""Convenience function to track model usage"""
|
464
|
+
return get_model_billing_tracker().track_model_usage(**kwargs)
|
465
|
+
|
466
|
+
def print_model_billing_summary(model_id: str = None, operation_type: str = None):
|
467
|
+
"""Convenience function to print billing summary"""
|
468
|
+
tracker = get_model_billing_tracker()
|
469
|
+
if model_id:
|
470
|
+
tracker.print_model_summary(model_id)
|
471
|
+
elif operation_type:
|
472
|
+
summary = tracker.get_operation_summary(operation_type)
|
473
|
+
print(f"\n💰 {summary['title']} Summary")
|
474
|
+
print("=" * 50)
|
475
|
+
print(f"💵 Total Cost: ${summary['total_cost']:.6f}")
|
476
|
+
print(f"📊 Total Operations: {summary['total_requests']}")
|