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.
Files changed (56) hide show
  1. justllms/__init__.py +43 -0
  2. justllms/__version__.py +3 -0
  3. justllms/analytics/__init__.py +23 -0
  4. justllms/analytics/dashboard.py +486 -0
  5. justllms/analytics/models.py +172 -0
  6. justllms/analytics/reports.py +530 -0
  7. justllms/cache/__init__.py +15 -0
  8. justllms/cache/backends.py +204 -0
  9. justllms/cache/cache_manager.py +238 -0
  10. justllms/cache/redis_backend.py +276 -0
  11. justllms/config/__init__.py +9 -0
  12. justllms/config/config.py +274 -0
  13. justllms/conversations/__init__.py +18 -0
  14. justllms/conversations/context.py +215 -0
  15. justllms/conversations/conversation.py +406 -0
  16. justllms/conversations/manager.py +466 -0
  17. justllms/conversations/models.py +182 -0
  18. justllms/conversations/storage.py +491 -0
  19. justllms/core/__init__.py +17 -0
  20. justllms/core/base.py +126 -0
  21. justllms/core/client.py +357 -0
  22. justllms/core/completion.py +188 -0
  23. justllms/core/models.py +77 -0
  24. justllms/exceptions/__init__.py +23 -0
  25. justllms/exceptions/exceptions.py +119 -0
  26. justllms/health/__init__.py +11 -0
  27. justllms/health/checker.py +393 -0
  28. justllms/health/models.py +201 -0
  29. justllms/monitoring/__init__.py +13 -0
  30. justllms/monitoring/cost_tracker.py +239 -0
  31. justllms/monitoring/logger.py +250 -0
  32. justllms/monitoring/metrics.py +232 -0
  33. justllms/monitoring/monitor.py +276 -0
  34. justllms/providers/__init__.py +89 -0
  35. justllms/providers/anthropic.py +352 -0
  36. justllms/providers/azure_openai.py +395 -0
  37. justllms/providers/deepseek.py +334 -0
  38. justllms/providers/google.py +505 -0
  39. justllms/providers/grok.py +346 -0
  40. justllms/providers/openai.py +304 -0
  41. justllms/routing/__init__.py +17 -0
  42. justllms/routing/router.py +162 -0
  43. justllms/routing/strategies.py +422 -0
  44. justllms/utils/__init__.py +14 -0
  45. justllms/utils/retry.py +184 -0
  46. justllms/utils/token_counter.py +168 -0
  47. justllms/utils/validators.py +228 -0
  48. justllms/validation/__init__.py +29 -0
  49. justllms/validation/engine.py +360 -0
  50. justllms/validation/models.py +259 -0
  51. justllms/validation/processors.py +336 -0
  52. justllms-1.0.0.dist-info/METADATA +568 -0
  53. justllms-1.0.0.dist-info/RECORD +56 -0
  54. justllms-1.0.0.dist-info/WHEEL +5 -0
  55. justllms-1.0.0.dist-info/licenses/LICENSE +21 -0
  56. 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
+ ]
@@ -0,0 +1,3 @@
1
+ """Version information for JustLLMs."""
2
+
3
+ __version__ = "1.0.0"
@@ -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
+ }