-
-
Save joeskeen/ba9d090d58d93f1522f6439fd800f1cd to your computer and use it in GitHub Desktop.
Github Copilot subscription quota tracker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Retrieves GitHub Copilot premium usage and pace details via OpenCode-authenticated APIs. | |
| .DESCRIPTION | |
| This file contains only the function definition. To use it as a command, save it to its | |
| own `.ps1` file and dot-source that file from your `$PROFILE`. | |
| This command depends on OpenCode authentication state. Install OpenCode, then run | |
| `opencode auth` and complete GitHub Copilot authentication before using this command. | |
| By default, the command writes a formatted console report. Pass `-AsObject` to return | |
| the raw object so you can pipe it or further manipulate the data in scripts. | |
| .EXAMPLE | |
| Get-CopilotUsage | |
| Displays the formatted Copilot premium usage and pacing report. | |
| .EXAMPLE | |
| Get-CopilotUsage -AsObject | Select-Object Used, Remaining, PercentUsed | |
| Returns the raw usage object for scripting and custom output. | |
| #> | |
| function Get-CopilotUsage { | |
| [CmdletBinding()] | |
| [OutputType([pscustomobject])] | |
| param( | |
| [Parameter()] | |
| [string]$AuthFile = (Join-Path $HOME ".local/share/opencode/auth.json"), | |
| [Parameter()] | |
| [string]$CopilotVersion = "0.35.0", | |
| [Parameter()] | |
| [string]$EditorVersion = "vscode/1.107.0", | |
| [Parameter()] | |
| [int]$BarWidth = 34, | |
| [Parameter()] | |
| [switch]$AsObject | |
| ) | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = "Stop" | |
| $utf8NoBom = [System.Text.UTF8Encoding]::new($false) | |
| [Console]::OutputEncoding = $utf8NoBom | |
| $OutputEncoding = $utf8NoBom | |
| function Invoke-CopilotApi { | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$Uri, | |
| [Parameter(Mandatory)] | |
| [hashtable]$Headers | |
| ) | |
| $response = Invoke-WebRequest -Uri $Uri -Method Get -Headers $Headers -SkipHttpErrorCheck | |
| [pscustomobject]@{ | |
| StatusCode = [int]$response.StatusCode | |
| Body = [string]($response.Content ?? "") | |
| } | |
| } | |
| function New-UsageBar { | |
| param( | |
| [Parameter(Mandatory)] | |
| [double]$FilledPercent, | |
| [Parameter()] | |
| [double]$SecondaryPercent, | |
| [Parameter(Mandatory)] | |
| [int]$Width | |
| ) | |
| $filledPct = [Math]::Max(0, [Math]::Min(100, $FilledPercent)) | |
| $filledCount = [Math]::Round(($filledPct / 100) * $Width) | |
| $filledCount = [Math]::Max(0, [Math]::Min($Width, $filledCount)) | |
| if ($PSBoundParameters.ContainsKey('SecondaryPercent')) { | |
| $secondaryPct = [Math]::Max(0, [Math]::Min(100, $SecondaryPercent)) | |
| $secondaryCount = [Math]::Round(($secondaryPct / 100) * $Width) | |
| $secondaryCount = [Math]::Max($filledCount, [Math]::Min($Width, $secondaryCount)) | |
| } | |
| else { | |
| $secondaryCount = $filledCount | |
| } | |
| "[{0}{1}{2}]" -f ("█" * $filledCount), ("▒" * ($secondaryCount - $filledCount)), ("░" * ($Width - $secondaryCount)) | |
| } | |
| if (-not (Test-Path -Path $AuthFile -PathType Leaf)) { | |
| throw "opencode auth file not found at $AuthFile" | |
| } | |
| $authJson = Get-Content -Path $AuthFile -Raw | ConvertFrom-Json | |
| $ghAuth = $authJson.'github-copilot' | |
| if ($null -eq $ghAuth) { | |
| throw "No 'github-copilot' section found in auth.json" | |
| } | |
| $oauthToken = $ghAuth.refresh | |
| if ([string]::IsNullOrWhiteSpace($oauthToken)) { | |
| $oauthToken = $ghAuth.access | |
| } | |
| if ([string]::IsNullOrWhiteSpace($oauthToken)) { | |
| throw "No GitHub Copilot token found in auth.json" | |
| } | |
| $commonHeaders = @{ | |
| "Accept" = "application/json" | |
| "User-Agent" = "GitHubCopilotChat/$CopilotVersion" | |
| "Editor-Version" = $EditorVersion | |
| "Editor-Plugin-Version" = "copilot-chat/$CopilotVersion" | |
| "Copilot-Integration-Id" = "vscode-chat" | |
| } | |
| $exchangeHeaders = $commonHeaders.Clone() | |
| $exchangeHeaders["Authorization"] = "Bearer $oauthToken" | |
| $copilotToken = $null | |
| $exchange = Invoke-CopilotApi -Uri "https://api.github.com/copilot_internal/v2/token" -Headers $exchangeHeaders | |
| if ($exchange.StatusCode -eq 200) { | |
| try { | |
| $copilotToken = ($exchange.Body | ConvertFrom-Json).token | |
| } | |
| catch { | |
| $copilotToken = $null | |
| } | |
| } | |
| $quotaHeaders = $commonHeaders.Clone() | |
| $quotaHeaders["Content-Type"] = "application/json" | |
| $quotaHeaders["Authorization"] = if ([string]::IsNullOrWhiteSpace($copilotToken)) { "token $oauthToken" } else { "Bearer $copilotToken" } | |
| $quota = Invoke-CopilotApi -Uri "https://api.github.com/copilot_internal/user" -Headers $quotaHeaders | |
| if ($quota.StatusCode -ne 200 -and -not [string]::IsNullOrWhiteSpace($copilotToken)) { | |
| $fallbackHeaders = $commonHeaders.Clone() | |
| $fallbackHeaders["Content-Type"] = "application/json" | |
| $fallbackHeaders["Authorization"] = "Bearer $oauthToken" | |
| $quota = Invoke-CopilotApi -Uri "https://api.github.com/copilot_internal/user" -Headers $fallbackHeaders | |
| } | |
| if ($quota.StatusCode -ne 200) { | |
| throw "Copilot API call failed (HTTP $($quota.StatusCode)): $($quota.Body)" | |
| } | |
| try { | |
| $data = $quota.Body | ConvertFrom-Json | |
| } | |
| catch { | |
| throw "Failed to parse Copilot API response" | |
| } | |
| $premium = $data.quota_snapshots.premium_interactions | |
| if ($null -eq $premium) { | |
| throw "No premium_interactions quota data found" | |
| } | |
| if ($premium.unlimited -eq $true) { | |
| $unlimited = [pscustomobject]@{ | |
| Entitlement = "Unlimited" | |
| } | |
| if ($AsObject) { | |
| return $unlimited | |
| } | |
| Write-Output "── Copilot Premium Requests ─────────────────" | |
| Write-Output " Entitlement : Unlimited" | |
| return $unlimited | |
| } | |
| $entitlement = [int]$premium.entitlement | |
| $remaining = [int]$premium.remaining | |
| $used = $entitlement - $remaining | |
| $pctUsed = if ($entitlement -gt 0) { ($used / $entitlement) * 100 } else { 0 } | |
| try { | |
| $resetDate = [datetimeoffset]::Parse($data.quota_reset_date).Date | |
| $prior = $resetDate.AddMonths(-1) | |
| $cycleDay = [Math]::Min($resetDate.Day, [datetime]::DaysInMonth($prior.Year, $prior.Month)) | |
| $cycleStart = Get-Date -Year $prior.Year -Month $prior.Month -Day $cycleDay -Hour 0 -Minute 0 -Second 0 | |
| $today = (Get-Date).Date | |
| $daysElapsed = [int](($today - $cycleStart).TotalDays) + 1 | |
| $daysInMonth = [int](($resetDate - $cycleStart).TotalDays) | |
| $daysRemaining = $daysInMonth - $daysElapsed | |
| } | |
| catch { | |
| $today = (Get-Date).Date | |
| $cycleStart = $today | |
| $resetDate = $today.AddDays(30) | |
| $daysElapsed = 1 | |
| $daysInMonth = 30 | |
| $daysRemaining = 29 | |
| } | |
| if ($daysElapsed -lt 1) { $daysElapsed = 1 } | |
| $pctMonthElapsed = if ($daysInMonth -gt 0) { ($daysElapsed / $daysInMonth) * 100 } else { 0 } | |
| $dailyBudget = if ($daysInMonth -gt 0) { $entitlement / $daysInMonth } else { 0 } | |
| $actualDailyRate = if ($daysElapsed -gt 0) { $used / $daysElapsed } else { 0 } | |
| $expectedUsed = if ($daysInMonth -gt 0) { [Math]::Floor($entitlement * $daysElapsed / $daysInMonth) } else { 0 } | |
| $paceDiff = $expectedUsed - $used | |
| $daysDelta = if ($dailyBudget -gt 0) { [Math]::Abs($paceDiff) / $dailyBudget } else { 0 } | |
| $projectedEom = [Math]::Round($actualDailyRate * $daysInMonth) | |
| $daysUntilEmpty = if ($actualDailyRate -gt 0) { $remaining / $actualDailyRate } else { [double]::PositiveInfinity } | |
| $result = [pscustomobject]@{ | |
| Entitlement = $entitlement | |
| Used = $used | |
| Remaining = $remaining | |
| PercentUsed = [Math]::Round($pctUsed, 1) | |
| CycleStart = $cycleStart | |
| ResetDate = $resetDate | |
| DaysElapsed = $daysElapsed | |
| DaysInCycle = $daysInMonth | |
| PercentMonthElapsed = [Math]::Round($pctMonthElapsed, 1) | |
| DailyBudget = [Math]::Round($dailyBudget, 2) | |
| ActualDailyRate = [Math]::Round($actualDailyRate, 2) | |
| ExpectedUsed = [int]$expectedUsed | |
| PaceDiff = [int]$paceDiff | |
| DaysDelta = [Math]::Round($daysDelta, 1) | |
| ProjectedEndOfMonth = [int]$projectedEom | |
| DaysUntilEmpty = if ([double]::IsInfinity($daysUntilEmpty)) { "Infinity" } else { [Math]::Round($daysUntilEmpty, 1) } | |
| } | |
| if ($AsObject) { | |
| return $result | |
| } | |
| $usageBar = New-UsageBar -FilledPercent $pctUsed -SecondaryPercent $pctMonthElapsed -Width $BarWidth | |
| $monthBar = New-UsageBar -FilledPercent $pctMonthElapsed -Width $BarWidth | |
| $pacePct = if ($entitlement -gt 0) { [Math]::Round(([Math]::Abs($paceDiff) / $entitlement) * 100, 1) } else { 0 } | |
| $projPct = if ($entitlement -gt 0) { ($projectedEom / $entitlement) * 100 } else { 0 } | |
| Write-Output "── Copilot Premium Requests ─────────────────────────" | |
| Write-Output (" Entitlement : {0} / month ({1:N1} / day)" -f $entitlement, $dailyBudget) | |
| Write-Output (" Used : {0} ({1:N1}%)" -f $used, $pctUsed) | |
| Write-Output (" Remaining : {0}" -f $remaining) | |
| Write-Output "" | |
| Write-Output "── Usage vs Month ───────────────────────────────────" | |
| Write-Output (" Usage {0} {1:N1}%" -f $usageBar, $pctUsed) | |
| Write-Output (" Month {0} {1:N1}% (day {2}/{3})" -f $monthBar, $pctMonthElapsed, $daysElapsed, $daysInMonth) | |
| Write-Output "" | |
| Write-Output "── Pace Check ───────────────────────────────────────" | |
| Write-Output (" Burn rate : {0:N1} req/day (budget {1:N1}/day)" -f $actualDailyRate, $dailyBudget) | |
| Write-Output (" Expected used: {0}" -f $expectedUsed) | |
| Write-Output (" Actual used : {0}" -f $used) | |
| if ($paceDiff -ge 0) { | |
| Write-Output (" Status : ✅ UNDER pace by {0} req ({1:N1}%) · {2:N1} days ahead" -f $paceDiff, $pacePct, $daysDelta) | |
| } | |
| else { | |
| $over = -1 * $paceDiff | |
| Write-Output (" Status : ❌ OVER pace by {0} req ({1:N1}%) · {2:N1} days behind" -f $over, $pacePct, $daysDelta) | |
| } | |
| Write-Output "" | |
| Write-Output "── Forecast ─────────────────────────────────────────" | |
| Write-Output (" Projected EOM: {0} req ({1:N1}% of entitlement)" -f $projectedEom, $projPct) | |
| if ([double]::IsInfinity($daysUntilEmpty)) { | |
| Write-Output " Quota runs out: never at current rate" | |
| } | |
| elseif ($daysUntilEmpty -gt $daysRemaining) { | |
| $surplus = $remaining - [Math]::Round($dailyBudget * $daysRemaining) | |
| Write-Output (" Quota runs out: after reset (~{0:+#;-#;0} req surplus)" -f $surplus) | |
| } | |
| else { | |
| $exhaustionDate = $today.AddDays($daysUntilEmpty) | |
| Write-Output (" Quota runs out: ~{0} ({1:N1} days at current rate)" -f $exhaustionDate.ToString("MMM dd"), $daysUntilEmpty) | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example output: