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
120 lines
6.3 KiB
Python
120 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Agregă rezultatele claude_usage_extract.py și generează raportul + CSV-urile."""
|
|
import json, csv, sys, os
|
|
from collections import defaultdict
|
|
|
|
OUT = sys.argv[1] if len(sys.argv) > 1 else "."
|
|
INPUTS = sys.argv[2:]
|
|
|
|
# Preturi USD / 1M tokeni. Verificate 2026-09-01.
|
|
PRICES = {
|
|
"GLM-5.3": {"in": 1.40, "cache_read": 0.26, "cache_write": None, "out": 4.40},
|
|
"GLM-5.3-Flash (promo)":{"in": 0.075, "cache_read": 0.015, "cache_write": None, "out": 0.25},
|
|
"GLM-5.3-Flash (list)": {"in": 0.15, "cache_read": 0.03, "cache_write": None, "out": 0.50},
|
|
# referinta: aceleasi modele Claude, dar platite la bucata pe Zen
|
|
"Zen Claude Opus 5": {"in": 5.00, "cache_read": 0.50, "cache_write": 6.25, "out": 25.00},
|
|
"Zen Claude Sonnet 5": {"in": 2.00, "cache_read": 0.20, "cache_write": 2.50, "out": 10.00},
|
|
"Zen Claude Haiku 4.5": {"in": 1.00, "cache_read": 0.10, "cache_write": 1.25, "out": 5.00},
|
|
"Zen Claude Fable 5": {"in": 10.00, "cache_read": 1.00, "cache_write": 12.50,"out": 50.00},
|
|
}
|
|
# mapare model observat -> tarif Zen pentru acelasi model (referinta "cat ar costa la bucata")
|
|
ZEN_SELF = {"claude-opus-5":"Zen Claude Opus 5", "claude-sonnet-5":"Zen Claude Sonnet 5",
|
|
"claude-haiku-4-5-20251001":"Zen Claude Haiku 4.5", "claude-fable-5":"Zen Claude Fable 5"}
|
|
|
|
rows, sessions, meta = [], {}, []
|
|
for p in INPUTS:
|
|
d = json.load(open(p))
|
|
meta.append({k: v for k, v in d.items() if k not in ("rows", "sessions")})
|
|
m = d["machine"]
|
|
for r in d["rows"]:
|
|
rows.append({**r, "machine": m})
|
|
for sid, s in d["sessions"].items():
|
|
sessions[(m, sid)] = s
|
|
|
|
# --- deduplicare cross-masina pe session id ---
|
|
by_sid = defaultdict(set)
|
|
for (m, sid) in sessions: by_sid[sid].add(m)
|
|
cross_dupes = {sid for sid, ms in by_sid.items() if len(ms) > 1}
|
|
|
|
def cost(tok, price, no_cache=False):
|
|
"""tok: dict cu input/output/cache_creation/cache_read. Returneaza breakdown USD."""
|
|
p = PRICES[price]
|
|
cw = p["cache_write"] if p["cache_write"] is not None else p["in"]
|
|
if no_cache:
|
|
# Scenariul B: fara avantajul cache-ului -> tot contextul reincarcat ca input normal
|
|
return {"input": (tok["input"] + tok["cache_read"] + tok["cache_creation"]) / 1e6 * p["in"],
|
|
"cache_read": 0.0, "cache_write": 0.0,
|
|
"output": tok["output"] / 1e6 * p["out"]}
|
|
return {"input": tok["input"] / 1e6 * p["in"],
|
|
"cache_read": tok["cache_read"] / 1e6 * p["cache_read"],
|
|
"cache_write": tok["cache_creation"] / 1e6 * cw,
|
|
"output": tok["output"] / 1e6 * p["out"]}
|
|
|
|
def zero(): return {"input": 0, "output": 0, "cache_creation": 0, "cache_read": 0, "turns": 0, "thinking": 0}
|
|
def add(a, b):
|
|
for k in a: a[k] += b.get(k, 0)
|
|
return a
|
|
|
|
TOT = zero()
|
|
by_model, by_machine, by_month, by_day, by_session = (defaultdict(zero) for _ in range(5))
|
|
sess_machine, sess_models = {}, defaultdict(set)
|
|
for r in rows:
|
|
add(TOT, r)
|
|
add(by_model[r["model"]], r); add(by_machine[r["machine"]], r)
|
|
add(by_month[r["date"][:7]], r); add(by_day[r["date"]], r)
|
|
add(by_session[r["session"]], r)
|
|
sess_machine[r["session"]] = r["machine"]; sess_models[r["session"]].add(r["model"])
|
|
|
|
def tot(t): return t["input"] + t["output"] + t["cache_creation"] + t["cache_read"]
|
|
|
|
days = sorted(by_day)
|
|
import datetime
|
|
d0 = datetime.date.fromisoformat(days[0]); d1 = datetime.date.fromisoformat(days[-1])
|
|
span = (d1 - d0).days + 1
|
|
missing = [ (d0 + datetime.timedelta(i)).isoformat() for i in range(span)
|
|
if (d0 + datetime.timedelta(i)).isoformat() not in by_day ]
|
|
|
|
# ---------------- CSV sesiuni ----------------
|
|
with open(os.path.join(OUT, "claude_usage_sessions.csv"), "w", newline="") as f:
|
|
w = csv.writer(f)
|
|
w.writerow(["session_id","machine","project","models","first_ts","last_ts","user_turns",
|
|
"assistant_turns","input_tokens","output_tokens","cache_creation_tokens",
|
|
"cache_read_tokens","total_tokens","cost_glm53_usd","cost_glm53flash_promo_usd"])
|
|
for sid, t in sorted(by_session.items(), key=lambda kv: -tot(kv[1])):
|
|
m = sess_machine[sid]; s = sessions.get((m, sid), {})
|
|
w.writerow([sid, m, s.get("project",""), "|".join(sorted(sess_models[sid])),
|
|
s.get("first_ts",""), s.get("last_ts",""), s.get("user_msgs",""), t["turns"],
|
|
t["input"], t["output"], t["cache_creation"], t["cache_read"], tot(t),
|
|
round(sum(cost(t,"GLM-5.3").values()),6),
|
|
round(sum(cost(t,"GLM-5.3-Flash (promo)").values()),6)])
|
|
|
|
# ---------------- CSV summary ----------------
|
|
with open(os.path.join(OUT, "claude_usage_summary.csv"), "w", newline="") as f:
|
|
w = csv.writer(f)
|
|
w.writerow(["dimension","key","sessions","turns","input_tokens","output_tokens",
|
|
"cache_creation_tokens","cache_read_tokens","total_tokens",
|
|
"cost_glm53_usd","cost_glm53flash_promo_usd","cost_glm53flash_list_usd"])
|
|
def emit(dim, key, t, nsess=""):
|
|
w.writerow([dim, key, nsess, t["turns"], t["input"], t["output"], t["cache_creation"],
|
|
t["cache_read"], tot(t),
|
|
round(sum(cost(t,"GLM-5.3").values()),4),
|
|
round(sum(cost(t,"GLM-5.3-Flash (promo)").values()),4),
|
|
round(sum(cost(t,"GLM-5.3-Flash (list)").values()),4)])
|
|
emit("TOTAL","ALL",TOT,len(by_session))
|
|
for k,v in sorted(by_machine.items()): emit("machine",k,v,sum(1 for s in sess_machine.values() if s==k))
|
|
for k,v in sorted(by_model.items()): emit("model",k,v)
|
|
for k,v in sorted(by_month.items()): emit("month",k,v)
|
|
for k,v in sorted(by_day.items()): emit("day",k,v)
|
|
|
|
zen_self = 0.0; zen_self_missing = []
|
|
for _m,_t in by_model.items():
|
|
if _m in ZEN_SELF: zen_self += sum(cost(_t, ZEN_SELF[_m]).values())
|
|
else: zen_self_missing.append(_m)
|
|
json.dump({"zen_self":zen_self,"zen_self_missing":zen_self_missing,"TOT":TOT,"by_model":dict(by_model),"by_machine":dict(by_machine),
|
|
"by_month":dict(by_month),"by_day":dict(by_day),"meta":meta,
|
|
"days":days,"span":span,"missing":missing,"cross_dupes":sorted(cross_dupes),
|
|
"n_sessions":len(by_session),
|
|
"sess_per_machine":{k:sum(1 for s in sess_machine.values() if s==k) for k in by_machine}},
|
|
open(os.path.join(OUT,"_agg.json"),"w"), indent=1)
|
|
print("OK", len(by_session), "sesiuni,", len(days), "zile")
|