isa-model 0.3.9__py3-none-any.whl → 0.4.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.
Files changed (124) hide show
  1. isa_model/__init__.py +1 -1
  2. isa_model/client.py +732 -565
  3. isa_model/core/cache/redis_cache.py +401 -0
  4. isa_model/core/config/config_manager.py +53 -10
  5. isa_model/core/config.py +1 -1
  6. isa_model/core/database/__init__.py +1 -0
  7. isa_model/core/database/migrations.py +277 -0
  8. isa_model/core/database/supabase_client.py +123 -0
  9. isa_model/core/models/__init__.py +37 -0
  10. isa_model/core/models/model_billing_tracker.py +60 -88
  11. isa_model/core/models/model_manager.py +36 -18
  12. isa_model/core/models/model_repo.py +44 -38
  13. isa_model/core/models/model_statistics_tracker.py +234 -0
  14. isa_model/core/models/model_storage.py +0 -1
  15. isa_model/core/models/model_version_manager.py +959 -0
  16. isa_model/core/pricing_manager.py +2 -249
  17. isa_model/core/resilience/circuit_breaker.py +366 -0
  18. isa_model/core/security/secrets.py +358 -0
  19. isa_model/core/services/__init__.py +2 -4
  20. isa_model/core/services/intelligent_model_selector.py +101 -370
  21. isa_model/core/storage/hf_storage.py +1 -1
  22. isa_model/core/types.py +7 -0
  23. isa_model/deployment/cloud/modal/isa_audio_chatTTS_service.py +520 -0
  24. isa_model/deployment/cloud/modal/isa_audio_fish_service.py +0 -0
  25. isa_model/deployment/cloud/modal/isa_audio_openvoice_service.py +758 -0
  26. isa_model/deployment/cloud/modal/isa_audio_service_v2.py +1044 -0
  27. isa_model/deployment/cloud/modal/isa_embed_rerank_service.py +296 -0
  28. isa_model/deployment/cloud/modal/isa_video_hunyuan_service.py +423 -0
  29. isa_model/deployment/cloud/modal/isa_vision_ocr_service.py +519 -0
  30. isa_model/deployment/cloud/modal/isa_vision_qwen25_service.py +709 -0
  31. isa_model/deployment/cloud/modal/isa_vision_table_service.py +467 -323
  32. isa_model/deployment/cloud/modal/isa_vision_ui_service.py +607 -180
  33. isa_model/deployment/cloud/modal/isa_vision_ui_service_optimized.py +660 -0
  34. isa_model/deployment/core/deployment_manager.py +6 -4
  35. isa_model/deployment/services/auto_hf_modal_deployer.py +894 -0
  36. isa_model/eval/benchmarks/__init__.py +27 -0
  37. isa_model/eval/benchmarks/multimodal_datasets.py +460 -0
  38. isa_model/eval/benchmarks.py +244 -12
  39. isa_model/eval/evaluators/__init__.py +8 -2
  40. isa_model/eval/evaluators/audio_evaluator.py +727 -0
  41. isa_model/eval/evaluators/embedding_evaluator.py +742 -0
  42. isa_model/eval/evaluators/vision_evaluator.py +564 -0
  43. isa_model/eval/example_evaluation.py +395 -0
  44. isa_model/eval/factory.py +272 -5
  45. isa_model/eval/isa_benchmarks.py +700 -0
  46. isa_model/eval/isa_integration.py +582 -0
  47. isa_model/eval/metrics.py +159 -6
  48. isa_model/eval/tests/unit/test_basic.py +396 -0
  49. isa_model/inference/ai_factory.py +44 -8
  50. isa_model/inference/services/audio/__init__.py +21 -0
  51. isa_model/inference/services/audio/base_realtime_service.py +225 -0
  52. isa_model/inference/services/audio/isa_tts_service.py +0 -0
  53. isa_model/inference/services/audio/openai_realtime_service.py +320 -124
  54. isa_model/inference/services/audio/openai_stt_service.py +32 -6
  55. isa_model/inference/services/base_service.py +17 -1
  56. isa_model/inference/services/embedding/__init__.py +13 -0
  57. isa_model/inference/services/embedding/base_embed_service.py +111 -8
  58. isa_model/inference/services/embedding/isa_embed_service.py +305 -0
  59. isa_model/inference/services/embedding/openai_embed_service.py +2 -4
  60. isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
  61. isa_model/inference/services/img/__init__.py +2 -2
  62. isa_model/inference/services/img/base_image_gen_service.py +24 -7
  63. isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
  64. isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
  65. isa_model/inference/services/img/services/replicate_flux.py +226 -0
  66. isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
  67. isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
  68. isa_model/inference/services/img/tests/test_img_client.py +297 -0
  69. isa_model/inference/services/llm/base_llm_service.py +30 -6
  70. isa_model/inference/services/llm/helpers/llm_adapter.py +63 -9
  71. isa_model/inference/services/llm/ollama_llm_service.py +2 -1
  72. isa_model/inference/services/llm/openai_llm_service.py +652 -55
  73. isa_model/inference/services/llm/yyds_llm_service.py +2 -1
  74. isa_model/inference/services/vision/__init__.py +5 -5
  75. isa_model/inference/services/vision/base_vision_service.py +118 -185
  76. isa_model/inference/services/vision/helpers/image_utils.py +11 -5
  77. isa_model/inference/services/vision/isa_vision_service.py +573 -0
  78. isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
  79. isa_model/serving/api/fastapi_server.py +88 -16
  80. isa_model/serving/api/middleware/auth.py +311 -0
  81. isa_model/serving/api/middleware/security.py +278 -0
  82. isa_model/serving/api/routes/analytics.py +486 -0
  83. isa_model/serving/api/routes/deployments.py +339 -0
  84. isa_model/serving/api/routes/evaluations.py +579 -0
  85. isa_model/serving/api/routes/logs.py +430 -0
  86. isa_model/serving/api/routes/settings.py +582 -0
  87. isa_model/serving/api/routes/unified.py +324 -165
  88. isa_model/serving/api/startup.py +304 -0
  89. isa_model/serving/modal_proxy_server.py +249 -0
  90. isa_model/training/__init__.py +100 -6
  91. isa_model/training/core/__init__.py +4 -1
  92. isa_model/training/examples/intelligent_training_example.py +281 -0
  93. isa_model/training/intelligent/__init__.py +25 -0
  94. isa_model/training/intelligent/decision_engine.py +643 -0
  95. isa_model/training/intelligent/intelligent_factory.py +888 -0
  96. isa_model/training/intelligent/knowledge_base.py +751 -0
  97. isa_model/training/intelligent/resource_optimizer.py +839 -0
  98. isa_model/training/intelligent/task_classifier.py +576 -0
  99. isa_model/training/storage/__init__.py +24 -0
  100. isa_model/training/storage/core_integration.py +439 -0
  101. isa_model/training/storage/training_repository.py +552 -0
  102. isa_model/training/storage/training_storage.py +628 -0
  103. {isa_model-0.3.9.dist-info → isa_model-0.4.0.dist-info}/METADATA +13 -1
  104. isa_model-0.4.0.dist-info/RECORD +182 -0
  105. isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
  106. isa_model/deployment/cloud/modal/register_models.py +0 -321
  107. isa_model/inference/adapter/unified_api.py +0 -248
  108. isa_model/inference/services/helpers/stacked_config.py +0 -148
  109. isa_model/inference/services/img/flux_professional_service.py +0 -603
  110. isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
  111. isa_model/inference/services/others/table_transformer_service.py +0 -61
  112. isa_model/inference/services/vision/doc_analysis_service.py +0 -640
  113. isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
  114. isa_model/inference/services/vision/ui_analysis_service.py +0 -823
  115. isa_model/scripts/inference_tracker.py +0 -283
  116. isa_model/scripts/mlflow_manager.py +0 -379
  117. isa_model/scripts/model_registry.py +0 -465
  118. isa_model/scripts/register_models.py +0 -370
  119. isa_model/scripts/register_models_with_embeddings.py +0 -510
  120. isa_model/scripts/start_mlflow.py +0 -95
  121. isa_model/scripts/training_tracker.py +0 -257
  122. isa_model-0.3.9.dist-info/RECORD +0 -138
  123. {isa_model-0.3.9.dist-info → isa_model-0.4.0.dist-info}/WHEEL +0 -0
  124. {isa_model-0.3.9.dist-info → isa_model-0.4.0.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,7 @@ from huggingface_hub.errors import HfHubHTTPError
7
7
  from .model_storage import ModelStorage, LocalModelStorage
8
8
  from .model_repo import ModelRegistry, ModelType, ModelCapability
9
9
  from .model_billing_tracker import ModelBillingTracker, ModelOperationType
10
- from ..pricing_manager import PricingManager
10
+ from .model_statistics_tracker import ModelStatisticsTracker
11
11
  from ..config import ConfigManager
12
12
 
13
13
  logger = logging.getLogger(__name__)
@@ -28,38 +28,56 @@ class ModelManager:
28
28
  storage: Optional[ModelStorage] = None,
29
29
  registry: Optional[ModelRegistry] = None,
30
30
  billing_tracker: Optional[ModelBillingTracker] = None,
31
- pricing_manager: Optional[PricingManager] = None,
31
+ statistics_tracker: Optional[ModelStatisticsTracker] = None,
32
32
  config_manager: Optional[ConfigManager] = None):
