#!/usr/bin/env python3 """ token_budget.py — Daily token budget tracker for pipeline orchestration. Tracks token usage per pipeline per day, enforces daily limits, and provides a query interface for the orchestrator. Data: ~/.hermes/pipeline_budget.json """ import json import os from datetime import datetime, timezone from pathlib import Path BUDGET_FILE = Path.home() / ".hermes" / "pipeline_budget.json" DEFAULT_DAILY_LIMIT = 500_000 def _load() -> dict: if BUDGET_FILE.exists(): try: return json.loads(BUDGET_FILE.read_text()) except (json.JSONDecodeError, OSError): pass return {} def _save(data: dict): BUDGET_FILE.parent.mkdir(parents=True, exist_ok=True) BUDGET_FILE.write_text(json.dumps(data, indent=2)) def today_key() -> str: return datetime.now(timezone.utc).strftime("%Y-%m-%d") def get_daily_usage(pipeline: str = None) -> dict: """Get token usage for today. If pipeline specified, return just that pipeline.""" data = _load() day = data.get("daily", {}).get(today_key(), {"tokens_used": 0, "pipelines": {}}) if pipeline: return { "pipeline": pipeline, "tokens_used": day.get("pipelines", {}).get(pipeline, 0), "daily_total": day.get("tokens_used", 0), } return day def get_remaining(limit: int = DEFAULT_DAILY_LIMIT) -> int: """Get remaining token budget for today.""" usage = get_daily_usage() return max(0, limit - usage.get("tokens_used", 0)) def can_afford(tokens: int, limit: int = DEFAULT_DAILY_LIMIT) -> bool: """Check if we have budget for a token spend.""" return get_remaining(limit) >= tokens def record_usage(pipeline: str, input_tokens: int, output_tokens: int) -> dict: """ Record token usage for a pipeline task. Called automatically by the orchestrator after each pipeline task completes. Returns the updated daily state. """ total = input_tokens + output_tokens data = _load() today = today_key() daily = data.setdefault("daily", {}) day = daily.setdefault(today, {"tokens_used": 0, "pipelines": {}}) day["tokens_used"] = day.get("tokens_used", 0) + total pipes = day.setdefault("pipelines", {}) pipes[pipeline] = pipes.get(pipeline, 0) + total # Track breakdown breakdown = day.setdefault("breakdown", {}) pb = breakdown.setdefault(pipeline, {"input": 0, "output": 0, "calls": 0}) pb["input"] += input_tokens pb["output"] += output_tokens pb["calls"] += 1 # Track lifetime stats lifetime = data.setdefault("lifetime", {"total_tokens": 0, "total_days": 0}) lifetime["total_tokens"] = lifetime.get("total_tokens", 0) + total _save(data) return { "pipeline": pipeline, "input_tokens": input_tokens, "output_tokens": output_tokens, "total": total, "daily_used": day["tokens_used"], "daily_remaining": get_remaining(), } def get_report() -> str: """Generate a human-readable budget report.""" data = _load() today = today_key() day = data.get("daily", {}).get(today, {"tokens_used": 0, "pipelines": {}}) lines = [] lines.append(f"Token Budget — {today}") lines.append(f" Daily usage: {day.get('tokens_used', 0):,} / {DEFAULT_DAILY_LIMIT:,}") lines.append(f" Remaining: {get_remaining():,}") lines.append("") lines.append(" Pipelines:") breakdown = day.get("breakdown", {}) for name, stats in sorted(breakdown.items(), key=lambda x: -x[1]["output"]): total = stats["input"] + stats["output"] lines.append(f" {name}: {total:,} tokens ({stats['calls']} calls)") if not breakdown: lines.append(" (no pipelines run today)") lifetime = data.get("lifetime", {}) lines.append("") lines.append(f" Lifetime: {lifetime.get('total_tokens', 0):,} total tokens") return "\n".join(lines) if __name__ == "__main__": import sys if "--report" in sys.argv: print(get_report()) elif "--remaining" in sys.argv: print(get_remaining()) elif "--can-afford" in sys.argv: idx = sys.argv.index("--can-afford") tokens = int(sys.argv[idx + 1]) print("yes" if can_afford(tokens) else "no") else: print(get_report())