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
This commit is contained in:
112
tools/claude_usage_extract.py
Normal file
112
tools/claude_usage_extract.py
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user