Skip to content

Instantly share code, notes, and snippets.

@m0wer
Last active April 16, 2026 14:46
Show Gist options
  • Select an option

  • Save m0wer/06181c2f445285f9f03ae74312b8c64e to your computer and use it in GitHub Desktop.

Select an option

Save m0wer/06181c2f445285f9f03ae74312b8c64e to your computer and use it in GitHub Desktop.
Github Copilot subscription quota tracker
#!/usr/bin/env bash
# copilot-quota.sh — Shell function to check GitHub Copilot premium request quota
#
# Installation:
# Copy this file somewhere, e.g. ~/.config/opencode/copilot-quota.sh
# Then add to your ~/.bashrc or ~/.zshrc:
# source ~/.config/opencode/copilot-quota.sh
#
# Usage:
# copilot-quota
copilot-quota() {
local AUTH_FILE="$HOME/.local/share/opencode/auth.json"
local COPILOT_VERSION="0.35.0"
local EDITOR_VERSION="vscode/1.107.0"
# ── 1. Read token from opencode auth.json ──────────────────────────────────
if [[ ! -f "$AUTH_FILE" ]]; then
echo "Error: opencode auth file not found at $AUTH_FILE" >&2
return 1
fi
local OAUTH_TOKEN
OAUTH_TOKEN=$(python3 -c "
import json, sys
try:
data = json.load(open('$AUTH_FILE'))
gh = data.get('github-copilot', {})
token = gh.get('refresh') or gh.get('access') or ''
print(token)
except Exception as e:
print('', end='')
" 2>/dev/null)
if [[ -z "$OAUTH_TOKEN" ]]; then
echo "Error: No GitHub Copilot token found in auth.json" >&2
return 1
fi
# ── 2. Try token exchange (for new gho_ OAuth tokens) ──────────────────────
local COPILOT_TOKEN
local EXCHANGE_RESP
EXCHANGE_RESP=$(curl -s -w "\n%{http_code}" \
-H "Accept: application/json" \
-H "Authorization: Bearer $OAUTH_TOKEN" \
-H "User-Agent: GitHubCopilotChat/$COPILOT_VERSION" \
-H "Editor-Version: $EDITOR_VERSION" \
-H "Editor-Plugin-Version: copilot-chat/$COPILOT_VERSION" \
-H "Copilot-Integration-Id: vscode-chat" \
"https://api.github.com/copilot_internal/v2/token" 2>/dev/null)
local EXCHANGE_BODY EXCHANGE_STATUS
EXCHANGE_BODY=$(echo "$EXCHANGE_RESP" | head -n -1)
EXCHANGE_STATUS=$(echo "$EXCHANGE_RESP" | tail -n 1)
if [[ "$EXCHANGE_STATUS" == "200" ]]; then
COPILOT_TOKEN=$(python3 -c "
import json, sys
try:
data = json.loads('''$EXCHANGE_BODY''')
print(data.get('token', ''))
except:
print('')
" 2>/dev/null)
fi
# ── 3. Call the Copilot quota API ──────────────────────────────────────────
local API_RESP API_BODY API_STATUS
local AUTH_HEADER
if [[ -n "$COPILOT_TOKEN" ]]; then
AUTH_HEADER="Bearer $COPILOT_TOKEN"
else
AUTH_HEADER="token $OAUTH_TOKEN"
fi
API_RESP=$(curl -s -w "\n%{http_code}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: $AUTH_HEADER" \
-H "User-Agent: GitHubCopilotChat/$COPILOT_VERSION" \
-H "Editor-Version: $EDITOR_VERSION" \
-H "Editor-Plugin-Version: copilot-chat/$COPILOT_VERSION" \
-H "Copilot-Integration-Id: vscode-chat" \
"https://api.github.com/copilot_internal/user" 2>/dev/null)
API_BODY=$(echo "$API_RESP" | head -n -1)
API_STATUS=$(echo "$API_RESP" | tail -n 1)
if [[ "$API_STATUS" != "200" && -n "$COPILOT_TOKEN" ]]; then
API_RESP=$(curl -s -w "\n%{http_code}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Bearer $OAUTH_TOKEN" \
-H "User-Agent: GitHubCopilotChat/$COPILOT_VERSION" \
-H "Editor-Version: $EDITOR_VERSION" \
-H "Editor-Plugin-Version: copilot-chat/$COPILOT_VERSION" \
-H "Copilot-Integration-Id: vscode-chat" \
"https://api.github.com/copilot_internal/user" 2>/dev/null)
API_BODY=$(echo "$API_RESP" | head -n -1)
API_STATUS=$(echo "$API_RESP" | tail -n 1)
fi
if [[ "$API_STATUS" != "200" ]]; then
echo "Error: Copilot API call failed (HTTP $API_STATUS): $API_BODY" >&2
return 1
fi
# ── 4. Parse response, calculate pace, and render ──────────────────────────
python3 - "$API_BODY" <<'PYEOF'
import sys, json, math
from datetime import date, timedelta
import calendar
BAR_WIDTH = 34
def bar(filled_pct, secondary_pct=None, width=BAR_WIDTH):
"""
Render a two-tone ASCII bar.
filled_pct : primary fill (e.g. used) → '█'
secondary_pct: secondary fill (e.g. month) → '▒' (shown only when > filled_pct)
Both are clamped to [0, 100].
"""
filled_pct = max(0.0, min(100.0, filled_pct))
n_fill = round(filled_pct / 100 * width)
n_fill = max(0, min(width, n_fill))
if secondary_pct is not None:
secondary_pct = max(0.0, min(100.0, secondary_pct))
n_sec = round(secondary_pct / 100 * width)
n_sec = max(n_fill, min(width, n_sec)) # secondary never less than primary
else:
n_sec = n_fill
filled = '█' * n_fill
secondary = '▒' * (n_sec - n_fill)
empty = '░' * (width - n_sec)
return f"[{filled}{secondary}{empty}]"
raw = sys.argv[1]
try:
data = json.loads(raw)
except json.JSONDecodeError:
print("Error: Failed to parse API response", file=sys.stderr)
sys.exit(1)
snapshots = data.get("quota_snapshots", {})
premium = snapshots.get("premium_interactions", {})
if not premium:
print("Error: No premium_interactions quota data found", file=sys.stderr)
sys.exit(1)
if premium.get("unlimited"):
print("── Copilot Premium Requests ─────────────────")
print(" Entitlement : Unlimited")
sys.exit(0)
entitlement = premium.get("entitlement", 0)
remaining = premium.get("remaining", 0)
used = entitlement - remaining
pct_used = (used / entitlement * 100) if entitlement > 0 else 0.0
# ── Billing cycle dates ────────────────────────────────────────────────────
reset_date_str = data.get("quota_reset_date", "")
try:
reset_date = date.fromisoformat(reset_date_str.replace("Z", "").split("T")[0])
year, month = reset_date.year, reset_date.month
if month == 1:
cycle_start = date(year - 1, 12, reset_date.day)
else:
last_day = calendar.monthrange(year, month - 1)[1]
day = min(reset_date.day, last_day)
cycle_start = date(year, month - 1, day)
today = date.today()
days_elapsed = (today - cycle_start).days + 1
days_in_month = (reset_date - cycle_start).days
days_remaining_in_month = days_in_month - days_elapsed
except Exception:
cycle_start = today = date.today()
days_elapsed = 1
days_in_month = 30
days_remaining_in_month = 29
pct_month_elapsed = (days_elapsed / days_in_month * 100) if days_in_month > 0 else 0.0
# ── Derived metrics ────────────────────────────────────────────────────────
daily_budget = entitlement / days_in_month if days_in_month > 0 else 0
actual_daily_rate = used / days_elapsed if days_elapsed > 0 else 0
expected_used = math.floor(entitlement * days_elapsed / days_in_month) if days_in_month > 0 else 0
pace_diff = expected_used - used # positive = under pace (good)
# Days ahead/behind: how many days of budget the diff represents
days_delta = abs(pace_diff) / daily_budget if daily_budget > 0 else 0
# Projected EOM usage at current burn rate
projected_eom = round(actual_daily_rate * days_in_month)
# Days until exhaustion at current rate (from today)
if actual_daily_rate > 0:
days_until_empty = remaining / actual_daily_rate
else:
days_until_empty = float('inf')
# ── Render ─────────────────────────────────────────────────────────────────
print("── Copilot Premium Requests ─────────────────────────")
print(f" Entitlement : {entitlement} / month ({daily_budget:.1f} / day)")
print(f" Used : {used} ({pct_used:.1f}%)")
print(f" Remaining : {remaining}")
print()
# ASCII chart — two rows
usage_bar = bar(pct_used, pct_month_elapsed)
month_bar = bar(pct_month_elapsed)
print("── Usage vs Month ───────────────────────────────────")
print(f" Usage {usage_bar} {pct_used:.1f}%")
print(f" Month {month_bar} {pct_month_elapsed:.1f}% (day {days_elapsed}/{days_in_month})")
print()
# Pace block
print(f"── Pace Check ───────────────────────────────────────")
print(f" Burn rate : {actual_daily_rate:.1f} req/day (budget {daily_budget:.1f}/day)")
print(f" Expected used: {expected_used}")
print(f" Actual used : {used}")
pace_pct = round(abs(pace_diff) / entitlement * 100, 1) if entitlement > 0 else 0
if pace_diff >= 0:
print(f" Status : ✅ UNDER pace by {pace_diff} req ({pace_pct}%) · {days_delta:.1f} days ahead")
else:
over = -pace_diff
print(f" Status : ❌ OVER pace by {over} req ({pace_pct}%) · {days_delta:.1f} days behind")
print()
print(f"── Forecast (at {actual_daily_rate:.1f} req/day current burn) ────────")
projected_eom_smart = used + round(actual_daily_rate * max(0, days_remaining_in_month))
proj_pct = projected_eom_smart / entitlement * 100 if entitlement > 0 else 0
print(f" Projected EOM : {projected_eom_smart} req ({proj_pct:.1f}% of entitlement)")
if math.isinf(days_until_empty):
print(f" Quota runs out : never at current rate")
elif days_until_empty > days_remaining_in_month:
projected_spend = round(actual_daily_rate * days_remaining_in_month)
surplus = remaining - projected_spend
print(f" Quota runs out : after reset (~{surplus:+} req surplus at current pace)")
else:
exhaustion_date = today + timedelta(days=days_until_empty)
print(f" Quota runs out : ~{exhaustion_date.strftime('%b %d')} ({days_until_empty:.1f} days at current rate)")
PYEOF
}
@m0wer

m0wer commented Mar 12, 2026

Copy link
Copy Markdown
Author

Example output:

── Copilot Premium Requests ─────────────────────────
  Entitlement : 1500 / month  (48.4 / day)
  Used        : 373  (24.9%)
  Remaining   : 1127

── Usage vs Month ───────────────────────────────────
  Usage  [████████▒▒▒░░░░░░░░░░░░░░░░░░░░░░░] 24.9%
  Month  [███████████░░░░░░░░░░░░░░░░░░░░░░░] 32.3%  (day 10/31)

── Pace Check ───────────────────────────────────────
  Burn rate    : 37.3 req/day  (budget 48.4/day)
  Expected used: 483
  Actual used  : 373
  Status       : ✅ UNDER pace by 110 req (7.3%) · 2.3 days ahead

── Forecast ─────────────────────────────────────────
  Projected EOM: 1156 req (77.1% of entitlement)
  Quota runs out: after reset  (~+111 req surplus)

@joeskeen

joeskeen commented Apr 2, 2026

Copy link
Copy Markdown

Thanks @m0wer for putting this together, it's super helpful!

I've adapted it as a PowerShell (pwsh core) version here: https://gist.github.com/joeskeen/ba9d090d58d93f1522f6439fd800f1cd
That allows you to pass -AsObject and then be able to pipe the raw data object to whatever other commands you want.

@m0wer

m0wer commented Apr 16, 2026

Copy link
Copy Markdown
Author

Improved the forescast to use the current pace:

── Copilot Premium Requests ─────────────────────────
  Entitlement : 1500 / month  (50.0 / day)
  Used        : 804  (53.6%)
  Remaining   : 696

── Usage vs Month ───────────────────────────────────
  Usage  [██████████████████░░░░░░░░░░░░░░░░] 53.6%
  Month  [██████████████████░░░░░░░░░░░░░░░░] 53.3%  (day 16/30)

── Pace Check ───────────────────────────────────────
  Burn rate    : 50.2 req/day  (budget 50.0/day)
  Expected used: 800
  Actual used  : 804
  Status       : ❌ OVER pace by 4 req (0.3%) · 0.1 days behind

── Forecast (at 50.2 req/day current burn) ────────
  Projected EOM  : 1508 req (100.5% of entitlement)
  Quota runs out : ~Apr 29 (13.9 days at current rate)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment