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
@@ -1,116 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Initialize Vision Models in Model Registry
|
3
|
-
|
4
|
-
Simple function to register vision models for UI analysis in Supabase
|
5
|
-
"""
|
6
|
-
|
7
|
-
from .model_registry import ModelRegistry, ModelType, ModelCapability
|
8
|
-
|
9
|
-
def register_vision_models():
|
10
|
-
"""Register vision models for UI analysis pipeline"""
|
11
|
-
|
12
|
-
registry = ModelRegistry() # Auto-detects Supabase from .env
|
13
|
-
|
14
|
-
# Vision models for UI analysis
|
15
|
-
models = [
|
16
|
-
{
|
17
|
-
"model_id": "omniparser-v2.0",
|
18
|
-
"model_type": ModelType.VISION,
|
19
|
-
"capabilities": [ModelCapability.UI_DETECTION, ModelCapability.IMAGE_ANALYSIS],
|
20
|
-
"metadata": {
|
21
|
-
"repo_id": "microsoft/OmniParser-v2.0",
|
22
|
-
"provider": "microsoft",
|
23
|
-
"version": "2.0",
|
24
|
-
"description": "Advanced UI element detection",
|
25
|
-
"gpu_memory_mb": 8192,
|
26
|
-
"modal_service": "isa-vision"
|
27
|
-
}
|
28
|
-
},
|
29
|
-
{
|
30
|
-
"model_id": "table-transformer-detection",
|
31
|
-
"model_type": ModelType.VISION,
|
32
|
-
"capabilities": [ModelCapability.TABLE_DETECTION, ModelCapability.IMAGE_ANALYSIS],
|
33
|
-
"metadata": {
|
34
|
-
"repo_id": "microsoft/table-transformer-detection",
|
35
|
-
"provider": "microsoft",
|
36
|
-
"version": "1.1",
|
37
|
-
"description": "Table detection in documents",
|
38
|
-
"gpu_memory_mb": 4096,
|
39
|
-
"modal_service": "isa-vision"
|
40
|
-
}
|
41
|
-
},
|
42
|
-
{
|
43
|
-
"model_id": "table-transformer-structure",
|
44
|
-
"model_type": ModelType.VISION,
|
45
|
-
"capabilities": [ModelCapability.TABLE_STRUCTURE_RECOGNITION, ModelCapability.IMAGE_ANALYSIS],
|
46
|
-
"metadata": {
|
47
|
-
"repo_id": "microsoft/table-transformer-structure-recognition-v1.1-all",
|
48
|
-
"provider": "microsoft",
|
49
|
-
"version": "1.1",
|
50
|
-
"description": "Table structure recognition",
|
51
|
-
"gpu_memory_mb": 4096,
|
52
|
-
"modal_service": "isa-vision"
|
53
|
-
}
|
54
|
-
},
|
55
|
-
{
|
56
|
-
"model_id": "paddleocr-v3.0",
|
57
|
-
"model_type": ModelType.VISION,
|
58
|
-
"capabilities": [ModelCapability.OCR, ModelCapability.IMAGE_ANALYSIS],
|
59
|
-
"metadata": {
|
60
|
-
"repo_id": "PaddlePaddle/PaddleOCR",
|
61
|
-
"provider": "paddlepaddle",
|
62
|
-
"version": "3.0",
|
63
|
-
"description": "Multilingual OCR",
|
64
|
-
"gpu_memory_mb": 2048,
|
65
|
-
"modal_service": "isa-vision"
|
66
|
-
}
|
67
|
-
}
|
68
|
-
]
|
69
|
-
|
70
|
-
print("🔧 Registering vision models in Supabase...")
|
71
|
-
|
72
|
-
success_count = 0
|
73
|
-
for model in models:
|
74
|
-
success = registry.register_model(
|
75
|
-
model_id=model["model_id"],
|
76
|
-
model_type=model["model_type"],
|
77
|
-
capabilities=model["capabilities"],
|
78
|
-
metadata=model["metadata"]
|
79
|
-
)
|
80
|
-
|
81
|
-
if success:
|
82
|
-
print(f"✅ {model['model_id']}")
|
83
|
-
success_count += 1
|
84
|
-
else:
|
85
|
-
print(f"❌ {model['model_id']}")
|
86
|
-
|
87
|
-
print(f"\n📊 Registered {success_count}/{len(models)} models")
|
88
|
-
return success_count == len(models)
|
89
|
-
|
90
|
-
def get_model_for_capability(capability: ModelCapability) -> str:
|
91
|
-
"""Get best model for a capability"""
|
92
|
-
|
93
|
-
registry = ModelRegistry()
|
94
|
-
models = registry.get_models_by_capability(capability)
|
95
|
-
|
96
|
-
# Priority mapping
|
97
|
-
priorities = {
|
98
|
-
ModelCapability.UI_DETECTION: ["omniparser-v2.0"],
|
99
|
-
ModelCapability.OCR: ["paddleocr-v3.0"],
|
100
|
-
ModelCapability.TABLE_DETECTION: ["table-transformer-detection"],
|
101
|
-
ModelCapability.TABLE_STRUCTURE_RECOGNITION: ["table-transformer-structure"]
|
102
|
-
}
|
103
|
-
|
104
|
-
preferred = priorities.get(capability, [])
|
105
|
-
for model_id in preferred:
|
106
|
-
if model_id in models:
|
107
|
-
return model_id
|
108
|
-
|
109
|
-
return list(models.keys())[0] if models else None
|
110
|
-
|
111
|
-
if __name__ == "__main__":
|
112
|
-
success = register_vision_models()
|
113
|
-
if success:
|
114
|
-
print("🎉 All vision models registered successfully!")
|
115
|
-
else:
|
116
|
-
print("⚠️ Some models failed to register")
|
@@ -1,406 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python
|
2
|
-
# -*- coding: utf-8 -*-
|
3
|
-
|
4
|
-
"""
|
5
|
-
Billing Tracker for isA_Model Services
|
6
|
-
Tracks usage and costs across all AI providers
|
7
|
-
"""
|
8
|
-
|
9
|
-
from typing import Dict, List, Optional, Any, Union
|
10
|
-
from datetime import datetime, timezone
|
11
|
-
from dataclasses import dataclass, asdict
|
12
|
-
import json
|
13
|
-
import logging
|
14
|
-
from pathlib import Path
|
15
|
-
from enum import Enum
|
16
|
-
import os
|
17
|
-
|
18
|
-
logger = logging.getLogger(__name__)
|
19
|
-
|
20
|
-
class ServiceType(Enum):
|
21
|
-
"""Types of AI services"""
|
22
|
-
LLM = "llm"
|
23
|
-
EMBEDDING = "embedding"
|
24
|
-
VISION = "vision"
|
25
|
-
IMAGE_GENERATION = "image_generation"
|
26
|
-
AUDIO_STT = "audio_stt"
|
27
|
-
AUDIO_TTS = "audio_tts"
|
28
|
-
|
29
|
-
class Provider(Enum):
|
30
|
-
"""AI service providers"""
|
31
|
-
OPENAI = "openai"
|
32
|
-
REPLICATE = "replicate"
|
33
|
-
OLLAMA = "ollama"
|
34
|
-
ANTHROPIC = "anthropic"
|
35
|
-
GOOGLE = "google"
|
36
|
-
|
37
|
-
@dataclass
|
38
|
-
class UsageRecord:
|
39
|
-
"""Record of a single API usage"""
|
40
|
-
timestamp: str
|
41
|
-
provider: str
|
42
|
-
service_type: str
|
43
|
-
model_name: str
|
44
|
-
operation: str
|
45
|
-
input_tokens: Optional[int] = None
|
46
|
-
output_tokens: Optional[int] = None
|
47
|
-
total_tokens: Optional[int] = None
|
48
|
-
input_units: Optional[float] = None # For non-token based services (images, audio)
|
49
|
-
output_units: Optional[float] = None
|
50
|
-
cost_usd: Optional[float] = None
|
51
|
-
metadata: Optional[Dict[str, Any]] = None
|
52
|
-
|
53
|
-
def to_dict(self) -> Dict[str, Any]:
|
54
|
-
"""Convert to dictionary"""
|
55
|
-
return asdict(self)
|
56
|
-
|
57
|
-
@classmethod
|
58
|
-
def from_dict(cls, data: Dict[str, Any]) -> 'UsageRecord':
|
59
|
-
"""Create from dictionary"""
|
60
|
-
return cls(**data)
|
61
|
-
|
62
|
-
class BillingTracker:
|
63
|
-
"""
|
64
|
-
Tracks billing and usage across all AI services
|
65
|
-
"""
|
66
|
-
|
67
|
-
def __init__(self, storage_path: Optional[str] = None):
|
68
|
-
"""
|
69
|
-
Initialize billing tracker
|
70
|
-
|
71
|
-
Args:
|
72
|
-
storage_path: Path to store billing data (defaults to project root)
|
73
|
-
"""
|
74
|
-
if storage_path is None:
|
75
|
-
project_root = Path(__file__).parent.parent.parent
|
76
|
-
self.storage_path = project_root / "billing_data.json"
|
77
|
-
else:
|
78
|
-
self.storage_path = Path(storage_path)
|
79
|
-
self.usage_records: List[UsageRecord] = []
|
80
|
-
self.session_start = datetime.now(timezone.utc).isoformat()
|
81
|
-
|
82
|
-
# Load existing data
|
83
|
-
self._load_data()
|
84
|
-
|
85
|
-
def _load_data(self):
|
86
|
-
"""Load existing billing data"""
|
87
|
-
try:
|
88
|
-
if self.storage_path.exists():
|
89
|
-
with open(self.storage_path, 'r') as f:
|
90
|
-
data = json.load(f)
|
91
|
-
self.usage_records = [
|
92
|
-
UsageRecord.from_dict(record)
|
93
|
-
for record in data.get('usage_records', [])
|
94
|
-
]
|
95
|
-
logger.info(f"Loaded {len(self.usage_records)} billing records")
|
96
|
-
except Exception as e:
|
97
|
-
logger.warning(f"Could not load billing data: {e}")
|
98
|
-
self.usage_records = []
|
99
|
-
|
100
|
-
def _save_data(self):
|
101
|
-
"""Save billing data to storage"""
|
102
|
-
try:
|
103
|
-
# Ensure directory exists
|
104
|
-
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
|
105
|
-
|
106
|
-
data = {
|
107
|
-
"session_start": self.session_start,
|
108
|
-
"last_updated": datetime.now(timezone.utc).isoformat(),
|
109
|
-
"usage_records": [record.to_dict() for record in self.usage_records]
|
110
|
-
}
|
111
|
-
|
112
|
-
with open(self.storage_path, 'w') as f:
|
113
|
-
json.dump(data, f, indent=2)
|
114
|
-
|
115
|
-
except Exception as e:
|
116
|
-
logger.error(f"Could not save billing data: {e}")
|
117
|
-
|
118
|
-
def track_usage(
|
119
|
-
self,
|
120
|
-
provider: Union[str, Provider],
|
121
|
-
service_type: Union[str, ServiceType],
|
122
|
-
model_name: str,
|
123
|
-
operation: str,
|
124
|
-
input_tokens: Optional[int] = None,
|
125
|
-
output_tokens: Optional[int] = None,
|
126
|
-
input_units: Optional[float] = None,
|
127
|
-
output_units: Optional[float] = None,
|
128
|
-
metadata: Optional[Dict[str, Any]] = None
|
129
|
-
) -> UsageRecord:
|
130
|
-
"""
|
131
|
-
Track a usage event
|
132
|
-
|
133
|
-
Args:
|
134
|
-
provider: AI provider name
|
135
|
-
service_type: Type of service used
|
136
|
-
model_name: Name of the model
|
137
|
-
operation: Operation performed (e.g., 'chat', 'embedding', 'image_generation')
|
138
|
-
input_tokens: Number of input tokens
|
139
|
-
output_tokens: Number of output tokens
|
140
|
-
input_units: Input units for non-token services (e.g., audio seconds, image count)
|
141
|
-
output_units: Output units for non-token services
|
142
|
-
metadata: Additional metadata
|
143
|
-
|
144
|
-
Returns:
|
145
|
-
UsageRecord object
|
146
|
-
"""
|
147
|
-
# Convert enums to strings
|
148
|
-
if isinstance(provider, Provider):
|
149
|
-
provider = provider.value
|
150
|
-
if isinstance(service_type, ServiceType):
|
151
|
-
service_type = service_type.value
|
152
|
-
|
153
|
-
# Calculate total tokens
|
154
|
-
total_tokens = None
|
155
|
-
if input_tokens is not None or output_tokens is not None:
|
156
|
-
total_tokens = (input_tokens or 0) + (output_tokens or 0)
|
157
|
-
|
158
|
-
# Calculate cost
|
159
|
-
cost_usd = self._calculate_cost(
|
160
|
-
provider, model_name, operation,
|
161
|
-
input_tokens, output_tokens, input_units, output_units
|
162
|
-
)
|
163
|
-
|
164
|
-
# Create usage record
|
165
|
-
record = UsageRecord(
|
166
|
-
timestamp=datetime.now(timezone.utc).isoformat(),
|
167
|
-
provider=provider,
|
168
|
-
service_type=service_type,
|
169
|
-
model_name=model_name,
|
170
|
-
operation=operation,
|
171
|
-
input_tokens=input_tokens,
|
172
|
-
output_tokens=output_tokens,
|
173
|
-
total_tokens=total_tokens,
|
174
|
-
input_units=input_units,
|
175
|
-
output_units=output_units,
|
176
|
-
cost_usd=cost_usd,
|
177
|
-
metadata=metadata or {}
|
178
|
-
)
|
179
|
-
|
180
|
-
# Add to records and save
|
181
|
-
self.usage_records.append(record)
|
182
|
-
self._save_data()
|
183
|
-
|
184
|
-
logger.info(f"Tracked usage: {provider}/{model_name} - ${cost_usd:.6f}")
|
185
|
-
return record
|
186
|
-
|
187
|
-
def _get_model_pricing(self, provider: str, model_name: str) -> Optional[Dict[str, float]]:
|
188
|
-
"""Get pricing information from ModelManager"""
|
189
|
-
try:
|
190
|
-
from isa_model.core.model_manager import ModelManager
|
191
|
-
pricing = ModelManager.MODEL_PRICING.get(provider, {}).get(model_name)
|
192
|
-
if pricing:
|
193
|
-
return pricing
|
194
|
-
|
195
|
-
# Fallback to legacy pricing for backward compatibility
|
196
|
-
legacy_pricing = self._get_legacy_pricing(provider, model_name)
|
197
|
-
if legacy_pricing:
|
198
|
-
return legacy_pricing
|
199
|
-
|
200
|
-
return None
|
201
|
-
except ImportError:
|
202
|
-
# Fallback to legacy pricing if ModelManager is not available
|
203
|
-
return self._get_legacy_pricing(provider, model_name)
|
204
|
-
|
205
|
-
def _get_legacy_pricing(self, provider: str, model_name: str) -> Optional[Dict[str, float]]:
|
206
|
-
"""Legacy pricing information for backward compatibility"""
|
207
|
-
LEGACY_PRICING = {
|
208
|
-
"openai": {
|
209
|
-
"gpt-4.1-mini": {"input": 0.4, "output": 1.6},
|
210
|
-
"gpt-4o": {"input": 5.0, "output": 15.0},
|
211
|
-
"gpt-4o-mini": {"input": 0.15, "output": 0.6},
|
212
|
-
"text-embedding-3-small": {"input": 0.02, "output": 0.0},
|
213
|
-
"text-embedding-3-large": {"input": 0.13, "output": 0.0},
|
214
|
-
"whisper-1": {"input": 6.0, "output": 0.0},
|
215
|
-
"tts-1": {"input": 15.0, "output": 0.0},
|
216
|
-
"tts-1-hd": {"input": 30.0, "output": 0.0},
|
217
|
-
},
|
218
|
-
"ollama": {
|
219
|
-
"default": {"input": 0.0, "output": 0.0}
|
220
|
-
},
|
221
|
-
"replicate": {
|
222
|
-
"black-forest-labs/flux-schnell": {"input": 0.003, "output": 0.0},
|
223
|
-
"meta/meta-llama-3-8b-instruct": {"input": 0.05, "output": 0.25},
|
224
|
-
}
|
225
|
-
}
|
226
|
-
|
227
|
-
provider_pricing = LEGACY_PRICING.get(provider, {})
|
228
|
-
return provider_pricing.get(model_name) or provider_pricing.get("default")
|
229
|
-
|
230
|
-
def _calculate_cost(
|
231
|
-
self,
|
232
|
-
provider: str,
|
233
|
-
model_name: str,
|
234
|
-
operation: str,
|
235
|
-
input_tokens: Optional[int] = None,
|
236
|
-
output_tokens: Optional[int] = None,
|
237
|
-
input_units: Optional[float] = None,
|
238
|
-
output_units: Optional[float] = None
|
239
|
-
) -> float:
|
240
|
-
"""Calculate cost for a usage event"""
|
241
|
-
try:
|
242
|
-
# Get pricing using unified model manager
|
243
|
-
model_pricing = self._get_model_pricing(provider, model_name)
|
244
|
-
|
245
|
-
if not model_pricing:
|
246
|
-
logger.warning(f"No pricing found for {provider}/{model_name}")
|
247
|
-
return 0.0
|
248
|
-
|
249
|
-
cost = 0.0
|
250
|
-
|
251
|
-
# Token-based pricing (per 1M tokens)
|
252
|
-
if input_tokens is not None and "input" in model_pricing:
|
253
|
-
cost += (input_tokens / 1000000) * model_pricing["input"]
|
254
|
-
|
255
|
-
if output_tokens is not None and "output" in model_pricing:
|
256
|
-
cost += (output_tokens / 1000000) * model_pricing["output"]
|
257
|
-
|
258
|
-
return cost
|
259
|
-
|
260
|
-
except Exception as e:
|
261
|
-
logger.error(f"Error calculating cost: {e}")
|
262
|
-
return 0.0
|
263
|
-
|
264
|
-
def get_session_summary(self) -> Dict[str, Any]:
|
265
|
-
"""Get billing summary for current session"""
|
266
|
-
session_records = [
|
267
|
-
record for record in self.usage_records
|
268
|
-
if record.timestamp >= self.session_start
|
269
|
-
]
|
270
|
-
|
271
|
-
return self._generate_summary(session_records, "Current Session")
|
272
|
-
|
273
|
-
def get_total_summary(self) -> Dict[str, Any]:
|
274
|
-
"""Get total billing summary"""
|
275
|
-
return self._generate_summary(self.usage_records, "Total Usage")
|
276
|
-
|
277
|
-
def get_provider_summary(self, provider: Union[str, Provider]) -> Dict[str, Any]:
|
278
|
-
"""Get billing summary for a specific provider"""
|
279
|
-
if isinstance(provider, Provider):
|
280
|
-
provider = provider.value
|
281
|
-
|
282
|
-
provider_records = [
|
283
|
-
record for record in self.usage_records
|
284
|
-
if record.provider == provider
|
285
|
-
]
|
286
|
-
|
287
|
-
return self._generate_summary(provider_records, f"{provider.title()} Usage")
|
288
|
-
|
289
|
-
def _generate_summary(self, records: List[UsageRecord], title: str) -> Dict[str, Any]:
|
290
|
-
"""Generate billing summary from records"""
|
291
|
-
if not records:
|
292
|
-
return {
|
293
|
-
"title": title,
|
294
|
-
"total_cost": 0.0,
|
295
|
-
"total_requests": 0,
|
296
|
-
"providers": {},
|
297
|
-
"services": {},
|
298
|
-
"models": {}
|
299
|
-
}
|
300
|
-
|
301
|
-
total_cost = sum(record.cost_usd or 0 for record in records)
|
302
|
-
total_requests = len(records)
|
303
|
-
|
304
|
-
# Group by provider
|
305
|
-
providers = {}
|
306
|
-
for record in records:
|
307
|
-
if record.provider not in providers:
|
308
|
-
providers[record.provider] = {
|
309
|
-
"cost": 0.0,
|
310
|
-
"requests": 0,
|
311
|
-
"models": set()
|
312
|
-
}
|
313
|
-
providers[record.provider]["cost"] += record.cost_usd or 0
|
314
|
-
providers[record.provider]["requests"] += 1
|
315
|
-
providers[record.provider]["models"].add(record.model_name)
|
316
|
-
|
317
|
-
# Convert sets to lists for JSON serialization
|
318
|
-
for provider_data in providers.values():
|
319
|
-
provider_data["models"] = list(provider_data["models"])
|
320
|
-
|
321
|
-
# Group by service type
|
322
|
-
services = {}
|
323
|
-
for record in records:
|
324
|
-
if record.service_type not in services:
|
325
|
-
services[record.service_type] = {
|
326
|
-
"cost": 0.0,
|
327
|
-
"requests": 0
|
328
|
-
}
|
329
|
-
services[record.service_type]["cost"] += record.cost_usd or 0
|
330
|
-
services[record.service_type]["requests"] += 1
|
331
|
-
|
332
|
-
# Group by model
|
333
|
-
models = {}
|
334
|
-
for record in records:
|
335
|
-
model_key = f"{record.provider}/{record.model_name}"
|
336
|
-
if model_key not in models:
|
337
|
-
models[model_key] = {
|
338
|
-
"cost": 0.0,
|
339
|
-
"requests": 0,
|
340
|
-
"total_tokens": 0
|
341
|
-
}
|
342
|
-
models[model_key]["cost"] += record.cost_usd or 0
|
343
|
-
models[model_key]["requests"] += 1
|
344
|
-
if record.total_tokens:
|
345
|
-
models[model_key]["total_tokens"] += record.total_tokens
|
346
|
-
|
347
|
-
return {
|
348
|
-
"title": title,
|
349
|
-
"total_cost": round(total_cost, 6),
|
350
|
-
"total_requests": total_requests,
|
351
|
-
"providers": providers,
|
352
|
-
"services": services,
|
353
|
-
"models": models,
|
354
|
-
"period": {
|
355
|
-
"start": records[0].timestamp if records else None,
|
356
|
-
"end": records[-1].timestamp if records else None
|
357
|
-
}
|
358
|
-
}
|
359
|
-
|
360
|
-
def print_summary(self, summary_type: str = "session"):
|
361
|
-
"""Print billing summary to console"""
|
362
|
-
if summary_type == "session":
|
363
|
-
summary = self.get_session_summary()
|
364
|
-
elif summary_type == "total":
|
365
|
-
summary = self.get_total_summary()
|
366
|
-
else:
|
367
|
-
raise ValueError("summary_type must be 'session' or 'total'")
|
368
|
-
|
369
|
-
print(f"\n💰 {summary['title']} Billing Summary")
|
370
|
-
print("=" * 50)
|
371
|
-
print(f"💵 Total Cost: ${summary['total_cost']:.6f}")
|
372
|
-
print(f"📊 Total Requests: {summary['total_requests']}")
|
373
|
-
|
374
|
-
if summary['providers']:
|
375
|
-
print("\n📈 By Provider:")
|
376
|
-
for provider, data in summary['providers'].items():
|
377
|
-
print(f" {provider}: ${data['cost']:.6f} ({data['requests']} requests)")
|
378
|
-
|
379
|
-
if summary['services']:
|
380
|
-
print("\n🔧 By Service:")
|
381
|
-
for service, data in summary['services'].items():
|
382
|
-
print(f" {service}: ${data['cost']:.6f} ({data['requests']} requests)")
|
383
|
-
|
384
|
-
if summary['models']:
|
385
|
-
print("\n🤖 By Model:")
|
386
|
-
for model, data in summary['models'].items():
|
387
|
-
tokens_info = f" ({data['total_tokens']} tokens)" if data['total_tokens'] > 0 else ""
|
388
|
-
print(f" {model}: ${data['cost']:.6f} ({data['requests']} requests){tokens_info}")
|
389
|
-
|
390
|
-
# Global billing tracker instance
|
391
|
-
_global_tracker: Optional[BillingTracker] = None
|
392
|
-
|
393
|
-
def get_billing_tracker() -> BillingTracker:
|
394
|
-
"""Get the global billing tracker instance"""
|
395
|
-
global _global_tracker
|
396
|
-
if _global_tracker is None:
|
397
|
-
_global_tracker = BillingTracker()
|
398
|
-
return _global_tracker
|
399
|
-
|
400
|
-
def track_usage(**kwargs) -> UsageRecord:
|
401
|
-
"""Convenience function to track usage"""
|
402
|
-
return get_billing_tracker().track_usage(**kwargs)
|
403
|
-
|
404
|
-
def print_billing_summary(summary_type: str = "session"):
|
405
|
-
"""Convenience function to print billing summary"""
|
406
|
-
get_billing_tracker().print_summary(summary_type)
|