Files
ROMFASTSQL/tools/claude_usage_extract.py
Claude Agent a01cac0657 feat(analiza): consum real Claude Code si dimensionarea suplimentului GLM
Plafonul general saptamanal Max 5x se consuma in ~2 zile, urmate de ~5 zile
blocate. Masurat pe 3 masini (Windows, claude-agent, moltbot), 2026-07-29 ->
2026-09-01: 11,15 miliarde tokeni, 435 sesiuni, 64.211 cereri API, dedup pe
message.id + requestId. Deficit +32% (~570 prompturi/saptamana).

Concluzia nu e cea asteptata: bugetele plafonate in dolari (OpenCode Zen $20,
OpenCode Go) se evapora la acest volum -- Go a tinut 1-2 zile in practica.
Planurile Z.AI se contorizeaza in prompturi, nu in dolari, si rezista la un
workload cu 7,1 cereri API per prompt. Z.AI Coding Lite ($18/luna) acopera 70%
din deficit; Pro ($72) il acopera integral.

Verificat empiric ca subagentii nu pot rula pe GLM (in proces, moștenesc auth),
dar sesiunile separate pot: endpoint fals local a primit cererea cu tokenul
alternativ, fara sa atinga OAuth-ul Max. Masurat: ~186MB RSS si 20-60s pornire
per sesiune -- de aici regulile de orchestrare (sarcini mari, max 4 procese,
fan-out in interiorul unei sesiuni GLM).

Ramane neverificat multiplicatorul de credite pentru GLM-5.3-Flash; la 3x in
loc de 1x, Lite pica si decizia se muta pe Pro.

- docs/supliment-glm-zai.md: rezumat de implementare (indexat in CLAUDE.md)
- claude_usage_report.md + CSV-uri: analiza completa si datele brute
- tools/claude_usage_*.py: pipeline de masurare read-only, re-rulabil remote
- tools/claude-glm.sh: wrapper de sesiune GLM (task/resume/shell)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
2026-09-01 11:32:56 +00:00

113 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Extrage token usage real din transcripturile Claude Code (~/.claude/projects/**/*.jsonl).
READ-ONLY: nu scrie, nu mută, nu șterge nimic. Emite JSON agregat pe stdout.
Poate fi rulat remote fără a atinge discul: ssh host 'python3 -' < acest_fisier
Deduplicare: cheia (message.id, requestId) — aceeași cerere API poate apărea în
mai multe fișiere (resume, fork de sesiune, backup-uri).
"""
import json, os, sys, glob, argparse, hashlib
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--root", default=os.path.expanduser("~/.claude/projects"))
ap.add_argument("--machine", default=os.uname().nodename)
a = ap.parse_args()
rows = {} # (session, date, model) -> tokens
sessions = {} # session -> meta
seen = set() # chei de deduplicare
dup = 0
total_assistant = 0
files = sorted(glob.glob(os.path.join(a.root, "**", "*.jsonl"), recursive=True))
bad_lines = 0
for fp in files:
project = os.path.basename(os.path.dirname(fp))
try:
fh = open(fp, "r", encoding="utf-8", errors="replace")
except OSError:
continue
with fh:
for line in fh:
line = line.strip()
if not line or not line.startswith("{"):
continue
try:
d = json.loads(line)
except Exception:
bad_lines += 1
continue
t = d.get("type")
sid = d.get("sessionId")
if not sid:
continue
ts = d.get("timestamp") or ""
s = sessions.setdefault(sid, {
"machine": a.machine, "project": project, "first_ts": ts, "last_ts": ts,
"user_msgs": 0, "assistant_msgs": 0, "models": set(),
"version": d.get("version"), "entrypoint": d.get("entrypoint"),
"cwd": d.get("cwd"), "files": set(),
})
s["files"].add(os.path.basename(fp))
if ts:
if not s["first_ts"] or ts < s["first_ts"]: s["first_ts"] = ts
if ts > s["last_ts"]: s["last_ts"] = ts
if t == "user" and not d.get("isSidechain"):
s["user_msgs"] += 1
if t != "assistant":
continue
msg = d.get("message") or {}
u = msg.get("usage")
if not isinstance(u, dict):
continue
total_assistant += 1
# cheie de deduplicare
mid = msg.get("id"); rid = d.get("requestId")
if mid or rid:
key = f"{mid}|{rid}"
else:
key = "h:" + hashlib.sha1((sid + ts + str(u.get("output_tokens"))).encode()).hexdigest()
if key in seen:
dup += 1
continue
seen.add(key)
model = msg.get("model") or "unknown"
if model.startswith("<") or model == "unknown":
continue # synthetic / mesaje locale fără cost
s["assistant_msgs"] += 1
s["models"].add(model)
date = ts[:10] if ts else "unknown"
k = (sid, date, model)
r = rows.setdefault(k, {"input": 0, "output": 0, "cache_creation": 0,
"cache_read": 0, "thinking": 0, "turns": 0,
"web_search": 0, "web_fetch": 0})
r["input"] += u.get("input_tokens") or 0
r["output"] += u.get("output_tokens") or 0
r["cache_creation"] += u.get("cache_creation_input_tokens") or 0
r["cache_read"] += u.get("cache_read_input_tokens") or 0
otd = u.get("output_tokens_details") or {}
r["thinking"] += otd.get("thinking_tokens") or 0
stu = u.get("server_tool_use") or {}
r["web_search"] += stu.get("web_search_requests") or 0
r["web_fetch"] += stu.get("web_fetch_requests") or 0
r["turns"] += 1
out = {
"machine": a.machine,
"root": a.root,
"files_scanned": len(files),
"assistant_records_with_usage": total_assistant,
"duplicates_removed": dup,
"malformed_lines": bad_lines,
"rows": [{"session": k[0], "date": k[1], "model": k[2], **v} for k, v in rows.items()],
"sessions": {sid: {**{kk: vv for kk, vv in m.items() if kk not in ("models", "files")},
"models": sorted(m["models"]), "files": len(m["files"])}
for sid, m in sessions.items()},
}
json.dump(out, sys.stdout)
main()