33
33
  self.storage = storage or LocalModelStorage()
34
34
  self.registry = registry or ModelRegistry()
35
35
  self.billing_tracker = billing_tracker or ModelBillingTracker(model_registry=self.registry)
36
- self.pricing_manager = pricing_manager or PricingManager()
36
+ self.statistics_tracker = statistics_tracker or ModelStatisticsTracker(model_registry=self.registry)
37
37
  self.config_manager = config_manager or ConfigManager()
38
38
 
39
39
  def get_model_pricing(self, provider: str, model_name: str) -> Dict[str, float]:
40
40
  """获取模型定价信息"""
41
- pricing = self.pricing_manager.get_model_pricing(provider, model_name)
42
- if pricing:
43
- return {"input": pricing.input_cost, "output": pricing.output_cost}
41
+ try:
42
+ models = self.config_manager.get_models_by_provider(provider)
43
+ for model in models:
44
+ if model.get("model_id") == model_name:
45
+ metadata = model.get("metadata", {})
46
+ if "cost_per_1000_tokens" in metadata:
47
+ return {"input": metadata["cost_per_1000_tokens"], "output": metadata["cost_per_1000_tokens"]}
48
+ elif "cost_per_minute" in metadata:
49
+ return {"input": metadata["cost_per_minute"], "output": 0.0}
50
+ elif "cost_per_1000_chars" in metadata:
51
+ return {"input": metadata["cost_per_1000_chars"], "output": 0.0}
52
+ except Exception as e:
53
+ logger.warning(f"Failed to get pricing for {provider}/{model_name}: {e}")
44
54
  return {"input": 0.0, "output": 0.0}
