#!/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")