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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|