45
55
 
46
56
  def calculate_cost(self, provider: str, model_name: str, input_tokens: int, output_tokens: int) -> float:
47
57
  """计算请求成本"""
48
- return self.pricing_manager.calculate_cost(
49
- provider=provider,
50
- model_name=model_name,
51
- input_units=input_tokens,
52
- output_units=output_tokens
53
- )
58
+ pricing = self.get_model_pricing(provider, model_name)
59
+ if pricing["input"] > 0:
60
+ return (input_tokens + output_tokens) * pricing["input"] / 1000.0
61
+ return 0.0
54
62
 
55
63
  def get_cheapest_model(self, provider: str, model_type: str = "llm") -> Optional[str]:
56
64
  """获取最便宜的模型"""
57
- result = self.pricing_manager.get_cheapest_model(
58
- provider=provider,
59
- unit_type="token",
60
- min_input_units=1000 # Assume 1K tokens for comparison
61
- )
62
- return result["model_name"] if result else None
65
+ try:
66
+ models = self.config_manager.get_models_by_provider(provider)
67
+ cheapest_model = None
68
+ lowest_cost = float('inf')
69
+
70
+ for model in models:
71
+ if model.get("model_type") == model_type:
72
+ pricing = self.get_model_pricing(provider, model["model_id"])
73
+ if pricing["input"] > 0 and pricing["input"] < lowest_cost:
74
+ lowest_cost = pricing["input"]
75
+ cheapest_model = model["model_id"]
76
+
77
+ return cheapest_model
78
+ except Exception as e:
79
+ logger.warning(f"Failed to find cheapest model for {provider}: {e}")
80
+ return None
63
81
 
