isa-model 0.3.91__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.
- isa_model/client.py +732 -573
- isa_model/core/cache/redis_cache.py +401 -0
- isa_model/core/config/config_manager.py +53 -10
- isa_model/core/config.py +1 -1
- isa_model/core/database/__init__.py +1 -0
- isa_model/core/database/migrations.py +277 -0
- isa_model/core/database/supabase_client.py +123 -0
- isa_model/core/models/__init__.py +37 -0
- isa_model/core/models/model_billing_tracker.py +60 -88
- isa_model/core/models/model_manager.py +36 -18
- isa_model/core/models/model_repo.py +44 -38
- isa_model/core/models/model_statistics_tracker.py +234 -0
- isa_model/core/models/model_storage.py +0 -1
- isa_model/core/models/model_version_manager.py +959 -0
- isa_model/core/pricing_manager.py +2 -249
- isa_model/core/resilience/circuit_breaker.py +366 -0
- isa_model/core/security/secrets.py +358 -0
- isa_model/core/services/__init__.py +2 -4
- isa_model/core/services/intelligent_model_selector.py +101 -370
- isa_model/core/storage/hf_storage.py +1 -1
- isa_model/core/types.py +7 -0
- isa_model/deployment/cloud/modal/isa_audio_chatTTS_service.py +520 -0
- isa_model/deployment/cloud/modal/isa_audio_fish_service.py +0 -0
- isa_model/deployment/cloud/modal/isa_audio_openvoice_service.py +758 -0
- isa_model/deployment/cloud/modal/isa_audio_service_v2.py +1044 -0
- isa_model/deployment/cloud/modal/isa_embed_rerank_service.py +296 -0
- isa_model/deployment/cloud/modal/isa_video_hunyuan_service.py +423 -0
- isa_model/deployment/cloud/modal/isa_vision_ocr_service.py +519 -0
- isa_model/deployment/cloud/modal/isa_vision_qwen25_service.py +709 -0
- isa_model/deployment/cloud/modal/isa_vision_table_service.py +467 -323
- isa_model/deployment/cloud/modal/isa_vision_ui_service.py +607 -180
- isa_model/deployment/cloud/modal/isa_vision_ui_service_optimized.py +660 -0
- isa_model/deployment/core/deployment_manager.py +6 -4
- isa_model/deployment/services/auto_hf_modal_deployer.py +894 -0
- isa_model/eval/benchmarks/__init__.py +27 -0
- isa_model/eval/benchmarks/multimodal_datasets.py +460 -0
- isa_model/eval/benchmarks.py +244 -12
- isa_model/eval/evaluators/__init__.py +8 -2
- isa_model/eval/evaluators/audio_evaluator.py +727 -0
- isa_model/eval/evaluators/embedding_evaluator.py +742 -0
- isa_model/eval/evaluators/vision_evaluator.py +564 -0
- isa_model/eval/example_evaluation.py +395 -0
- isa_model/eval/factory.py +272 -5
- isa_model/eval/isa_benchmarks.py +700 -0
- isa_model/eval/isa_integration.py +582 -0
- isa_model/eval/metrics.py +159 -6
- isa_model/eval/tests/unit/test_basic.py +396 -0
- isa_model/inference/ai_factory.py +44 -8
- isa_model/inference/services/audio/__init__.py +21 -0
- isa_model/inference/services/audio/base_realtime_service.py +225 -0
- isa_model/inference/services/audio/isa_tts_service.py +0 -0
- isa_model/inference/services/audio/openai_realtime_service.py +320 -124
- isa_model/inference/services/audio/openai_stt_service.py +32 -6
- isa_model/inference/services/base_service.py +17 -1
- isa_model/inference/services/embedding/__init__.py +13 -0
- isa_model/inference/services/embedding/base_embed_service.py +111 -8
- isa_model/inference/services/embedding/isa_embed_service.py +305 -0
- isa_model/inference/services/embedding/openai_embed_service.py +2 -4
- isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
- isa_model/inference/services/img/__init__.py +2 -2
- isa_model/inference/services/img/base_image_gen_service.py +24 -7
- isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
- isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
- isa_model/inference/services/img/services/replicate_flux.py +226 -0
- isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
- isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
- isa_model/inference/services/img/tests/test_img_client.py +297 -0
- isa_model/inference/services/llm/base_llm_service.py +30 -6
- isa_model/inference/services/llm/helpers/llm_adapter.py +63 -9
- isa_model/inference/services/llm/ollama_llm_service.py +2 -1
- isa_model/inference/services/llm/openai_llm_service.py +652 -55
- isa_model/inference/services/llm/yyds_llm_service.py +2 -1
- isa_model/inference/services/vision/__init__.py +5 -5
- isa_model/inference/services/vision/base_vision_service.py +118 -185
- isa_model/inference/services/vision/helpers/image_utils.py +11 -5
- isa_model/inference/services/vision/isa_vision_service.py +573 -0
- isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
- isa_model/serving/api/fastapi_server.py +88 -16
- isa_model/serving/api/middleware/auth.py +311 -0
- isa_model/serving/api/middleware/security.py +278 -0
- isa_model/serving/api/routes/analytics.py +486 -0
- isa_model/serving/api/routes/deployments.py +339 -0
- isa_model/serving/api/routes/evaluations.py +579 -0
- isa_model/serving/api/routes/logs.py +430 -0
- isa_model/serving/api/routes/settings.py +582 -0
- isa_model/serving/api/routes/unified.py +324 -165
- isa_model/serving/api/startup.py +304 -0
- isa_model/serving/modal_proxy_server.py +249 -0
- isa_model/training/__init__.py +100 -6
- isa_model/training/core/__init__.py +4 -1
- isa_model/training/examples/intelligent_training_example.py +281 -0
- isa_model/training/intelligent/__init__.py +25 -0
- isa_model/training/intelligent/decision_engine.py +643 -0
- isa_model/training/intelligent/intelligent_factory.py +888 -0
- isa_model/training/intelligent/knowledge_base.py +751 -0
- isa_model/training/intelligent/resource_optimizer.py +839 -0
- isa_model/training/intelligent/task_classifier.py +576 -0
- isa_model/training/storage/__init__.py +24 -0
- isa_model/training/storage/core_integration.py +439 -0
- isa_model/training/storage/training_repository.py +552 -0
- isa_model/training/storage/training_storage.py +628 -0
- {isa_model-0.3.91.dist-info → isa_model-0.4.0.dist-info}/METADATA +13 -1
- isa_model-0.4.0.dist-info/RECORD +182 -0
- isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
- isa_model/deployment/cloud/modal/register_models.py +0 -321
- isa_model/inference/adapter/unified_api.py +0 -248
- isa_model/inference/services/helpers/stacked_config.py +0 -148
- isa_model/inference/services/img/flux_professional_service.py +0 -603
- isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/others/table_transformer_service.py +0 -61
- isa_model/inference/services/vision/doc_analysis_service.py +0 -640
- isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/vision/ui_analysis_service.py +0 -823
- isa_model/scripts/inference_tracker.py +0 -283
- isa_model/scripts/mlflow_manager.py +0 -379
- isa_model/scripts/model_registry.py +0 -465
- isa_model/scripts/register_models.py +0 -370
- isa_model/scripts/register_models_with_embeddings.py +0 -510
- isa_model/scripts/start_mlflow.py +0 -95
- isa_model/scripts/training_tracker.py +0 -257
- isa_model-0.3.91.dist-info/RECORD +0 -138
- {isa_model-0.3.91.dist-info → isa_model-0.4.0.dist-info}/WHEEL +0 -0
- {isa_model-0.3.91.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
|
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
|
-
|
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.
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
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
|
59
|
-
self.
|
60
|
-
|
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
|
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.
|
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]
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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")
|