#!/usr/bin/env python3 """Claude Banana - Cost Tracker Track image generation costs, view summaries, and estimate batch costs. Usage: cost_tracker.py log --model MODEL --resolution RES --prompt "summary" cost_tracker.py summary cost_tracker.py today cost_tracker.py estimate --model MODEL --resolution RES --count N cost_tracker.py reset --confirm """ import argparse import json import sys from datetime import datetime, timezone from pathlib import Path LEDGER_PATH = Path.home() / ".banana" / "costs.json" PRICING_PATH = Path.home() / ".banana" / "pricing.json" PRICING_SOURCE = "https://ai.google.dev/gemini-api/docs/pricing" # Batch API gets 50% discount BATCH_DISCOUNT = 0.5 def _load_ledger(): """Load the cost ledger from disk.""" if not LEDGER_PATH.exists(): return {"total_cost": 0.0, "total_images": 0, "entries": [], "daily": {}} with open(LEDGER_PATH, "r") as f: return json.load(f) def _save_ledger(ledger): """Save the cost ledger to disk.""" LEDGER_PATH.parent.mkdir(parents=True, exist_ok=True) with open(LEDGER_PATH, "w") as f: json.dump(ledger, f, indent=2) def _load_pricing_config(): """Load dated pricing config from disk.""" if not PRICING_PATH.exists(): print(f"Error: Missing pricing config at {PRICING_PATH}.", file=sys.stderr) print(f"Check current pricing at {PRICING_SOURCE}, then create a dated pricing.json.", file=sys.stderr) sys.exit(1) with open(PRICING_PATH, "r") as f: data = json.load(f) models = data.get("models", {}) checked_date = data.get("checked_date") if not models or not checked_date: print("Error: pricing.json must include checked_date and models.", file=sys.stderr) sys.exit(1) return models, checked_date def _lookup_cost(model, resolution, batch=False): """Look up cost for a model+resolution combination.""" pricing, checked_date = _load_pricing_config() model_pricing = pricing.get(model) if not model_pricing: # Try partial match for key in pricing: if key in model or model in key: model_pricing = pricing[key] break if not model_pricing: print(f"Error: No pricing for model '{model}' in {PRICING_PATH}.", file=sys.stderr) print(f"Check current pricing at {PRICING_SOURCE} and update pricing.json.", file=sys.stderr) sys.exit(1) valid_resolutions = {"512", "1K", "2K", "4K"} if resolution not in valid_resolutions: print(f"Warning: Unknown resolution '{resolution}', using 1K pricing", file=sys.stderr) cost = model_pricing.get(resolution, model_pricing.get("1K")) if cost is None: print(f"Error: No pricing for resolution '{resolution}' in {PRICING_PATH}.", file=sys.stderr) sys.exit(1) if batch: cost *= BATCH_DISCOUNT return cost, checked_date def cmd_log(args): """Log a generation to the ledger.""" ledger = _load_ledger() cost, checked_date = _lookup_cost(args.model, args.resolution, getattr(args, "batch", False)) today = datetime.now(timezone.utc).strftime("%Y-%m-%d") now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S") entry = { "ts": now, "model": args.model, "res": args.resolution, "cost": cost, "pricing_checked_date": checked_date, "approximate": True, "prompt": args.prompt[:100], } ledger["entries"].append(entry) ledger["total_cost"] = round(ledger["total_cost"] + cost, 4) ledger["total_images"] += 1 if today not in ledger["daily"]: ledger["daily"][today] = {"count": 0, "cost": 0.0} ledger["daily"][today]["count"] += 1 ledger["daily"][today]["cost"] = round(ledger["daily"][today]["cost"] + cost, 4) _save_ledger(ledger) print(json.dumps({"logged": True, "cost": cost, "total_cost": ledger["total_cost"], "total_images": ledger["total_images"], "approximate": True, "pricing_checked_date": checked_date})) def cmd_summary(args): """Show cost summary.""" ledger = _load_ledger() print(f"Total images: {ledger['total_images']}") print(f"Total cost: approx ${ledger['total_cost']:.3f}") print() daily = ledger.get("daily", {}) if daily: # Show last 7 days sorted_days = sorted(daily.keys(), reverse=True)[:7] print("Last 7 days:") for day in sorted_days: d = daily[day] print(f" {day}: {d['count']} images, approx ${d['cost']:.3f}") else: print("No usage recorded yet.") def cmd_today(args): """Show today's usage.""" ledger = _load_ledger() today = datetime.now(timezone.utc).strftime("%Y-%m-%d") daily = ledger.get("daily", {}).get(today, {"count": 0, "cost": 0.0}) print(f"Today ({today}): {daily['count']} images, approx ${daily['cost']:.3f}") def cmd_estimate(args): """Estimate cost for a batch.""" cost_per, checked_date = _lookup_cost(args.model, args.resolution, getattr(args, "batch", False)) total = round(cost_per * args.count, 3) print(f"Model: {args.model}") print(f"Resolution: {args.resolution}") print(f"Count: {args.count}") print(f"Pricing checked: {checked_date}") print(f"Approx cost/image: ${cost_per:.3f}") print(f"Approx total est: ${total:.3f}") if not getattr(args, "batch", False): batch_total = round(cost_per * BATCH_DISCOUNT * args.count, 3) print(f"Approx batch est: ${batch_total:.3f} (50% discount)") def cmd_reset(args): """Reset the ledger.""" if not args.confirm: print("Error: Pass --confirm to reset the cost ledger.", file=sys.stderr) sys.exit(1) _save_ledger({"total_cost": 0.0, "total_images": 0, "entries": [], "daily": {}}) print("Cost ledger reset.") def main(): parser = argparse.ArgumentParser(description="Claude Banana Cost Tracker") sub = parser.add_subparsers(dest="command", required=True) # log p_log = sub.add_parser("log", help="Log a generation") p_log.add_argument("--model", required=True, help="Model ID") p_log.add_argument("--resolution", required=True, help="Resolution (512, 1K, 2K, 4K)") p_log.add_argument("--prompt", required=True, help="Brief prompt description") p_log.add_argument("--batch", action="store_true", help="Batch API (50%% discount)") # summary sub.add_parser("summary", help="Show cost summary") # today sub.add_parser("today", help="Show today's usage") # estimate p_est = sub.add_parser("estimate", help="Estimate batch cost") p_est.add_argument("--model", required=True, help="Model ID") p_est.add_argument("--resolution", required=True, help="Resolution (512, 1K, 2K, 4K)") p_est.add_argument("--count", required=True, type=int, help="Number of images") p_est.add_argument("--batch", action="store_true", help="Use batch pricing (50%% discount)") # reset p_reset = sub.add_parser("reset", help="Reset cost ledger") p_reset.add_argument("--confirm", action="store_true", help="Confirm reset") args = parser.parse_args() cmds = {"log": cmd_log, "summary": cmd_summary, "today": cmd_today, "estimate": cmd_estimate, "reset": cmd_reset} cmds[args.command](args) if __name__ == "__main__": main()