View your Github Copilot usage from the CLI
- Python3 - should work with just about any Python sunce 3.6
- gh cli installed and authed - we use the gh cli auth token to fetch the usage data
copy to somewhere in your $PATH and make it executable
| #!/usr/bin/env python3 | |
| import argparse | |
| import json | |
| import subprocess | |
| import sys | |
| from datetime import datetime | |
| from typing import Optional | |
| API_PATH = "/copilot_internal/user" | |
| def gh_api_json(hostname: Optional[str], timeout: int) -> dict: | |
| cmd = ["gh", "api", API_PATH, "-H", "Accept: application/json"] | |
| if hostname: | |
| cmd += ["--hostname", hostname] | |
| try: | |
| p = subprocess.run( | |
| cmd, | |
| capture_output=True, | |
| text=True, | |
| timeout=timeout, | |
| ) | |
| except FileNotFoundError as e: | |
| raise RuntimeError("gh is not installed (or not on PATH).") from e | |
| except subprocess.TimeoutExpired as e: | |
| raise RuntimeError(f"Timed out after {timeout}s.") from e | |
| if p.returncode != 0: | |
| msg = (p.stderr or p.stdout or "").strip() | |
| raise RuntimeError(msg or f"`gh api` failed (exit {p.returncode}).") | |
| try: | |
| return json.loads(p.stdout) | |
| except json.JSONDecodeError as e: | |
| raise RuntimeError("Failed to parse JSON returned by `gh api`.") from e | |
| def parse_quota(data: dict): | |
| """Returns (used, total, remaining) or None if unlimited/unknown.""" | |
| snapshots = data.get("quota_snapshots", {}) | |
| premium = snapshots.get("premium_interactions", {}) | |
| if premium.get("unlimited"): | |
| return None | |
| total = premium.get("entitlement") | |
| remaining = premium.get("remaining") or premium.get("quota_remaining") | |
| if total is None or remaining is None: | |
| return None | |
| used = max(total - remaining, 0) | |
| return used, total, remaining | |
| def format_reset_date_ddmmyy(data: dict) -> Optional[str]: | |
| s = data.get("quota_reset_date") | |
| if isinstance(s, str) and s: | |
| try: | |
| return datetime.strptime(s, "%Y-%m-%d").strftime("%d/%m/%y") | |
| except ValueError: | |
| pass | |
| s = data.get("quota_reset_date_utc") | |
| if isinstance(s, str) and s: | |
| try: | |
| return datetime.fromisoformat(s.replace("Z", "+00:00")).strftime("%d/%m/%y") | |
| except ValueError: | |
| pass | |
| return None | |
| def parse_snapshot(snapshot: dict): | |
| """Returns (used, total, remaining), 'unlimited', or None if unknown.""" | |
| if not isinstance(snapshot, dict) or not snapshot: | |
| return None | |
| if snapshot.get("unlimited"): | |
| return "unlimited" | |
| total = snapshot.get("entitlement") | |
| remaining = snapshot.get("remaining") | |
| if remaining is None: | |
| remaining = snapshot.get("quota_remaining") | |
| if total is None or remaining is None: | |
| return None | |
| try: | |
| total_f = float(total) | |
| remaining_f = float(remaining) | |
| except (TypeError, ValueError): | |
| return None | |
| used_f = max(total_f - remaining_f, 0.0) | |
| def as_int_if_close(v: float): | |
| if abs(v - round(v)) < 1e-9: | |
| return int(round(v)) | |
| return v | |
| return as_int_if_close(used_f), as_int_if_close(total_f), as_int_if_close(remaining_f) | |
| def format_quota_value(parsed) -> str: | |
| if parsed == "unlimited": | |
| return "unlimited" | |
| if not parsed: | |
| return "unknown" | |
| used, total, _remaining = parsed | |
| try: | |
| pct = int((float(used) / float(total)) * 100) if float(total) else 0 | |
| except (TypeError, ValueError, ZeroDivisionError): | |
| pct = 0 | |
| return f"{used}/{total} - {pct}%" | |
| def metrics_from_parsed_snapshot(parsed): | |
| if parsed == "unlimited": | |
| return {"status": "unlimited"} | |
| if not parsed: | |
| return {"status": "unknown"} | |
| used, total, remaining = parsed | |
| try: | |
| pct = int((float(used) / float(total)) * 100) if float(total) else 0 | |
| except (TypeError, ValueError, ZeroDivisionError): | |
| pct = 0 | |
| return { | |
| "used": used, | |
| "total": total, | |
| "remaining": remaining, | |
| "pct": pct, | |
| } | |
| def format_metrics_value(metrics: dict) -> str: | |
| status = metrics.get("status") | |
| if status == "unlimited": | |
| return "unlimited" | |
| if status == "unknown": | |
| return "unknown" | |
| used = metrics.get("used") | |
| total = metrics.get("total") | |
| pct = metrics.get("pct") | |
| return f"{used}/{total} - {pct}%" | |
| def collect_snapshot_metrics(data: dict) -> dict: | |
| snapshots = data.get("quota_snapshots") | |
| if not isinstance(snapshots, dict): | |
| return {} | |
| out = {} | |
| for k, v in snapshots.items(): | |
| out[k] = metrics_from_parsed_snapshot(parse_snapshot(v)) | |
| return out | |
| def main() -> int: | |
| ap = argparse.ArgumentParser( | |
| prog="ghcp-usage", | |
| description="Show Copilot premium interaction quota usage.", | |
| ) | |
| ap.add_argument( | |
| "--json", | |
| action="store_true", | |
| help="Print JSON for the selected output mode.", | |
| ) | |
| mode = ap.add_mutually_exclusive_group() | |
| mode.add_argument( | |
| "--detailed", | |
| action="store_true", | |
| help="Show premium/chat/completions plus reset date.", | |
| ) | |
| mode.add_argument( | |
| "--all", | |
| action="store_true", | |
| help="Show all quota snapshot values plus reset date.", | |
| ) | |
| ap.add_argument( | |
| "--hostname", | |
| help="GitHub hostname (for GHES); defaults to github.com via gh config.", | |
| ) | |
| ap.add_argument("--timeout", type=int, default=15, help="Timeout seconds (default: 15).") | |
| args = ap.parse_args() | |
| try: | |
| data = gh_api_json(args.hostname, args.timeout) | |
| except Exception as e: | |
| print(f"Error: {e}", file=sys.stderr) | |
| return 1 | |
| reset = format_reset_date_ddmmyy(data) | |
| reset_suffix = f" - {reset}" if reset else "" | |
| snapshots = collect_snapshot_metrics(data) | |
| def snap(key: str) -> dict: | |
| return snapshots.get(key) or {"status": "unknown"} | |
| selected_mode = "default" | |
| if args.all: | |
| selected_mode = "all" | |
| elif args.detailed: | |
| selected_mode = "detailed" | |
| if args.json: | |
| if selected_mode == "default": | |
| payload = { | |
| "mode": "default", | |
| "reset_date": reset, | |
| "premium": snap("premium_interactions"), | |
| } | |
| elif selected_mode == "detailed": | |
| payload = { | |
| "mode": "detailed", | |
| "reset_date": reset, | |
| "premium": snap("premium_interactions"), | |
| "chat": snap("chat"), | |
| "completions": snap("completions"), | |
| } | |
| else: | |
| payload = { | |
| "mode": "all", | |
| "reset_date": reset, | |
| "snapshots": snapshots, | |
| } | |
| print(json.dumps(payload, indent=2, sort_keys=True)) | |
| return 0 | |
| if args.detailed: | |
| print(f"premium: {format_metrics_value(snap('premium_interactions'))}") | |
| print(f"chat: {format_metrics_value(snap('chat'))}") | |
| print(f"completions: {format_metrics_value(snap('completions'))}") | |
| print(f"reset date: {reset or 'unknown'}") | |
| return 0 | |
| if args.all: | |
| for k in sorted(snapshots.keys()): | |
| print(f"{k}: {format_metrics_value(snapshots[k])}") | |
| print(f"reset date: {reset or 'unknown'}") | |
| return 0 | |
| premium_value = format_metrics_value(snap("premium_interactions")) | |
| print(f"{premium_value}{reset_suffix}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |