justllms 1.0.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.
- justllms/__init__.py +43 -0
- justllms/__version__.py +3 -0
- justllms/analytics/__init__.py +23 -0
- justllms/analytics/dashboard.py +486 -0
- justllms/analytics/models.py +172 -0
- justllms/analytics/reports.py +530 -0
- justllms/cache/__init__.py +15 -0
- justllms/cache/backends.py +204 -0
- justllms/cache/cache_manager.py +238 -0
- justllms/cache/redis_backend.py +276 -0
- justllms/config/__init__.py +9 -0
- justllms/config/config.py +274 -0
- justllms/conversations/__init__.py +18 -0
- justllms/conversations/context.py +215 -0
- justllms/conversations/conversation.py +406 -0
- justllms/conversations/manager.py +466 -0
- justllms/conversations/models.py +182 -0
- justllms/conversations/storage.py +491 -0
- justllms/core/__init__.py +17 -0
- justllms/core/base.py +126 -0
- justllms/core/client.py +357 -0
- justllms/core/completion.py +188 -0
- justllms/core/models.py +77 -0
- justllms/exceptions/__init__.py +23 -0
- justllms/exceptions/exceptions.py +119 -0
- justllms/health/__init__.py +11 -0
- justllms/health/checker.py +393 -0
- justllms/health/models.py +201 -0
- justllms/monitoring/__init__.py +13 -0
- justllms/monitoring/cost_tracker.py +239 -0
- justllms/monitoring/logger.py +250 -0
- justllms/monitoring/metrics.py +232 -0
- justllms/monitoring/monitor.py +276 -0
- justllms/providers/__init__.py +89 -0
- justllms/providers/anthropic.py +352 -0
- justllms/providers/azure_openai.py +395 -0
- justllms/providers/deepseek.py +334 -0
- justllms/providers/google.py +505 -0
- justllms/providers/grok.py +346 -0
- justllms/providers/openai.py +304 -0
- justllms/routing/__init__.py +17 -0
- justllms/routing/router.py +162 -0
- justllms/routing/strategies.py +422 -0
- justllms/utils/__init__.py +14 -0
- justllms/utils/retry.py +184 -0
- justllms/utils/token_counter.py +168 -0
- justllms/utils/validators.py +228 -0
- justllms/validation/__init__.py +29 -0
- justllms/validation/engine.py +360 -0
- justllms/validation/models.py +259 -0
- justllms/validation/processors.py +336 -0
- justllms-1.0.0.dist-info/METADATA +568 -0
- justllms-1.0.0.dist-info/RECORD +56 -0
- justllms-1.0.0.dist-info/WHEEL +5 -0
- justllms-1.0.0.dist-info/licenses/LICENSE +21 -0
- justllms-1.0.0.dist-info/top_level.txt +1 -0
justllms/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""JustLLMs - A unified gateway for multiple LLM providers.
|
|
2
|
+
|
|
3
|
+
This package provides:
|
|
4
|
+
- Unified API interface for multiple LLM providers
|
|
5
|
+
- Conversational AI with automatic context management
|
|
6
|
+
- Intelligent model routing and selection
|
|
7
|
+
- Built-in monitoring, logging, and cost tracking
|
|
8
|
+
- Response caching and fallback mechanisms
|
|
9
|
+
- Persistent conversation storage (memory, disk, Redis)
|
|
10
|
+
- Extensible provider system
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from justllms.__version__ import __version__
|
|
14
|
+
|
|
15
|
+
__author__ = "Your Name"
|
|
16
|
+
__email__ = "your.email@example.com"
|
|
17
|
+
|
|
18
|
+
from justllms.core.client import Client
|
|
19
|
+
from justllms.core.completion import Completion, CompletionResponse
|
|
20
|
+
from justllms.core.models import Message, Role
|
|
21
|
+
from justllms.conversations import Conversation, ConversationManager
|
|
22
|
+
from justllms.conversations.models import ConversationConfig, ConversationState
|
|
23
|
+
from justllms.exceptions import JustLLMsError, ProviderError, RouteError
|
|
24
|
+
|
|
25
|
+
# Main client alias
|
|
26
|
+
JustLLM = Client
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"__version__",
|
|
30
|
+
"JustLLM", # Main client
|
|
31
|
+
"Client", # Alternative name
|
|
32
|
+
"Completion",
|
|
33
|
+
"CompletionResponse",
|
|
34
|
+
"Message",
|
|
35
|
+
"Role",
|
|
36
|
+
"Conversation",
|
|
37
|
+
"ConversationManager",
|
|
38
|
+
"ConversationConfig",
|
|
39
|
+
"ConversationState",
|
|
40
|
+
"JustLLMsError",
|
|
41
|
+
"ProviderError",
|
|
42
|
+
"RouteError",
|
|
43
|
+
]
|
justllms/__version__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Unified analytics and reporting system."""
|
|
2
|
+
|
|
3
|
+
from .dashboard import AnalyticsDashboard
|
|
4
|
+
from .models import (
|
|
5
|
+
AnalyticsReport,
|
|
6
|
+
CrossProviderMetrics,
|
|
7
|
+
ProviderStats,
|
|
8
|
+
TimeSeriesData,
|
|
9
|
+
UsageBreakdown,
|
|
10
|
+
)
|
|
11
|
+
from .reports import CSVExporter, PDFExporter, ReportExporter
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AnalyticsDashboard",
|
|
15
|
+
"AnalyticsReport",
|
|
16
|
+
"CrossProviderMetrics",
|
|
17
|
+
"ProviderStats",
|
|
18
|
+
"TimeSeriesData",
|
|
19
|
+
"UsageBreakdown",
|
|
20
|
+
"CSVExporter",
|
|
21
|
+
"PDFExporter",
|
|
22
|
+
"ReportExporter",
|
|
23
|
+
]
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Unified analytics dashboard for cross-provider metrics."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from justllms.analytics.models import (
|
|
9
|
+
AnalyticsReport,
|
|
10
|
+
CrossProviderMetrics,
|
|
11
|
+
ModelStats,
|
|
12
|
+
ProviderStats,
|
|
13
|
+
ReportConfig,
|
|
14
|
+
TimeSeriesData,
|
|
15
|
+
UsageBreakdown,
|
|
16
|
+
)
|
|
17
|
+
from justllms.monitoring.cost_tracker import CostTracker
|
|
18
|
+
from justllms.monitoring.metrics import MetricsCollector
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AnalyticsDashboard:
|
|
22
|
+
"""Unified analytics dashboard for cross-provider LLM usage."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
metrics_collector: Optional[MetricsCollector] = None,
|
|
27
|
+
cost_tracker: Optional[CostTracker] = None,
|
|
28
|
+
):
|
|
29
|
+
self.metrics_collector = metrics_collector
|
|
30
|
+
self.cost_tracker = cost_tracker
|
|
31
|
+
|
|
32
|
+
# In-memory aggregated data (for when no external collectors available)
|
|
33
|
+
self._request_data = []
|
|
34
|
+
self._cost_data = []
|
|
35
|
+
self._error_data = []
|
|
36
|
+
self._latency_data = []
|
|
37
|
+
self._cache_data = {"hits": 0, "misses": 0}
|
|
38
|
+
|
|
39
|
+
def add_request_data(
|
|
40
|
+
self,
|
|
41
|
+
provider: str,
|
|
42
|
+
model: str,
|
|
43
|
+
request_type: str = "completion",
|
|
44
|
+
tokens_used: int = 0,
|
|
45
|
+
cost: float = 0.0,
|
|
46
|
+
latency_ms: float = 0.0,
|
|
47
|
+
timestamp: Optional[datetime] = None,
|
|
48
|
+
success: bool = True,
|
|
49
|
+
cached: bool = False,
|
|
50
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Add request data point to dashboard."""
|
|
53
|
+
timestamp = timestamp or datetime.now()
|
|
54
|
+
|
|
55
|
+
entry = {
|
|
56
|
+
"timestamp": timestamp,
|
|
57
|
+
"provider": provider,
|
|
58
|
+
"model": model,
|
|
59
|
+
"request_type": request_type,
|
|
60
|
+
"tokens_used": tokens_used,
|
|
61
|
+
"cost": cost,
|
|
62
|
+
"latency_ms": latency_ms,
|
|
63
|
+
"success": success,
|
|
64
|
+
"cached": cached,
|
|
65
|
+
"metadata": metadata or {},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
self._request_data.append(entry)
|
|
69
|
+
|
|
70
|
+
# Update cache stats
|
|
71
|
+
if cached:
|
|
72
|
+
self._cache_data["hits"] += 1
|
|
73
|
+
else:
|
|
74
|
+
self._cache_data["misses"] += 1
|
|
75
|
+
|
|
76
|
+
def generate_report(self, config: Optional[ReportConfig] = None) -> AnalyticsReport:
|
|
77
|
+
"""Generate comprehensive analytics report."""
|
|
78
|
+
config = config or ReportConfig()
|
|
79
|
+
|
|
80
|
+
# Determine time period
|
|
81
|
+
end_time = config.period_end or datetime.now()
|
|
82
|
+
start_time = config.period_start or (end_time - timedelta(hours=24))
|
|
83
|
+
|
|
84
|
+
# Filter data
|
|
85
|
+
filtered_data = self._filter_data(start_time, end_time, config)
|
|
86
|
+
|
|
87
|
+
# Generate cross-provider metrics
|
|
88
|
+
cross_provider_metrics = self._calculate_cross_provider_metrics(filtered_data, config)
|
|
89
|
+
|
|
90
|
+
# Generate usage breakdown
|
|
91
|
+
usage_breakdown = self._calculate_usage_breakdown(filtered_data, config)
|
|
92
|
+
|
|
93
|
+
# Generate time series data
|
|
94
|
+
time_series = []
|
|
95
|
+
if config.include_time_series:
|
|
96
|
+
time_series = self._generate_time_series(filtered_data, start_time, end_time, config)
|
|
97
|
+
|
|
98
|
+
# Generate top lists
|
|
99
|
+
top_models_by_usage = self._get_top_models_by_usage(filtered_data, config.top_n)
|
|
100
|
+
top_models_by_cost = self._get_top_models_by_cost(filtered_data, config.top_n)
|
|
101
|
+
top_providers_by_requests = self._get_top_providers_by_requests(filtered_data, config.top_n)
|
|
102
|
+
|
|
103
|
+
report = AnalyticsReport(
|
|
104
|
+
report_id=str(uuid.uuid4()),
|
|
105
|
+
period_start=start_time,
|
|
106
|
+
period_end=end_time,
|
|
107
|
+
cross_provider_metrics=cross_provider_metrics,
|
|
108
|
+
usage_breakdown=usage_breakdown,
|
|
109
|
+
time_series=time_series,
|
|
110
|
+
top_models_by_usage=top_models_by_usage,
|
|
111
|
+
top_models_by_cost=top_models_by_cost,
|
|
112
|
+
top_providers_by_requests=top_providers_by_requests,
|
|
113
|
+
filters={
|
|
114
|
+
"providers": config.providers,
|
|
115
|
+
"models": config.models,
|
|
116
|
+
"custom_filters": config.custom_filters,
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return report
|
|
121
|
+
|
|
122
|
+
def _filter_data(
|
|
123
|
+
self,
|
|
124
|
+
start_time: datetime,
|
|
125
|
+
end_time: datetime,
|
|
126
|
+
config: ReportConfig
|
|
127
|
+
) -> List[Dict[str, Any]]:
|
|
128
|
+
"""Filter request data based on time period and config."""
|
|
129
|
+
filtered = []
|
|
130
|
+
|
|
131
|
+
# Combine data from multiple sources
|
|
132
|
+
data_sources = [self._request_data]
|
|
133
|
+
|
|
134
|
+
# Add cost tracker data if available
|
|
135
|
+
if self.cost_tracker:
|
|
136
|
+
cost_history = self.cost_tracker.get_cost_history(limit=10000)
|
|
137
|
+
for entry in cost_history:
|
|
138
|
+
timestamp = datetime.fromisoformat(entry["timestamp"])
|
|
139
|
+
if start_time <= timestamp <= end_time:
|
|
140
|
+
data_sources.append([{
|
|
141
|
+
"timestamp": timestamp,
|
|
142
|
+
"provider": entry["provider"],
|
|
143
|
+
"model": entry["model"],
|
|
144
|
+
"request_type": "completion",
|
|
145
|
+
"tokens_used": entry["total_tokens"],
|
|
146
|
+
"cost": entry["estimated_cost"],
|
|
147
|
+
"latency_ms": 0.0,
|
|
148
|
+
"success": True,
|
|
149
|
+
"cached": False,
|
|
150
|
+
"metadata": entry.get("metadata", {}),
|
|
151
|
+
}])
|
|
152
|
+
|
|
153
|
+
# Add metrics collector data if available
|
|
154
|
+
if self.metrics_collector:
|
|
155
|
+
metrics_summary = self.metrics_collector.get_metrics_summary()
|
|
156
|
+
# Convert metrics to standardized format
|
|
157
|
+
# This is a simplified conversion - in practice you'd need more detailed data
|
|
158
|
+
|
|
159
|
+
for source in data_sources:
|
|
160
|
+
for entry in source:
|
|
161
|
+
# Time filter
|
|
162
|
+
if start_time <= entry["timestamp"] <= end_time:
|
|
163
|
+
# Provider filter
|
|
164
|
+
if config.providers and entry["provider"] not in config.providers:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
# Model filter
|
|
168
|
+
if config.models and entry["model"] not in config.models:
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
filtered.append(entry)
|
|
172
|
+
|
|
173
|
+
return filtered
|
|
174
|
+
|
|
175
|
+
def _calculate_cross_provider_metrics(
|
|
176
|
+
self,
|
|
177
|
+
data: List[Dict[str, Any]],
|
|
178
|
+
config: ReportConfig
|
|
179
|
+
) -> CrossProviderMetrics:
|
|
180
|
+
"""Calculate cross-provider summary metrics."""
|
|
181
|
+
if not data:
|
|
182
|
+
return CrossProviderMetrics()
|
|
183
|
+
|
|
184
|
+
# Aggregate basic stats
|
|
185
|
+
total_requests = len(data)
|
|
186
|
+
total_tokens = sum(entry["tokens_used"] for entry in data)
|
|
187
|
+
total_cost = sum(entry["cost"] for entry in data)
|
|
188
|
+
total_errors = sum(1 for entry in data if not entry["success"])
|
|
189
|
+
|
|
190
|
+
# Calculate unique counts
|
|
191
|
+
unique_providers = len(set(entry["provider"] for entry in data))
|
|
192
|
+
unique_models = len(set(f"{entry['provider']}:{entry['model']}" for entry in data))
|
|
193
|
+
|
|
194
|
+
# Calculate cache hit rate
|
|
195
|
+
cache_hits = self._cache_data["hits"]
|
|
196
|
+
cache_misses = self._cache_data["misses"]
|
|
197
|
+
total_cache_requests = cache_hits + cache_misses
|
|
198
|
+
cache_hit_rate = (cache_hits / total_cache_requests * 100) if total_cache_requests > 0 else 0.0
|
|
199
|
+
|
|
200
|
+
# Calculate average latency
|
|
201
|
+
latencies = [entry["latency_ms"] for entry in data if entry["latency_ms"] > 0]
|
|
202
|
+
average_latency_ms = sum(latencies) / len(latencies) if latencies else 0.0
|
|
203
|
+
|
|
204
|
+
# Calculate provider-level stats for rankings
|
|
205
|
+
provider_stats = self._calculate_provider_stats(data)
|
|
206
|
+
|
|
207
|
+
# Determine best providers
|
|
208
|
+
most_used_provider = max(provider_stats.keys(),
|
|
209
|
+
key=lambda p: provider_stats[p]["requests"]) if provider_stats else None
|
|
210
|
+
|
|
211
|
+
most_expensive_provider = max(provider_stats.keys(),
|
|
212
|
+
key=lambda p: provider_stats[p]["cost"]) if provider_stats else None
|
|
213
|
+
|
|
214
|
+
fastest_provider = min(provider_stats.keys(),
|
|
215
|
+
key=lambda p: provider_stats[p]["avg_latency"]) if provider_stats else None
|
|
216
|
+
|
|
217
|
+
most_reliable_provider = max(provider_stats.keys(),
|
|
218
|
+
key=lambda p: provider_stats[p]["success_rate"]) if provider_stats else None
|
|
219
|
+
|
|
220
|
+
# Generate rankings
|
|
221
|
+
cost_efficiency_ranking = sorted(
|
|
222
|
+
provider_stats.keys(),
|
|
223
|
+
key=lambda p: provider_stats[p]["cost_per_token"]
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
performance_ranking = sorted(
|
|
227
|
+
provider_stats.keys(),
|
|
228
|
+
key=lambda p: provider_stats[p]["avg_latency"]
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
reliability_ranking = sorted(
|
|
232
|
+
provider_stats.keys(),
|
|
233
|
+
key=lambda p: provider_stats[p]["success_rate"],
|
|
234
|
+
reverse=True
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return CrossProviderMetrics(
|
|
238
|
+
total_requests=total_requests,
|
|
239
|
+
total_tokens=total_tokens,
|
|
240
|
+
total_cost=total_cost,
|
|
241
|
+
total_errors=total_errors,
|
|
242
|
+
unique_providers=unique_providers,
|
|
243
|
+
unique_models=unique_models,
|
|
244
|
+
cache_hit_rate=cache_hit_rate,
|
|
245
|
+
average_latency_ms=average_latency_ms,
|
|
246
|
+
most_used_provider=most_used_provider,
|
|
247
|
+
most_expensive_provider=most_expensive_provider,
|
|
248
|
+
fastest_provider=fastest_provider,
|
|
249
|
+
most_reliable_provider=most_reliable_provider,
|
|
250
|
+
cost_efficiency_ranking=cost_efficiency_ranking,
|
|
251
|
+
performance_ranking=performance_ranking,
|
|
252
|
+
reliability_ranking=reliability_ranking,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def _calculate_provider_stats(self, data: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
|
|
256
|
+
"""Calculate detailed stats for each provider."""
|
|
257
|
+
stats = defaultdict(lambda: {
|
|
258
|
+
"requests": 0,
|
|
259
|
+
"tokens": 0,
|
|
260
|
+
"cost": 0.0,
|
|
261
|
+
"errors": 0,
|
|
262
|
+
"latency_sum": 0.0,
|
|
263
|
+
"latency_count": 0,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
for entry in data:
|
|
267
|
+
provider = entry["provider"]
|
|
268
|
+
stats[provider]["requests"] += 1
|
|
269
|
+
stats[provider]["tokens"] += entry["tokens_used"]
|
|
270
|
+
stats[provider]["cost"] += entry["cost"]
|
|
271
|
+
|
|
272
|
+
if not entry["success"]:
|
|
273
|
+
stats[provider]["errors"] += 1
|
|
274
|
+
|
|
275
|
+
if entry["latency_ms"] > 0:
|
|
276
|
+
stats[provider]["latency_sum"] += entry["latency_ms"]
|
|
277
|
+
stats[provider]["latency_count"] += 1
|
|
278
|
+
|
|
279
|
+
# Calculate derived metrics
|
|
280
|
+
for provider, data in stats.items():
|
|
281
|
+
data["success_rate"] = ((data["requests"] - data["errors"]) / data["requests"] * 100) if data["requests"] > 0 else 0.0
|
|
282
|
+
data["cost_per_token"] = data["cost"] / data["tokens"] if data["tokens"] > 0 else 0.0
|
|
283
|
+
data["avg_latency"] = data["latency_sum"] / data["latency_count"] if data["latency_count"] > 0 else 0.0
|
|
284
|
+
|
|
285
|
+
return dict(stats)
|
|
286
|
+
|
|
287
|
+
def _calculate_usage_breakdown(
|
|
288
|
+
self,
|
|
289
|
+
data: List[Dict[str, Any]],
|
|
290
|
+
config: ReportConfig
|
|
291
|
+
) -> UsageBreakdown:
|
|
292
|
+
"""Calculate detailed usage breakdown."""
|
|
293
|
+
provider_stats = {}
|
|
294
|
+
model_stats = {}
|
|
295
|
+
request_type_counts = defaultdict(int)
|
|
296
|
+
|
|
297
|
+
# Calculate provider stats
|
|
298
|
+
provider_data = defaultdict(list)
|
|
299
|
+
for entry in data:
|
|
300
|
+
provider_data[entry["provider"]].append(entry)
|
|
301
|
+
|
|
302
|
+
for provider, entries in provider_data.items():
|
|
303
|
+
total_requests = len(entries)
|
|
304
|
+
total_tokens = sum(e["tokens_used"] for e in entries)
|
|
305
|
+
total_cost = sum(e["cost"] for e in entries)
|
|
306
|
+
errors = sum(1 for e in entries if not e["success"])
|
|
307
|
+
latencies = [e["latency_ms"] for e in entries if e["latency_ms"] > 0]
|
|
308
|
+
avg_latency = sum(latencies) / len(latencies) if latencies else 0.0
|
|
309
|
+
models_used = list(set(e["model"] for e in entries))
|
|
310
|
+
|
|
311
|
+
provider_stats[provider] = ProviderStats(
|
|
312
|
+
provider=provider,
|
|
313
|
+
total_requests=total_requests,
|
|
314
|
+
total_tokens=total_tokens,
|
|
315
|
+
total_cost=total_cost,
|
|
316
|
+
error_count=errors,
|
|
317
|
+
error_rate=(errors / total_requests * 100) if total_requests > 0 else 0.0,
|
|
318
|
+
average_latency_ms=avg_latency,
|
|
319
|
+
models_used=models_used,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Calculate model stats
|
|
323
|
+
model_data = defaultdict(list)
|
|
324
|
+
for entry in data:
|
|
325
|
+
key = f"{entry['provider']}:{entry['model']}"
|
|
326
|
+
model_data[key].append(entry)
|
|
327
|
+
|
|
328
|
+
for model_key, entries in model_data.items():
|
|
329
|
+
provider, model = model_key.split(":", 1)
|
|
330
|
+
total_requests = len(entries)
|
|
331
|
+
total_tokens = sum(e["tokens_used"] for e in entries)
|
|
332
|
+
total_cost = sum(e["cost"] for e in entries)
|
|
333
|
+
errors = sum(1 for e in entries if not e["success"])
|
|
334
|
+
latencies = [e["latency_ms"] for e in entries if e["latency_ms"] > 0]
|
|
335
|
+
avg_latency = sum(latencies) / len(latencies) if latencies else 0.0
|
|
336
|
+
|
|
337
|
+
model_stats[model_key] = ModelStats(
|
|
338
|
+
provider=provider,
|
|
339
|
+
model=model,
|
|
340
|
+
total_requests=total_requests,
|
|
341
|
+
total_tokens=total_tokens,
|
|
342
|
+
total_cost=total_cost,
|
|
343
|
+
error_count=errors,
|
|
344
|
+
average_latency_ms=avg_latency,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Calculate request type breakdown
|
|
348
|
+
for entry in data:
|
|
349
|
+
request_type_counts[entry["request_type"]] += 1
|
|
350
|
+
|
|
351
|
+
# Generate time series (simplified - hourly for last 24 hours)
|
|
352
|
+
hourly_data = self._generate_time_series(data,
|
|
353
|
+
datetime.now() - timedelta(hours=24),
|
|
354
|
+
datetime.now(),
|
|
355
|
+
config)
|
|
356
|
+
|
|
357
|
+
return UsageBreakdown(
|
|
358
|
+
by_provider=provider_stats,
|
|
359
|
+
by_model=model_stats,
|
|
360
|
+
by_request_type=dict(request_type_counts),
|
|
361
|
+
by_hour=hourly_data,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def _generate_time_series(
|
|
365
|
+
self,
|
|
366
|
+
data: List[Dict[str, Any]],
|
|
367
|
+
start_time: datetime,
|
|
368
|
+
end_time: datetime,
|
|
369
|
+
config: ReportConfig,
|
|
370
|
+
) -> List[TimeSeriesData]:
|
|
371
|
+
"""Generate time series data."""
|
|
372
|
+
if config.time_series_granularity == "hourly":
|
|
373
|
+
interval = timedelta(hours=1)
|
|
374
|
+
else: # daily
|
|
375
|
+
interval = timedelta(days=1)
|
|
376
|
+
|
|
377
|
+
time_series = []
|
|
378
|
+
current_time = start_time
|
|
379
|
+
|
|
380
|
+
while current_time < end_time:
|
|
381
|
+
next_time = current_time + interval
|
|
382
|
+
|
|
383
|
+
# Filter data for this time window
|
|
384
|
+
window_data = [
|
|
385
|
+
entry for entry in data
|
|
386
|
+
if current_time <= entry["timestamp"] < next_time
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
requests = len(window_data)
|
|
390
|
+
tokens = sum(entry["tokens_used"] for entry in window_data)
|
|
391
|
+
cost = sum(entry["cost"] for entry in window_data)
|
|
392
|
+
errors = sum(1 for entry in window_data if not entry["success"])
|
|
393
|
+
latencies = [entry["latency_ms"] for entry in window_data if entry["latency_ms"] > 0]
|
|
394
|
+
avg_latency = sum(latencies) / len(latencies) if latencies else 0.0
|
|
395
|
+
|
|
396
|
+
time_series.append(TimeSeriesData(
|
|
397
|
+
timestamp=current_time,
|
|
398
|
+
requests=requests,
|
|
399
|
+
tokens=tokens,
|
|
400
|
+
cost=cost,
|
|
401
|
+
errors=errors,
|
|
402
|
+
latency_ms=avg_latency,
|
|
403
|
+
))
|
|
404
|
+
|
|
405
|
+
current_time = next_time
|
|
406
|
+
|
|
407
|
+
return time_series
|
|
408
|
+
|
|
409
|
+
def _get_top_models_by_usage(self, data: List[Dict[str, Any]], top_n: int) -> List[ModelStats]:
|
|
410
|
+
"""Get top models by usage (request count)."""
|
|
411
|
+
model_data = defaultdict(list)
|
|
412
|
+
for entry in data:
|
|
413
|
+
key = f"{entry['provider']}:{entry['model']}"
|
|
414
|
+
model_data[key].append(entry)
|
|
415
|
+
|
|
416
|
+
model_stats = []
|
|
417
|
+
for model_key, entries in model_data.items():
|
|
418
|
+
provider, model = model_key.split(":", 1)
|
|
419
|
+
total_requests = len(entries)
|
|
420
|
+
total_tokens = sum(e["tokens_used"] for e in entries)
|
|
421
|
+
total_cost = sum(e["cost"] for e in entries)
|
|
422
|
+
errors = sum(1 for e in entries if not e["success"])
|
|
423
|
+
latencies = [e["latency_ms"] for e in entries if e["latency_ms"] > 0]
|
|
424
|
+
avg_latency = sum(latencies) / len(latencies) if latencies else 0.0
|
|
425
|
+
|
|
426
|
+
model_stats.append(ModelStats(
|
|
427
|
+
provider=provider,
|
|
428
|
+
model=model,
|
|
429
|
+
total_requests=total_requests,
|
|
430
|
+
total_tokens=total_tokens,
|
|
431
|
+
total_cost=total_cost,
|
|
432
|
+
error_count=errors,
|
|
433
|
+
average_latency_ms=avg_latency,
|
|
434
|
+
))
|
|
435
|
+
|
|
436
|
+
return sorted(model_stats, key=lambda x: x.total_requests, reverse=True)[:top_n]
|
|
437
|
+
|
|
438
|
+
def _get_top_models_by_cost(self, data: List[Dict[str, Any]], top_n: int) -> List[ModelStats]:
|
|
439
|
+
"""Get top models by cost."""
|
|
440
|
+
model_stats = self._get_top_models_by_usage(data, len(data)) # Get all, then sort by cost
|
|
441
|
+
return sorted(model_stats, key=lambda x: x.total_cost, reverse=True)[:top_n]
|
|
442
|
+
|
|
443
|
+
def _get_top_providers_by_requests(self, data: List[Dict[str, Any]], top_n: int) -> List[ProviderStats]:
|
|
444
|
+
"""Get top providers by request count."""
|
|
445
|
+
provider_data = defaultdict(list)
|
|
446
|
+
for entry in data:
|
|
447
|
+
provider_data[entry["provider"]].append(entry)
|
|
448
|
+
|
|
449
|
+
provider_stats = []
|
|
450
|
+
for provider, entries in provider_data.items():
|
|
451
|
+
total_requests = len(entries)
|
|
452
|
+
total_tokens = sum(e["tokens_used"] for e in entries)
|
|
453
|
+
total_cost = sum(e["cost"] for e in entries)
|
|
454
|
+
errors = sum(1 for e in entries if not e["success"])
|
|
455
|
+
latencies = [e["latency_ms"] for e in entries if e["latency_ms"] > 0]
|
|
456
|
+
avg_latency = sum(latencies) / len(latencies) if latencies else 0.0
|
|
457
|
+
models_used = list(set(e["model"] for e in entries))
|
|
458
|
+
|
|
459
|
+
provider_stats.append(ProviderStats(
|
|
460
|
+
provider=provider,
|
|
461
|
+
total_requests=total_requests,
|
|
462
|
+
total_tokens=total_tokens,
|
|
463
|
+
total_cost=total_cost,
|
|
464
|
+
error_count=errors,
|
|
465
|
+
error_rate=(errors / total_requests * 100) if total_requests > 0 else 0.0,
|
|
466
|
+
average_latency_ms=avg_latency,
|
|
467
|
+
models_used=models_used,
|
|
468
|
+
))
|
|
469
|
+
|
|
470
|
+
return sorted(provider_stats, key=lambda x: x.total_requests, reverse=True)[:top_n]
|
|
471
|
+
|
|
472
|
+
def get_live_metrics(self) -> Dict[str, Any]:
|
|
473
|
+
"""Get real-time metrics summary."""
|
|
474
|
+
recent_data = [
|
|
475
|
+
entry for entry in self._request_data
|
|
476
|
+
if entry["timestamp"] >= datetime.now() - timedelta(minutes=5)
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
"timestamp": datetime.now().isoformat(),
|
|
481
|
+
"recent_requests_5min": len(recent_data),
|
|
482
|
+
"active_providers": len(set(entry["provider"] for entry in recent_data)),
|
|
483
|
+
"cache_hit_rate": self._cache_data["hits"] / (self._cache_data["hits"] + self._cache_data["misses"]) * 100
|
|
484
|
+
if (self._cache_data["hits"] + self._cache_data["misses"]) > 0 else 0.0,
|
|
485
|
+
"total_requests_all_time": len(self._request_data),
|
|
486
|
+
}
|