isa-model 0.3.4__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.
Files changed (100) hide show
  1. isa_model/__init__.py +30 -1
  2. isa_model/client.py +770 -0
  3. isa_model/core/config/__init__.py +16 -0
  4. isa_model/core/config/config_manager.py +514 -0
  5. isa_model/core/config.py +426 -0
  6. isa_model/core/models/model_billing_tracker.py +476 -0
  7. isa_model/core/models/model_manager.py +399 -0
  8. isa_model/core/models/model_repo.py +343 -0
  9. isa_model/core/pricing_manager.py +426 -0
  10. isa_model/core/services/__init__.py +19 -0
  11. isa_model/core/services/intelligent_model_selector.py +547 -0
  12. isa_model/core/types.py +291 -0
  13. isa_model/deployment/__init__.py +2 -0
  14. isa_model/deployment/cloud/__init__.py +9 -0
  15. isa_model/deployment/cloud/modal/__init__.py +10 -0
  16. isa_model/deployment/cloud/modal/isa_vision_doc_service.py +766 -0
  17. isa_model/deployment/cloud/modal/isa_vision_table_service.py +532 -0
  18. isa_model/deployment/cloud/modal/isa_vision_ui_service.py +406 -0
  19. isa_model/deployment/cloud/modal/register_models.py +321 -0
  20. isa_model/deployment/runtime/deployed_service.py +338 -0
  21. isa_model/deployment/services/__init__.py +9 -0
  22. isa_model/deployment/services/auto_deploy_vision_service.py +537 -0
  23. isa_model/deployment/services/model_service.py +332 -0
  24. isa_model/deployment/services/service_monitor.py +356 -0
  25. isa_model/deployment/services/service_registry.py +527 -0
  26. isa_model/eval/__init__.py +80 -44
  27. isa_model/eval/config/__init__.py +10 -0
  28. isa_model/eval/config/evaluation_config.py +108 -0
  29. isa_model/eval/evaluators/__init__.py +18 -0
  30. isa_model/eval/evaluators/base_evaluator.py +503 -0
  31. isa_model/eval/evaluators/llm_evaluator.py +472 -0
  32. isa_model/eval/factory.py +417 -709
  33. isa_model/eval/infrastructure/__init__.py +24 -0
  34. isa_model/eval/infrastructure/experiment_tracker.py +466 -0
  35. isa_model/eval/metrics.py +191 -21
  36. isa_model/inference/ai_factory.py +187 -387
  37. isa_model/inference/providers/modal_provider.py +109 -0
  38. isa_model/inference/providers/yyds_provider.py +108 -0
  39. isa_model/inference/services/__init__.py +2 -1
  40. isa_model/inference/services/audio/base_stt_service.py +65 -1
  41. isa_model/inference/services/audio/base_tts_service.py +75 -1
  42. isa_model/inference/services/audio/openai_stt_service.py +189 -151
  43. isa_model/inference/services/audio/openai_tts_service.py +12 -10
  44. isa_model/inference/services/audio/replicate_tts_service.py +61 -56
  45. isa_model/inference/services/base_service.py +55 -55
  46. isa_model/inference/services/embedding/base_embed_service.py +65 -1
  47. isa_model/inference/services/embedding/ollama_embed_service.py +103 -43
  48. isa_model/inference/services/embedding/openai_embed_service.py +8 -10
  49. isa_model/inference/services/helpers/stacked_config.py +148 -0
  50. isa_model/inference/services/img/__init__.py +18 -0
  51. isa_model/inference/services/{vision → img}/base_image_gen_service.py +80 -35
  52. isa_model/inference/services/img/flux_professional_service.py +603 -0
  53. isa_model/inference/services/img/helpers/base_stacked_service.py +274 -0
  54. isa_model/inference/services/{vision → img}/replicate_image_gen_service.py +210 -69
  55. isa_model/inference/services/llm/__init__.py +3 -3
  56. isa_model/inference/services/llm/base_llm_service.py +519 -35
  57. isa_model/inference/services/llm/{llm_adapter.py → helpers/llm_adapter.py} +40 -0
  58. isa_model/inference/services/llm/helpers/llm_prompts.py +258 -0
  59. isa_model/inference/services/llm/helpers/llm_utils.py +280 -0
  60. isa_model/inference/services/llm/ollama_llm_service.py +150 -15
  61. isa_model/inference/services/llm/openai_llm_service.py +134 -31
  62. isa_model/inference/services/llm/yyds_llm_service.py +255 -0
  63. isa_model/inference/services/vision/__init__.py +38 -4
  64. isa_model/inference/services/vision/base_vision_service.py +241 -96
  65. isa_model/inference/services/vision/disabled/isA_vision_service.py +500 -0
  66. isa_model/inference/services/vision/doc_analysis_service.py +640 -0
  67. isa_model/inference/services/vision/helpers/base_stacked_service.py +274 -0
  68. isa_model/inference/services/vision/helpers/image_utils.py +272 -3
  69. isa_model/inference/services/vision/helpers/vision_prompts.py +297 -0
  70. isa_model/inference/services/vision/openai_vision_service.py +109 -170
  71. isa_model/inference/services/vision/replicate_vision_service.py +508 -0
  72. isa_model/inference/services/vision/ui_analysis_service.py +823 -0
  73. isa_model/scripts/register_models.py +370 -0
  74. isa_model/scripts/register_models_with_embeddings.py +510 -0
  75. isa_model/serving/__init__.py +19 -0
  76. isa_model/serving/api/__init__.py +10 -0
  77. isa_model/serving/api/fastapi_server.py +89 -0
  78. isa_model/serving/api/middleware/__init__.py +9 -0
  79. isa_model/serving/api/middleware/request_logger.py +88 -0
  80. isa_model/serving/api/routes/__init__.py +5 -0
  81. isa_model/serving/api/routes/health.py +82 -0
  82. isa_model/serving/api/routes/llm.py +19 -0
  83. isa_model/serving/api/routes/ui_analysis.py +223 -0
  84. isa_model/serving/api/routes/unified.py +202 -0
  85. isa_model/serving/api/routes/vision.py +19 -0
  86. isa_model/serving/api/schemas/__init__.py +17 -0
  87. isa_model/serving/api/schemas/common.py +33 -0
  88. isa_model/serving/api/schemas/ui_analysis.py +78 -0
  89. {isa_model-0.3.4.dist-info → isa_model-0.3.6.dist-info}/METADATA +4 -1
  90. isa_model-0.3.6.dist-info/RECORD +147 -0
  91. isa_model/core/model_manager.py +0 -208
  92. isa_model/core/model_registry.py +0 -342
  93. isa_model/inference/billing_tracker.py +0 -406
  94. isa_model/inference/services/llm/triton_llm_service.py +0 -481
  95. isa_model/inference/services/vision/ollama_vision_service.py +0 -194
  96. isa_model-0.3.4.dist-info/RECORD +0 -91
  97. /isa_model/core/{model_storage.py → models/model_storage.py} +0 -0
  98. /isa_model/inference/services/{vision → embedding}/helpers/text_splitter.py +0 -0
  99. {isa_model-0.3.4.dist-info → isa_model-0.3.6.dist-info}/WHEEL +0 -0
  100. {isa_model-0.3.4.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']}")