"""Event-Ledger: jeder LLM-Call wird eine append-only-Zeile in `events`. Kennzahlen sind Queries darauf; das Budget zählt live mit.""" import subprocess import db from config import PROJECT_ROOT class BudgetErschoepft(Exception): pass def git_stand() -> tuple[str, bool]: """(hash, dirty) des Repos — fürs Lauf-Protokoll; Fehler → ('', False).""" try: h = subprocess.run(["git", "rev-parse", "HEAD"], cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5).stdout.strip() d = subprocess.run(["git", "status", "--porcelain"], cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=5).stdout.strip() return h, bool(d) except Exception: return "", False def log_call(run_id: int, *, ebene: str, stage: str, item: str = "", template: str = "", template_hash: str = "", provider: str = "", model: str = "", role: str = "", status: str, dur_ms: int = 0, wait_ms: int = 0, tokens: dict | None = None, meta: dict | None = None) -> None: t = tokens or {} db.execute( "INSERT INTO events(run_id, ebene, stage, item, template, template_hash, provider," " model, role, status, dur_ms, wait_ms, tok_in, tok_out, tok_cache_read, tok_cache_write, meta)" " VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (run_id, ebene, stage, item, template, template_hash, provider, model, role, status, dur_ms, wait_ms, int(t.get("input", 0)), int(t.get("output", 0)), int(t.get("cache_read", 0)), int(t.get("cache_write", 0)), db.j(meta or {}))) def verbraucht(run_id: int) -> int: row = db.one("SELECT COALESCE(SUM(tok_in+tok_out),0) AS s FROM events WHERE run_id=?", (run_id,)) return int(row["s"]) if row else 0 def budget_pruefen(run_id: int) -> None: """Nach jedem Call. Hartes Limit: wirft statt weiterzubrennen.""" run = db.one("SELECT budget_tokens FROM runs WHERE id=?", (run_id,)) limit = int(run["budget_tokens"]) if run else 0 if limit and verbraucht(run_id) >= limit: raise BudgetErschoepft(f"Token-Budget erreicht ({limit})") def kennzahlen(run_id: int) -> list[dict]: return db.query( "SELECT ebene, stage, template, COUNT(*) AS calls," " SUM(tok_in) AS tok_in, SUM(tok_out) AS tok_out," " SUM(tok_cache_read) AS cache_read, SUM(dur_ms) AS dur_ms, SUM(wait_ms) AS wait_ms," " SUM(status='ok') AS ok, SUM(status='timeout') AS timeouts," " SUM(status IN ('error','infra')) AS fehler" " FROM events WHERE run_id=? GROUP BY ebene, stage, template ORDER BY MIN(id)", (run_id,))