64
82
  async def get_model(self,
65
83
  model_id: str,
@@ -13,7 +13,7 @@ from enum import Enum
13
13
  from datetime import datetime
14
14
 
15
15
  try:
16
- from supabase import create_client, Client
16
+ from ..database.supabase_client import get_supabase_client, get_supabase_table
17
17
  SUPABASE_AVAILABLE = True
18
18
  except ImportError:
19
19
  SUPABASE_AVAILABLE = False
@@ -32,6 +32,10 @@ class ModelCapability(str, Enum):
32
32
  IMAGE_GENERATION = "image_generation"
33
33
  IMAGE_ANALYSIS = "image_analysis"
34
34
  AUDIO_TRANSCRIPTION = "audio_transcription"
35
+ AUDIO_REALTIME = "audio_realtime"
36
+ SPEECH_TO_TEXT = "speech_to_text"
37
+ TEXT_TO_SPEECH = "text_to_speech"
38
+ CONVERSATION = "conversation"
35
39
  IMAGE_UNDERSTANDING = "image_understanding"
36
40
  UI_DETECTION = "ui_detection"
37
41
  OCR = "ocr"
@@ -55,62 +59,64 @@ class ModelRegistry:
55
59
  if not SUPABASE_AVAILABLE:
56
60
  raise ImportError("supabase-py is required. Install with: pip install supabase")
57
61
 
58
- # Get configuration from unified ConfigManager
59
- self.config_manager = ConfigManager()
60
- global_config = self.config_manager.get_global_config()
61
-
62
- # Get Supabase configuration from database config
63
- self.supabase_url = global_config.database.supabase_url or os.getenv("SUPABASE_URL")
64
- self.supabase_key = global_config.database.supabase_key or os.getenv("SUPABASE_ANON_KEY") or os.getenv("SERVICE_ROLE_KEY")
65
-
66
- if not self.supabase_url or not self.supabase_key:
67
- raise ValueError("SUPABASE_URL and SUPABASE_ANON_KEY (or SERVICE_ROLE_KEY) must be configured")
68
-
69
- # Initialize Supabase client
70
- self.supabase: Client = create_client(self.supabase_url, self.supabase_key)
62
+ # Get centralized Supabase client
63
+ self.supabase_client = get_supabase_client()
64
+ self.schema = self.supabase_client.get_schema()
65
+ self.environment = self.supabase_client.get_environment()
71
66
 
72
67
  # Verify connection
73
68
  self._ensure_tables()
74
69
 
75
- logger.info("Model registry initialized with Supabase backend")
70
+ logger.info(f"Model registry initialized with centralized Supabase client (env: {self.environment}, schema: {self.schema})")
71
+
72
+ def _table(self, table_name: str):
73
+ """Get table with correct schema"""
74
+ return self.supabase_client.table(table_name)
76
75
 
77
76
  def _ensure_tables(self):
78
77
  """Ensure required tables exist in Supabase"""
79
78
  try:
80
79
  # Check if models table exists by trying to query it
81
- result = self.supabase.table('models').select('model_id').limit(1).execute()
82
- logger.debug("Models table verified")
80
+ result = self._table('models').select('model_id').limit(1).execute()
81
+ logger.debug(f"Models table verified in {self.schema} schema")
83
82
  except Exception as e:
84
- logger.warning(f"Models table might not exist: {e}")
83
+ logger.warning(f"Models table might not exist in {self.schema} schema: {e}")
85
84
  # In production, tables should be created via Supabase migrations
86
85
 
87
86
  def register_model(self,
88
87
  model_id: str,
89
88
  model_type: ModelType,
90
89
  capabilities: List[ModelCapability],
91
- metadata: Dict[str, Any]) -> bool:
90
+ metadata: Dict[str, Any],
91
+ provider: Optional[str] = None) -> bool:
92
92
  """Register a model with its capabilities and metadata"""
93
93
  try:
94
94
  current_time = datetime.now().isoformat()
95
95
 
96
+ # Extract provider from metadata or use parameter
97
+ provider_value = provider or metadata.get('provider')
98
+ if not provider_value:
99
+ raise ValueError("Provider must be specified either as parameter or in metadata")
100
+
96
101
  # Prepare model data
97
102
  model_data = {
98
103
  'model_id': model_id,
99
104
  'model_type': model_type.value,
105
+ 'provider': provider_value,
100
106
  'metadata': json.dumps(metadata),
101
107
  'created_at': current_time,
102
108
  'updated_at': current_time
103
109
  }
104
110
 
105
111
  # Insert or update model
106
- result = self.supabase.table('models').upsert(model_data).execute()
112
+ result = self._table('models').upsert(model_data).execute()
107
113
 
108
114
  if not result.data:
109
115
  logger.error(f"Failed to insert model {model_id}")
110
116
  return False
111
117
 
112
118
  # Delete existing capabilities
