157 lines
4.8 KiB
Python
157 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Token usage tracker with budget alerts.
|
|
Monitors API usage and warns when approaching limits.
|
|
"""
|
|
import json
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
STATE_FILE = Path.home() / ".openclaw/workspace/memory/token-tracker-state.json"
|
|
|
|
def load_state():
|
|
"""Load tracking state from file."""
|
|
if STATE_FILE.exists():
|
|
with open(STATE_FILE, 'r') as f:
|
|
return json.load(f)
|
|
return {
|
|
"daily_usage": {},
|
|
"alerts_sent": [],
|
|
"last_reset": datetime.now().isoformat()
|
|
}
|
|
|
|
def save_state(state):
|
|
"""Save tracking state to file."""
|
|
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(STATE_FILE, 'w') as f:
|
|
json.dump(state, f, indent=2)
|
|
|
|
def get_usage_from_session_status():
|
|
"""Parse session status to extract token usage.
|
|
Returns dict with input_tokens, output_tokens, and cost.
|
|
"""
|
|
# This would integrate with OpenClaw's session_status tool
|
|
# For now, returns placeholder structure
|
|
return {
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"total_cost": 0.0,
|
|
"model": "anthropic/claude-sonnet-4-5"
|
|
}
|
|
|
|
def check_budget(daily_limit_usd=5.0, warn_threshold=0.8):
|
|
"""Check if usage is approaching daily budget.
|
|
|
|
Args:
|
|
daily_limit_usd: Daily spending limit in USD
|
|
warn_threshold: Fraction of limit to trigger warning (default 80%)
|
|
|
|
Returns:
|
|
dict with status, usage, limit, and alert message if applicable
|
|
"""
|
|
state = load_state()
|
|
today = datetime.now().date().isoformat()
|
|
|
|
# Reset if new day
|
|
if today not in state["daily_usage"]:
|
|
state["daily_usage"] = {today: {"cost": 0.0, "tokens": 0}}
|
|
state["alerts_sent"] = []
|
|
|
|
usage = state["daily_usage"][today]
|
|
percent_used = (usage["cost"] / daily_limit_usd) * 100
|
|
|
|
result = {
|
|
"date": today,
|
|
"cost": usage["cost"],
|
|
"tokens": usage["tokens"],
|
|
"limit": daily_limit_usd,
|
|
"percent_used": percent_used,
|
|
"status": "ok"
|
|
}
|
|
|
|
# Check thresholds
|
|
if percent_used >= 100:
|
|
result["status"] = "exceeded"
|
|
result["alert"] = f"⚠️ Daily budget exceeded! ${usage['cost']:.2f} / ${daily_limit_usd:.2f}"
|
|
elif percent_used >= (warn_threshold * 100):
|
|
result["status"] = "warning"
|
|
result["alert"] = f"⚠️ Approaching daily limit: ${usage['cost']:.2f} / ${daily_limit_usd:.2f} ({percent_used:.0f}%)"
|
|
|
|
return result
|
|
|
|
def suggest_cheaper_model(current_model, task_type="general"):
|
|
"""Suggest cheaper alternative models based on task type.
|
|
|
|
Args:
|
|
current_model: Currently configured model
|
|
task_type: Type of task (general, simple, complex)
|
|
|
|
Returns:
|
|
dict with suggestion and cost savings
|
|
"""
|
|
# Cost per 1M tokens (input/output average)
|
|
model_costs = {
|
|
"anthropic/claude-opus-4": 15.0,
|
|
"anthropic/claude-sonnet-4-5": 3.0,
|
|
"anthropic/claude-haiku-4": 0.25,
|
|
"google/gemini-2.0-flash-exp": 0.075,
|
|
"openai/gpt-4o": 2.5,
|
|
"openai/gpt-4o-mini": 0.15
|
|
}
|
|
|
|
suggestions = {
|
|
"simple": [
|
|
("anthropic/claude-haiku-4", "12x cheaper, great for file reads, routine checks"),
|
|
("google/gemini-2.0-flash-exp", "40x cheaper via OpenRouter, good for simple tasks")
|
|
],
|
|
"general": [
|
|
("anthropic/claude-sonnet-4-5", "Balanced performance and cost"),
|
|
("google/gemini-2.0-flash-exp", "Much cheaper, decent quality")
|
|
],
|
|
"complex": [
|
|
("anthropic/claude-opus-4", "Best reasoning, use sparingly"),
|
|
("anthropic/claude-sonnet-4-5", "Good balance for most complex tasks")
|
|
]
|
|
}
|
|
|
|
return {
|
|
"current": current_model,
|
|
"current_cost": model_costs.get(current_model, "unknown"),
|
|
"suggestions": suggestions.get(task_type, suggestions["general"])
|
|
}
|
|
|
|
def main():
|
|
"""CLI interface for token tracker."""
|
|
import sys
|
|
|
|
if len(sys.argv) < 2:
|
|
print("Usage: token_tracker.py [check|suggest|reset]")
|
|
sys.exit(1)
|
|
|
|
command = sys.argv[1]
|
|
|
|
if command == "check":
|
|
result = check_budget()
|
|
print(json.dumps(result, indent=2))
|
|
|
|
elif command == "suggest":
|
|
task = sys.argv[2] if len(sys.argv) > 2 else "general"
|
|
current = sys.argv[3] if len(sys.argv) > 3 else "anthropic/claude-sonnet-4-5"
|
|
result = suggest_cheaper_model(current, task)
|
|
print(json.dumps(result, indent=2))
|
|
|
|
elif command == "reset":
|
|
state = load_state()
|
|
state["daily_usage"] = {}
|
|
state["alerts_sent"] = []
|
|
save_state(state)
|
|
print("Token tracker state reset.")
|
|
|
|
else:
|
|
print(f"Unknown command: {command}")
|
|
sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|