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
113 lines
4.8 KiB
Python
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()
|