113
- self.supabase.table('model_capabilities').delete().eq('model_id', model_id).execute()
119
+ self._table('model_capabilities').delete().eq('model_id', model_id).execute()
114
120
 
115
121
  # Insert new capabilities
116
122
  if capabilities:
@@ -123,7 +129,7 @@ class ModelRegistry:
123
129
  for capability in capabilities
124
130
  ]
125
131
 
126
- cap_result = self.supabase.table('model_capabilities').insert(capability_data).execute()
132
+ cap_result = self._table('model_capabilities').insert(capability_data).execute()
127
133
 
128
134
  if not cap_result.data:
129
135
  logger.error(f"Failed to insert capabilities for {model_id}")
@@ -140,7 +146,7 @@ class ModelRegistry:
140
146
  """Unregister a model"""
141
147
  try:
142
148
  # Delete model (capabilities will be cascade deleted)
143
- result = self.supabase.table('models').delete().eq('model_id', model_id).execute()
149
+ result = self._table('models').delete().eq('model_id', model_id).execute()
144
150
 
145
151
  if result.data:
146
152
  logger.info(f"Unregistered model {model_id}")
@@ -155,7 +161,7 @@ class ModelRegistry:
155
161
  """Get model information"""
156
162
  try:
157
163
  # Get model info
158
- model_result = self.supabase.table('models').select('*').eq('model_id', model_id).execute()
164
+ model_result = self._table('models').select('*').eq('model_id', model_id).execute()
159
165
 
160
166
  if not model_result.data:
161
167
  return None
@@ -163,7 +169,7 @@ class ModelRegistry:
163
169
  model_row = model_result.data[0]
164
170
 
165
171
  # Get capabilities
166
- cap_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
172
+ cap_result = self._table('model_capabilities').select('capability').eq('model_id', model_id).execute()
167
173
  capabilities = [cap['capability'] for cap in cap_result.data]
168
174
 
