omnimancer-cli 0.1.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 (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,679 @@
1
+ """
2
+ Advanced Token Usage Tracking for Omnimancer Agent Performance Monitoring.
3
+
4
+ This module provides detailed token usage tracking with cost estimation,
5
+ provider-specific optimization, and usage pattern analysis.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import statistics
11
+ import threading
12
+ from collections import defaultdict, deque
13
+ from dataclasses import asdict, dataclass, field
14
+ from datetime import datetime, timedelta
15
+ from enum import Enum
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List, Optional
18
+
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class TokenCountMethod(Enum):
24
+ """Methods for counting tokens."""
25
+
26
+ ESTIMATED = "estimated"
27
+ API_REPORTED = "api_reported"
28
+ TIKTOKEN = "tiktoken"
29
+ PROVIDER_SPECIFIC = "provider_specific"
30
+
31
+
32
+ class UsageCategory(Enum):
33
+ """Categories of token usage."""
34
+
35
+ INPUT_CONTEXT = "input_context"
36
+ OUTPUT_GENERATION = "output_generation"
37
+ SYSTEM_PROMPTS = "system_prompts"
38
+ FUNCTION_CALLS = "function_calls"
39
+ CACHED_CONTENT = "cached_content"
40
+ RETRY_ATTEMPTS = "retry_attempts"
41
+
42
+
43
+ @dataclass
44
+ class TokenCost:
45
+ """Token cost information for a specific provider and model."""
46
+
47
+ provider_name: str
48
+ model_name: str
49
+ input_cost_per_1k: float # Cost per 1,000 input tokens
50
+ output_cost_per_1k: float # Cost per 1,000 output tokens
51
+ cached_cost_per_1k: float = 0.0 # Cost per 1,000 cached tokens (if different)
52
+ currency: str = "USD"
53
+ last_updated: datetime = field(default_factory=datetime.now)
54
+
55
+ def calculate_cost(
56
+ self, input_tokens: int, output_tokens: int, cached_tokens: int = 0
57
+ ) -> float:
58
+ """Calculate total cost for token usage."""
59
+ input_cost = (input_tokens / 1000.0) * self.input_cost_per_1k
60
+ output_cost = (output_tokens / 1000.0) * self.output_cost_per_1k
61
+ cached_cost = (cached_tokens / 1000.0) * self.cached_cost_per_1k
62
+ return input_cost + output_cost + cached_cost
63
+
64
+
65
+ @dataclass
66
+ class DetailedTokenUsage:
67
+ """Detailed token usage breakdown."""
68
+
69
+ total_tokens: int
70
+ input_tokens: int
71
+ output_tokens: int
72
+ cached_tokens: int = 0
73
+
74
+ # Detailed breakdown
75
+ context_tokens: int = 0
76
+ system_prompt_tokens: int = 0
77
+ user_prompt_tokens: int = 0
78
+ function_call_tokens: int = 0
79
+ response_tokens: int = 0
80
+
81
+ # Metadata
82
+ provider_name: str = ""
83
+ model_name: str = ""
84
+ persona_name: str = ""
85
+ operation_id: str = ""
86
+ count_method: TokenCountMethod = TokenCountMethod.ESTIMATED
87
+ timestamp: datetime = field(default_factory=datetime.now)
88
+
89
+ # Cost information
90
+ estimated_cost: float = 0.0
91
+ cost_currency: str = "USD"
92
+
93
+ # Efficiency metrics
94
+ compression_ratio: float = 0.0 # How much input was compressed
95
+ response_efficiency: float = 0.0 # Useful output / total output
96
+ context_utilization: float = 0.0 # How much context was actually used
97
+
98
+
99
+ @dataclass
100
+ class UsagePattern:
101
+ """Token usage pattern analysis."""
102
+
103
+ time_period: timedelta
104
+ total_requests: int
105
+ total_tokens: int
106
+ total_cost: float
107
+ average_tokens_per_request: float
108
+ average_cost_per_request: float
109
+ peak_usage_time: datetime
110
+ peak_tokens_per_minute: int
111
+
112
+ # Efficiency metrics
113
+ average_context_utilization: float = 0.0
114
+ average_response_efficiency: float = 0.0
115
+ cache_hit_rate: float = 0.0
116
+
117
+ # Provider breakdown
118
+ provider_breakdown: Dict[str, Dict[str, Any]] = field(default_factory=dict)
119
+ persona_breakdown: Dict[str, Dict[str, Any]] = field(default_factory=dict)
120
+
121
+
122
+ class CostCalculator:
123
+ """Calculates costs for different providers and models."""
124
+
125
+ def __init__(self):
126
+ """Initialize with default cost information."""
127
+ self.cost_data: Dict[str, TokenCost] = {}
128
+ self._initialize_default_costs()
129
+
130
+ def _initialize_default_costs(self) -> None:
131
+ """Initialize with known provider costs (as of 2024)."""
132
+ # Claude costs (Anthropic)
133
+ self.cost_data["claude:claude-3-5-sonnet-20241022"] = TokenCost(
134
+ provider_name="claude",
135
+ model_name="claude-3-5-sonnet-20241022",
136
+ input_cost_per_1k=3.00,
137
+ output_cost_per_1k=15.00,
138
+ )
139
+ self.cost_data["claude:claude-3-haiku-20240307"] = TokenCost(
140
+ provider_name="claude",
141
+ model_name="claude-3-haiku-20240307",
142
+ input_cost_per_1k=0.25,
143
+ output_cost_per_1k=1.25,
144
+ )
145
+
146
+ # OpenAI costs
147
+ self.cost_data["openai:gpt-4o"] = TokenCost(
148
+ provider_name="openai",
149
+ model_name="gpt-4o",
150
+ input_cost_per_1k=5.00,
151
+ output_cost_per_1k=15.00,
152
+ cached_cost_per_1k=2.50, # OpenAI has cached pricing
153
+ )
154
+ self.cost_data["openai:gpt-4o-mini"] = TokenCost(
155
+ provider_name="openai",
156
+ model_name="gpt-4o-mini",
157
+ input_cost_per_1k=0.15,
158
+ output_cost_per_1k=0.60,
159
+ cached_cost_per_1k=0.075,
160
+ )
161
+
162
+ # Google Gemini costs
163
+ self.cost_data["gemini:gemini-1.5-pro"] = TokenCost(
164
+ provider_name="gemini",
165
+ model_name="gemini-1.5-pro",
166
+ input_cost_per_1k=3.50,
167
+ output_cost_per_1k=10.50,
168
+ )
169
+
170
+ # Default fallback for unknown models
171
+ self.cost_data["default"] = TokenCost(
172
+ provider_name="unknown",
173
+ model_name="unknown",
174
+ input_cost_per_1k=2.00,
175
+ output_cost_per_1k=6.00,
176
+ )
177
+
178
+ def get_cost_info(self, provider_name: str, model_name: str) -> TokenCost:
179
+ """Get cost information for a provider and model."""
180
+ key = f"{provider_name}:{model_name}"
181
+ return self.cost_data.get(key, self.cost_data["default"])
182
+
183
+ def calculate_cost(
184
+ self,
185
+ provider_name: str,
186
+ model_name: str,
187
+ input_tokens: int,
188
+ output_tokens: int,
189
+ cached_tokens: int = 0,
190
+ ) -> float:
191
+ """Calculate cost for token usage."""
192
+ cost_info = self.get_cost_info(provider_name, model_name)
193
+ return cost_info.calculate_cost(input_tokens, output_tokens, cached_tokens)
194
+
195
+ def update_cost_data(
196
+ self, provider_name: str, model_name: str, cost_info: TokenCost
197
+ ) -> None:
198
+ """Update cost information for a provider/model."""
199
+ key = f"{provider_name}:{model_name}"
200
+ self.cost_data[key] = cost_info
201
+ logger.info(f"Updated cost data for {key}")
202
+
203
+
204
+ class TokenEstimator:
205
+ """Estimates token counts using various methods."""
206
+
207
+ def __init__(self):
208
+ """Initialize token estimator."""
209
+ self._tiktoken_encoding = None
210
+ self._initialize_tiktoken()
211
+
212
+ def _initialize_tiktoken(self) -> None:
213
+ """Initialize tiktoken for OpenAI-style token counting."""
214
+ try:
215
+ import tiktoken
216
+
217
+ self._tiktoken_encoding = tiktoken.get_encoding(
218
+ "cl100k_base"
219
+ ) # GPT-4 encoding
220
+ except ImportError:
221
+ logger.warning(
222
+ "tiktoken not available, falling back to character-based estimation"
223
+ )
224
+ self._tiktoken_encoding = None
225
+
226
+ def estimate_tokens(
227
+ self, text: str, method: TokenCountMethod = TokenCountMethod.ESTIMATED
228
+ ) -> int:
229
+ """Estimate token count for text."""
230
+ if method == TokenCountMethod.TIKTOKEN and self._tiktoken_encoding:
231
+ return len(self._tiktoken_encoding.encode(text))
232
+ elif method == TokenCountMethod.ESTIMATED:
233
+ # Rough estimation: ~4 characters per token for English text
234
+ return max(1, len(text) // 4)
235
+ else:
236
+ return max(1, len(text) // 4) # Fallback to estimation
237
+
238
+ def estimate_tokens_by_provider(
239
+ self, text: str, provider_name: str, model_name: str
240
+ ) -> int:
241
+ """Estimate tokens using provider-specific logic."""
242
+ # Provider-specific estimation logic can be added here
243
+ base_estimate = self.estimate_tokens(text)
244
+
245
+ # Adjust for provider differences
246
+ if provider_name == "claude":
247
+ # Claude tends to count slightly fewer tokens
248
+ return int(base_estimate * 0.95)
249
+ elif provider_name == "openai":
250
+ # Use tiktoken if available
251
+ if self._tiktoken_encoding:
252
+ return len(self._tiktoken_encoding.encode(text))
253
+ elif provider_name == "gemini":
254
+ # Gemini uses different tokenization
255
+ return int(base_estimate * 1.05)
256
+
257
+ return base_estimate
258
+
259
+
260
+ class TokenUsageTracker:
261
+ """Advanced token usage tracking with pattern analysis."""
262
+
263
+ def __init__(
264
+ self,
265
+ storage_path: Optional[Path] = None,
266
+ max_history_size: int = 50000,
267
+ ):
268
+ """Initialize the token usage tracker."""
269
+ self.storage_path = storage_path or Path.home() / ".omnimancer" / "token_usage"
270
+ self.storage_path.mkdir(parents=True, exist_ok=True)
271
+
272
+ self.max_history_size = max_history_size
273
+ self.usage_history: deque = deque(maxlen=max_history_size)
274
+ self.cost_calculator = CostCalculator()
275
+ self.token_estimator = TokenEstimator()
276
+
277
+ # Current session tracking
278
+ self.session_usage: Dict[str, Dict[str, Any]] = defaultdict(
279
+ lambda: {"tokens": 0, "cost": 0.0, "requests": 0}
280
+ )
281
+
282
+ self._lock = threading.RLock()
283
+ logger.info("Token usage tracker initialized")
284
+
285
+ def record_usage(
286
+ self,
287
+ provider_name: str,
288
+ model_name: str,
289
+ input_tokens: int,
290
+ output_tokens: int,
291
+ cached_tokens: int = 0,
292
+ persona_name: Optional[str] = None,
293
+ operation_id: Optional[str] = None,
294
+ context_utilization: float = 0.0,
295
+ response_efficiency: float = 0.0,
296
+ ) -> DetailedTokenUsage:
297
+ """Record detailed token usage."""
298
+ with self._lock:
299
+ # Calculate cost
300
+ estimated_cost = self.cost_calculator.calculate_cost(
301
+ provider_name,
302
+ model_name,
303
+ input_tokens,
304
+ output_tokens,
305
+ cached_tokens,
306
+ )
307
+
308
+ # Create detailed usage record
309
+ usage = DetailedTokenUsage(
310
+ total_tokens=input_tokens + output_tokens,
311
+ input_tokens=input_tokens,
312
+ output_tokens=output_tokens,
313
+ cached_tokens=cached_tokens,
314
+ provider_name=provider_name,
315
+ model_name=model_name,
316
+ persona_name=persona_name or "unknown",
317
+ operation_id=operation_id or "",
318
+ estimated_cost=estimated_cost,
319
+ context_utilization=context_utilization,
320
+ response_efficiency=response_efficiency,
321
+ count_method=TokenCountMethod.API_REPORTED,
322
+ )
323
+
324
+ # Store in history
325
+ self.usage_history.append(usage)
326
+
327
+ # Update session tracking
328
+ session_key = f"{provider_name}:{model_name}"
329
+ self.session_usage[session_key]["tokens"] += usage.total_tokens
330
+ self.session_usage[session_key]["cost"] += estimated_cost
331
+ self.session_usage[session_key]["requests"] += 1
332
+
333
+ logger.debug(
334
+ f"Recorded {usage.total_tokens} tokens for {provider_name}:{model_name} (${estimated_cost:.4f})"
335
+ )
336
+ return usage
337
+
338
+ def estimate_and_record_usage(
339
+ self,
340
+ provider_name: str,
341
+ model_name: str,
342
+ input_text: str,
343
+ output_text: str,
344
+ persona_name: Optional[str] = None,
345
+ operation_id: Optional[str] = None,
346
+ ) -> DetailedTokenUsage:
347
+ """Estimate and record token usage from text."""
348
+ input_tokens = self.token_estimator.estimate_tokens_by_provider(
349
+ input_text, provider_name, model_name
350
+ )
351
+ output_tokens = self.token_estimator.estimate_tokens_by_provider(
352
+ output_text, provider_name, model_name
353
+ )
354
+
355
+ usage = self.record_usage(
356
+ provider_name=provider_name,
357
+ model_name=model_name,
358
+ input_tokens=input_tokens,
359
+ output_tokens=output_tokens,
360
+ persona_name=persona_name,
361
+ operation_id=operation_id,
362
+ )
363
+ usage.count_method = TokenCountMethod.ESTIMATED
364
+ return usage
365
+
366
+ def get_usage_summary(
367
+ self, time_window: Optional[timedelta] = None
368
+ ) -> Dict[str, Any]:
369
+ """Get comprehensive usage summary."""
370
+ with self._lock:
371
+ if not self.usage_history:
372
+ return {
373
+ "total_tokens": 0,
374
+ "total_cost": 0.0,
375
+ "total_requests": 0,
376
+ }
377
+
378
+ # Filter by time window
379
+ cutoff_time = datetime.now() - time_window if time_window else None
380
+ relevant_usage = [
381
+ usage
382
+ for usage in self.usage_history
383
+ if not cutoff_time or usage.timestamp >= cutoff_time
384
+ ]
385
+
386
+ if not relevant_usage:
387
+ return {
388
+ "total_tokens": 0,
389
+ "total_cost": 0.0,
390
+ "total_requests": 0,
391
+ }
392
+
393
+ # Calculate totals
394
+ total_tokens = sum(u.total_tokens for u in relevant_usage)
395
+ total_cost = sum(u.estimated_cost for u in relevant_usage)
396
+ total_requests = len(relevant_usage)
397
+
398
+ # Provider breakdown
399
+ provider_stats = defaultdict(
400
+ lambda: {
401
+ "tokens": 0,
402
+ "cost": 0.0,
403
+ "requests": 0,
404
+ "models": set(),
405
+ }
406
+ )
407
+ for usage in relevant_usage:
408
+ key = usage.provider_name
409
+ provider_stats[key]["tokens"] += usage.total_tokens
410
+ provider_stats[key]["cost"] += usage.estimated_cost
411
+ provider_stats[key]["requests"] += 1
412
+ provider_stats[key]["models"].add(usage.model_name)
413
+
414
+ # Convert sets to lists for JSON serialization
415
+ for stats in provider_stats.values():
416
+ stats["models"] = list(stats["models"])
417
+
418
+ # Persona breakdown
419
+ persona_stats = defaultdict(
420
+ lambda: {"tokens": 0, "cost": 0.0, "requests": 0}
421
+ )
422
+ for usage in relevant_usage:
423
+ if usage.persona_name:
424
+ persona_stats[usage.persona_name]["tokens"] += usage.total_tokens
425
+ persona_stats[usage.persona_name]["cost"] += usage.estimated_cost
426
+ persona_stats[usage.persona_name]["requests"] += 1
427
+
428
+ # Efficiency metrics
429
+ efficiency_scores = [
430
+ u.context_utilization
431
+ for u in relevant_usage
432
+ if u.context_utilization > 0
433
+ ]
434
+ response_scores = [
435
+ u.response_efficiency
436
+ for u in relevant_usage
437
+ if u.response_efficiency > 0
438
+ ]
439
+
440
+ return {
441
+ "total_tokens": total_tokens,
442
+ "total_cost": total_cost,
443
+ "total_requests": total_requests,
444
+ "average_tokens_per_request": (
445
+ total_tokens / total_requests if total_requests > 0 else 0
446
+ ),
447
+ "average_cost_per_request": (
448
+ total_cost / total_requests if total_requests > 0 else 0
449
+ ),
450
+ "provider_breakdown": dict(provider_stats),
451
+ "persona_breakdown": dict(persona_stats),
452
+ "average_context_utilization": (
453
+ statistics.mean(efficiency_scores) if efficiency_scores else 0
454
+ ),
455
+ "average_response_efficiency": (
456
+ statistics.mean(response_scores) if response_scores else 0
457
+ ),
458
+ "time_period": (
459
+ f"{time_window.total_seconds():.0f} seconds"
460
+ if time_window
461
+ else "all time"
462
+ ),
463
+ "period_start": (cutoff_time.isoformat() if cutoff_time else None),
464
+ "period_end": datetime.now().isoformat(),
465
+ }
466
+
467
+ def analyze_usage_patterns(self, days_back: int = 7) -> UsagePattern:
468
+ """Analyze usage patterns over a period."""
469
+ with self._lock:
470
+ time_period = timedelta(days=days_back)
471
+ cutoff_time = datetime.now() - time_period
472
+
473
+ relevant_usage = [
474
+ usage for usage in self.usage_history if usage.timestamp >= cutoff_time
475
+ ]
476
+
477
+ if not relevant_usage:
478
+ return UsagePattern(
479
+ time_period=time_period,
480
+ total_requests=0,
481
+ total_tokens=0,
482
+ total_cost=0.0,
483
+ average_tokens_per_request=0,
484
+ average_cost_per_request=0,
485
+ peak_usage_time=datetime.now(),
486
+ peak_tokens_per_minute=0,
487
+ )
488
+
489
+ # Basic metrics
490
+ total_tokens = sum(u.total_tokens for u in relevant_usage)
491
+ total_cost = sum(u.estimated_cost for u in relevant_usage)
492
+ total_requests = len(relevant_usage)
493
+
494
+ # Find peak usage
495
+ # Group by hour and find the hour with most tokens
496
+ hourly_usage = defaultdict(int)
497
+ for usage in relevant_usage:
498
+ hour_key = usage.timestamp.replace(minute=0, second=0, microsecond=0)
499
+ hourly_usage[hour_key] += usage.total_tokens
500
+
501
+ peak_hour = (
502
+ max(hourly_usage.keys(), key=hourly_usage.get)
503
+ if hourly_usage
504
+ else datetime.now()
505
+ )
506
+ peak_tokens_per_hour = hourly_usage[peak_hour] if hourly_usage else 0
507
+
508
+ # Provider breakdown
509
+ provider_breakdown = defaultdict(
510
+ lambda: {"tokens": 0, "cost": 0.0, "requests": 0}
511
+ )
512
+ for usage in relevant_usage:
513
+ key = usage.provider_name
514
+ provider_breakdown[key]["tokens"] += usage.total_tokens
515
+ provider_breakdown[key]["cost"] += usage.estimated_cost
516
+ provider_breakdown[key]["requests"] += 1
517
+
518
+ # Persona breakdown
519
+ persona_breakdown = defaultdict(
520
+ lambda: {"tokens": 0, "cost": 0.0, "requests": 0}
521
+ )
522
+ for usage in relevant_usage:
523
+ key = usage.persona_name or "unknown"
524
+ persona_breakdown[key]["tokens"] += usage.total_tokens
525
+ persona_breakdown[key]["cost"] += usage.estimated_cost
526
+ persona_breakdown[key]["requests"] += 1
527
+
528
+ # Efficiency metrics
529
+ efficiency_scores = [
530
+ u.context_utilization
531
+ for u in relevant_usage
532
+ if u.context_utilization > 0
533
+ ]
534
+ response_scores = [
535
+ u.response_efficiency
536
+ for u in relevant_usage
537
+ if u.response_efficiency > 0
538
+ ]
539
+ cached_requests = len([u for u in relevant_usage if u.cached_tokens > 0])
540
+
541
+ return UsagePattern(
542
+ time_period=time_period,
543
+ total_requests=total_requests,
544
+ total_tokens=total_tokens,
545
+ total_cost=total_cost,
546
+ average_tokens_per_request=(
547
+ total_tokens / total_requests if total_requests > 0 else 0
548
+ ),
549
+ average_cost_per_request=(
550
+ total_cost / total_requests if total_requests > 0 else 0
551
+ ),
552
+ peak_usage_time=peak_hour,
553
+ peak_tokens_per_minute=int(peak_tokens_per_hour / 60),
554
+ average_context_utilization=(
555
+ statistics.mean(efficiency_scores) if efficiency_scores else 0
556
+ ),
557
+ average_response_efficiency=(
558
+ statistics.mean(response_scores) if response_scores else 0
559
+ ),
560
+ cache_hit_rate=(
561
+ (cached_requests / total_requests * 100)
562
+ if total_requests > 0
563
+ else 0
564
+ ),
565
+ provider_breakdown=dict(provider_breakdown),
566
+ persona_breakdown=dict(persona_breakdown),
567
+ )
568
+
569
+ def get_cost_optimization_suggestions(self) -> List[Dict[str, Any]]:
570
+ """Get cost optimization suggestions based on usage patterns."""
571
+ suggestions = []
572
+
573
+ # Analyze recent usage (last 24 hours)
574
+ recent_summary = self.get_usage_summary(timedelta(hours=24))
575
+
576
+ # Suggest provider switching if using expensive models heavily
577
+ provider_breakdown = recent_summary.get("provider_breakdown", {})
578
+ for provider, stats in provider_breakdown.items():
579
+ if stats["cost"] > 5.0: # More than $5 in 24 hours
580
+ suggestions.append(
581
+ {
582
+ "type": "provider_switching",
583
+ "title": f"High cost on {provider}",
584
+ "description": f"${stats['cost']:.2f} spent on {provider} in 24 hours",
585
+ "suggestion": "Consider switching to a more cost-effective model for routine tasks",
586
+ "potential_savings": "30-70%",
587
+ }
588
+ )
589
+
590
+ # Suggest caching for repeated patterns
591
+ if recent_summary.get("total_requests", 0) > 100:
592
+ avg_cost = recent_summary.get("average_cost_per_request", 0)
593
+ if avg_cost > 0.01: # More than 1 cent per request
594
+ suggestions.append(
595
+ {
596
+ "type": "response_caching",
597
+ "title": "High request frequency detected",
598
+ "description": f"{recent_summary['total_requests']} requests with avg cost ${avg_cost:.3f}",
599
+ "suggestion": "Implement response caching for similar queries",
600
+ "potential_savings": "20-50%",
601
+ }
602
+ )
603
+
604
+ return suggestions
605
+
606
+ def save_usage_history(self, filename: Optional[str] = None) -> None:
607
+ """Save usage history to disk."""
608
+ if not filename:
609
+ filename = f"token_usage_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
610
+
611
+ filepath = self.storage_path / filename
612
+
613
+ with self._lock:
614
+ usage_data = [asdict(usage) for usage in self.usage_history]
615
+
616
+ with open(filepath, "w") as f:
617
+ json.dump(
618
+ {
619
+ "metadata": {
620
+ "export_time": datetime.now().isoformat(),
621
+ "total_records": len(usage_data),
622
+ "tracker_version": "1.0",
623
+ },
624
+ "usage_history": usage_data,
625
+ },
626
+ f,
627
+ indent=2,
628
+ default=str,
629
+ )
630
+
631
+ logger.info(f"Saved {len(usage_data)} usage records to {filepath}")
632
+
633
+ def load_usage_history(self, filename: str) -> None:
634
+ """Load usage history from disk."""
635
+ filepath = self.storage_path / filename
636
+
637
+ if not filepath.exists():
638
+ logger.error(f"Usage history file not found: {filepath}")
639
+ return
640
+
641
+ with open(filepath, "r") as f:
642
+ data = json.load(f)
643
+
644
+ with self._lock:
645
+ for usage_dict in data.get("usage_history", []):
646
+ # Convert timestamp back to datetime
647
+ if isinstance(usage_dict.get("timestamp"), str):
648
+ usage_dict["timestamp"] = datetime.fromisoformat(
649
+ usage_dict["timestamp"]
650
+ )
651
+
652
+ usage = DetailedTokenUsage(**usage_dict)
653
+ self.usage_history.append(usage)
654
+
655
+ logger.info(
656
+ f"Loaded {len(data.get('usage_history', []))} usage records from {filepath}"
657
+ )
658
+
659
+
660
+ # Global token tracker instance
661
+ _token_tracker: Optional[TokenUsageTracker] = None
662
+ _tracker_lock = threading.Lock()
663
+
664
+
665
+ def get_token_tracker() -> TokenUsageTracker:
666
+ """Get the global token tracker instance."""
667
+ global _token_tracker
668
+ if _token_tracker is None:
669
+ with _tracker_lock:
670
+ if _token_tracker is None:
671
+ _token_tracker = TokenUsageTracker()
672
+ return _token_tracker
673
+
674
+
675
+ def set_token_tracker(tracker: TokenUsageTracker) -> None:
676
+ """Set the global token tracker instance."""
677
+ global _token_tracker
678
+ with _tracker_lock:
679
+ _token_tracker = tracker