169
175
  model_info = {
@@ -184,14 +190,14 @@ class ModelRegistry:
184
190
  def get_models_by_type(self, model_type: ModelType) -> Dict[str, Dict[str, Any]]:
185
191
  """Get all models of a specific type"""
186
192
  try:
187
- models_result = self.supabase.table('models').select('*').eq('model_type', model_type.value).execute()
193
+ models_result = self._table('models').select('*').eq('model_type', model_type.value).execute()
188
194
 
189
195
  result = {}
190
196
  for model in models_result.data:
191
197
  model_id = model["model_id"]
192
198
 
193
199
  # Get capabilities for this model
194
- cap_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
200
+ cap_result = self._table('model_capabilities').select('capability').eq('model_id', model_id).execute()
195
201
  capabilities = [cap['capability'] for cap in cap_result.data]
196
202
 
197
203
  result[model_id] = {
@@ -212,21 +218,21 @@ class ModelRegistry:
212
218
  """Get all models with a specific capability"""
213
219
  try:
214
220
  # Get model IDs with specific capability
215
- cap_result = self.supabase.table('model_capabilities').select('model_id').eq('capability', capability.value).execute()
221
+ cap_result = self._table('model_capabilities').select('model_id').eq('capability', capability.value).execute()
216
222
  model_ids = [row['model_id'] for row in cap_result.data]
217
223
 
218
224
  if not model_ids:
219
225
  return {}
220
226
 
221
227
  # Get model details
222
- models_result = self.supabase.table('models').select('*').in_('model_id', model_ids).execute()
228
+ models_result = self._table('models').select('*').in_('model_id', model_ids).execute()
223
229
 
224
230
  result = {}
225
231
  for model in models_result.data:
226
232
  model_id = model["model_id"]
227
233
 
228
234
  # Get all capabilities for this model
229
- all_caps_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
235
+ all_caps_result = self._table('model_capabilities').select('capability').eq('model_id', model_id).execute()
230
236
  capabilities = [cap['capability'] for cap in all_caps_result.data]
231
237
 
232
238
  result[model_id] = {
@@ -246,7 +252,7 @@ class ModelRegistry:
246
252
  def has_capability(self, model_id: str, capability: ModelCapability) -> bool:
247
253
  """Check if a model has a specific capability"""
248
254
  try:
249
- result = self.supabase.table('model_capabilities').select('model_id').eq('model_id', model_id).eq('capability', capability.value).execute()
255
+ result = self._table('model_capabilities').select('model_id').eq('model_id', model_id).eq('capability', capability.value).execute()
250
256
 
251
257
  return len(result.data) > 0
252
258
 
@@ -257,14 +263,14 @@ class ModelRegistry:
257
263
  def list_models(self) -> Dict[str, Dict[str, Any]]:
258
264
  """List all registered models"""
259
265
  try:
260
- models_result = self.supabase.table('models').select('*').order('created_at', desc=True).execute()
266
+ models_result = self._table('models').select('*').order('created_at', desc=True).execute()
261
267
 
262
268
  result = {}
263
269
  for model in models_result.data:
264
270
  model_id = model["model_id"]
265
271
 
266
272
  # Get capabilities for this model
267
- cap_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
273
+ cap_result = self._table('model_capabilities').select('capability').eq('model_id', model_id).execute()
268
274
  capabilities = [cap['capability'] for cap in cap_result.data]
269
275
 
270
276
  result[model_id] = {
@@ -285,18 +291,18 @@ class ModelRegistry:
285
291
  """Get registry statistics"""
286
292
  try:
287
293
  # Count total models
288
- total_result = self.supabase.table('models').select('model_id', count='exact').execute()
294
+ total_result = self._table('models').select('model_id', count='exact').execute()
289
295
  total_models = total_result.count if total_result.count is not None else 0
290
296
 
291
297
  # Count by type (manual aggregation since RPC might not exist)
292
- models_result = self.supabase.table('models').select('model_type').execute()
298
+ models_result = self._table('models').select('model_type').execute()
293
299
  type_counts = {}
294
300
  for model in models_result.data:
295
301
  model_type = model['model_type']
296
302
  type_counts[model_type] = type_counts.get(model_type, 0) + 1
297
303
 
298
304
  # Count by capability
299
- caps_result = self.supabase.table('model_capabilities').select('capability').execute()
305
+ caps_result = self._table('model_capabilities').select('capability').execute()
300
306
  capability_counts = {}
301
307
  for cap in caps_result.data:
302
308
  capability = cap['capability']
@@ -316,7 +322,7 @@ class ModelRegistry:
316
322
  """Search models by name or metadata"""
317
323
  try:
318
324
  # Search in model_id and metadata
319
- models_result = self.supabase.table('models').select('*').or_(
325
+ models_result = self._table('models').select('*').or_(
320
326
  f'model_id.ilike.%{query}%,metadata.ilike.%{query}%'
321
327
  ).order('created_at', desc=True).execute()
322
328
 
@@ -325,7 +331,7 @@ class ModelRegistry:
325
331
  model_id = model["model_id"]
326
332
 
327
333
  # Get capabilities for this model
328
- cap_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
334
+ cap_result = self._table('model_capabilities').select('capability').eq('model_id', model_id).execute()
329
335
  capabilities = [cap['capability'] for cap in cap_result.data]
330
336
 
331
337
  result[model_id] = {
@@ -0,0 +1,234 @@
1
+ """
2
+ Model Statistics Tracker - Aggregated Usage Tracking
3
+
4
+ Replaces the detailed ModelBillingTracker with efficient daily aggregation.
5
+ Instead of storing every individual request, we aggregate usage by model per day.
6
+ """
7
+
8
+ import logging
9
+ from datetime import datetime, timezone, date
10
+ from typing import Dict, Any, Optional, List
11
+ from decimal import Decimal
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class ModelStatisticsTracker:
16
+ """Tracks aggregated model usage statistics efficiently"""
17
+
18
+ def __init__(self, model_registry=None):
19
+ """Initialize the statistics tracker"""
20
+ self.model_registry = model_registry
21
+ self._daily_cache = {} # Cache for today's statistics
22
+
23
+ def track_usage(self,
24
+ model_id: str,
25
+ provider: str,
26
+ service_type: str,
27
+ operation_type: str = "inference",
28
+ operation: str = "",
29
+ input_tokens: Optional[int] = None,
30
+ output_tokens: Optional[int] = None,
31
+ total_tokens: Optional[int] = None,
32
+ input_units: Optional[float] = None,
33
+ output_units: Optional[float] = None,
34
+ cost_usd: float = 0.0,
35
+ metadata: Optional[Dict[str, Any]] = None) -> bool:
36
+ """
37
+ Track model usage by updating daily aggregated statistics
38
+
39
+ Args:
40
+ model_id: Model identifier
41
+ provider: Provider name (openai, anthropic, etc.)
42
+ service_type: Type of service (llm, vision, embedding, etc.)
43
+ operation_type: Type of operation (usually "inference")
44
+ operation: Specific operation name
45
+ input_tokens: Number of input tokens
46
+ output_tokens: Number of output tokens
47
+ total_tokens: Total tokens (input + output)
48
+ input_units: Input units for non-token services
49
+ output_units: Output units for non-token services
50
+ cost_usd: Cost in USD
51
+ metadata: Additional metadata
52
+
53
+ Returns:
54
+ bool: True if tracking succeeded
55
+ """
56
+ try:
57
+ today = date.today()
58
+ cache_key = f"{model_id}_{provider}_{service_type}_{operation_type}_{today}"
59
+
60
+ # Get current stats from cache or database
61
+ if cache_key not in self._daily_cache:
62
+ self._daily_cache[cache_key] = self._get_daily_stats(
63
+ model_id, provider, service_type, operation_type, today
64
+ )
65
+
66
+ # Update statistics
67
+ stats = self._daily_cache[cache_key]
68
+ stats['total_requests'] += 1
69
+ stats['total_input_tokens'] += input_tokens or 0
70
+ stats['total_output_tokens'] += output_tokens or 0
71
+ stats['total_tokens'] += total_tokens or (input_tokens or 0) + (output_tokens or 0)
72
+ stats['total_input_units'] += Decimal(str(input_units or 0))
73
+ stats['total_output_units'] += Decimal(str(output_units or 0))
74
+ stats['total_cost_usd'] += Decimal(str(cost_usd))
75
+ stats['last_updated'] = datetime.now(timezone.utc)
76
+
77
+ # Save to database immediately (upsert)
78
+ self._save_daily_stats(stats)
79
+
80
+ logger.debug(f"Tracked usage: {model_id} - {operation_type} - ${cost_usd:.6f}")
81
+ return True
82
+
83
+ except Exception as e:
84
+ logger.error(f"Failed to track model usage: {e}")
85
+ return False
86
+
87
+ def _get_daily_stats(self, model_id: str, provider: str, service_type: str,
88
+ operation_type: str, target_date: date) -> Dict[str, Any]:
89
+ """Get or create daily statistics record"""
90
+ try:
91
+ if not self.model_registry or not hasattr(self.model_registry, 'supabase_client'):
92
+ # Return empty stats if no database connection
93
+ return self._create_empty_stats(model_id, provider, service_type, operation_type, target_date)
94
+
95
+ result = self.model_registry.supabase_client.table('model_statistics').select('*').eq(
96
+ 'model_id', model_id
97
+ ).eq('provider', provider).eq('service_type', service_type).eq(
98
+ 'operation_type', operation_type
99
+ ).eq('date', target_date.isoformat()).execute()
100
+
101
+ if result.data and len(result.data) > 0:
102
+ # Convert to proper types
103
+ stats = result.data[0].copy()
104
+ stats['total_input_units'] = Decimal(str(stats.get('total_input_units', 0)))
105
+ stats['total_output_units'] = Decimal(str(stats.get('total_output_units', 0)))
106
+ stats['total_cost_usd'] = Decimal(str(stats.get('total_cost_usd', 0)))
107
+ return stats
108
+ else:
109
+ # Create new record
110
+ return self._create_empty_stats(model_id, provider, service_type, operation_type, target_date)
111
+
112
+ except Exception as e:
113
+ logger.error(f"Failed to get daily stats: {e}")
114
+ return self._create_empty_stats(model_id, provider, service_type, operation_type, target_date)
115
+
116
+ def _create_empty_stats(self, model_id: str, provider: str, service_type: str,
117
+ operation_type: str, target_date: date) -> Dict[str, Any]:
118
+ """Create empty statistics record"""
119
+ return {
120
+ 'model_id': model_id,
121
+ 'provider': provider,
122
+ 'service_type': service_type,
123
+ 'operation_type': operation_type,
124
+ 'date': target_date.isoformat(),
125
+ 'total_requests': 0,
126
+ 'total_input_tokens': 0,
127
+ 'total_output_tokens': 0,
128
+ 'total_tokens': 0,
129
+ 'total_input_units': Decimal('0'),
130
+ 'total_output_units': Decimal('0'),
131
+ 'total_cost_usd': Decimal('0'),
132
+ 'last_updated': datetime.now(timezone.utc),
133
+ 'created_at': datetime.now(timezone.utc)
134
+ }
135
+
136
+ def _save_daily_stats(self, stats: Dict[str, Any]) -> bool:
137
+ """Save daily statistics to database"""
138
+ try:
139
+ if not self.model_registry or not hasattr(self.model_registry, 'supabase_client'):
140
+ logger.warning("No Supabase client available for statistics saving")
141
+ return False
142
+
143
+ # Convert Decimal to float for JSON serialization
144
+ save_data = stats.copy()
145
+ save_data['total_input_units'] = float(stats['total_input_units'])
146
+ save_data['total_output_units'] = float(stats['total_output_units'])
147
+ save_data['total_cost_usd'] = float(stats['total_cost_usd'])
148
+ # Handle datetime conversion for last_updated
149
+ if isinstance(stats['last_updated'], str):
150
+ save_data['last_updated'] = stats['last_updated']
151
+ else:
152
+ save_data['last_updated'] = stats['last_updated'].isoformat()
153
+
154
+ # Handle datetime conversion for created_at
155
+ created_at = stats.get('created_at', datetime.now(timezone.utc))
156
+ if isinstance(created_at, str):
157
+ save_data['created_at'] = created_at
158
+ else:
159
+ save_data['created_at'] = created_at.isoformat()
160
+
161
+ # Upsert to handle duplicates
162
+ result = self.model_registry.supabase_client.table('model_statistics').upsert(
163
+ save_data,
164
+ on_conflict='model_id,provider,service_type,operation_type,date'
165
+ ).execute()
166
+
167
+ if result.data:
168
+ logger.debug(f"Updated statistics for {stats['model_id']} on {stats['date']}")
169
+ return True
170
+ else:
171
+ logger.warning("Failed to save statistics to database")
172
+ return False
173
+
174
+ except Exception as e:
175
+ logger.error(f"Failed to save daily statistics: {e}")
176
+ return False
177
+
178
+ def get_model_summary(self, model_id: str = None, days: int = 30) -> List[Dict[str, Any]]:
179
+ """Get usage summary for a model or all models"""
180
+ try:
181
+ if not self.model_registry or not hasattr(self.model_registry, 'supabase_client'):
182
+ return []
183
+
184
+ query = self.model_registry.supabase_client.table('model_statistics').select('*')
185
+
186
+ if model_id:
187
+ query = query.eq('model_id', model_id)
188
+
189
+ if days > 0:
190
+ from datetime import timedelta
191
+ start_date = (date.today() - timedelta(days=days)).isoformat()
192
+ query = query.gte('date', start_date)
193
+
194
+ result = query.order('date', desc=True).execute()
195
+ return result.data or []
196
+
197
+ except Exception as e:
198
+ logger.error(f"Failed to get model summary: {e}")
199
+ return []
200
+
201
+ def get_daily_totals(self, target_date: date = None) -> Dict[str, Any]:
202
+ """Get total usage for a specific date"""
203
+ if target_date is None:
204
+ target_date = date.today()
205
+
206
+ try:
207
+ if not self.model_registry or not hasattr(self.model_registry, 'supabase_client'):
208
+ return {}
209
+
210
+ result = self.model_registry.supabase_client.table('model_statistics').select(
211
+ 'total_requests,total_input_tokens,total_output_tokens,total_cost_usd'
212
+ ).eq('date', target_date.isoformat()).execute()
213
+
214
+ if not result.data:
215
+ return {'total_requests': 0, 'total_cost': 0.0}
216
+
217
+ totals = {
218
+ 'total_requests': sum(row.get('total_requests', 0) for row in result.data),
219
+ 'total_input_tokens': sum(row.get('total_input_tokens', 0) for row in result.data),
220
+ 'total_output_tokens': sum(row.get('total_output_tokens', 0) for row in result.data),
221
+ 'total_cost': sum(float(row.get('total_cost_usd', 0)) for row in result.data),
222
+ 'date': target_date.isoformat()
223
+ }
224
+
225
+ return totals
226
+
227
+ except Exception as e:
228
+ logger.error(f"Failed to get daily totals: {e}")
229
+ return {'total_requests': 0, 'total_cost': 0.0}
230
+
231
+ def clear_cache(self):
232
+ """Clear the daily cache"""
233
+ self._daily_cache.clear()
234
+ logger.debug("Statistics cache cleared")
@@ -57,7 +57,6 @@ class LocalModelStorage(ModelStorage):
57
57
  self.metadata = json.load(f)
58
58
  else:
59
59
  self.metadata = {}
60
- self._save_metadata()
61
60
 
62
61
  def _save_metadata(self):
63
62
  """Save model metadata to file"""