Created
May 22, 2026 08:54
-
-
Save wullemsb/740173f728a30712130afef39fe9cd3a to your computer and use it in GitHub Desktop.
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
| #Requires -Version 7 | |
| param() | |
| # Force output encoding to UTF-8 without BOM to ensure proper display of special characters in the status line. | |
| $utf8 = New-Object System.Text.UTF8Encoding $false | |
| [Console]::OutputEncoding = $utf8 | |
| $OutputEncoding = $utf8 | |
| $payload = $input | ConvertFrom-Json | |
| # ── Helpers ─────────────────────────────────────────────────────────────────── | |
| function Format-Tokens { | |
| param([object]$n) | |
| if ($null -eq $n -or -not ($n -match '^[0-9]+(\.[0-9]+)?$')) { return "" } | |
| $v = [double]$n | |
| if ($v -ge 1000000) { return "{0:F1}M" -f ($v / 1000000) } | |
| if ($v -ge 1000) { return "{0:F1}k" -f ($v / 1000) } | |
| return "{0:F0}" -f $v | |
| } | |
| function Format-Usd { | |
| param([double]$n) | |
| if ($n -gt 0 -and $n -lt 0.01) { return ('${0:F4}' -f $n) } | |
| return ('${0:F2}' -f $n) | |
| } | |
| function Format-Duration { | |
| param([long]$ms) | |
| $t = [TimeSpan]::FromMilliseconds($ms) | |
| return "{0:D2}:{1:D2}:{2:D2}" -f [int]$t.TotalHours, $t.Minutes, $t.Seconds | |
| } | |
| function Get-NormalizedModelId { | |
| param([string]$id) | |
| return $id.ToLower() ` | |
| -replace '\s*\(.*?\)', '' ` | |
| -replace '[^a-z0-9.+\-]', '-' ` | |
| -replace '-+', '-' ` | |
| -replace '^-|-$', '' | |
| } | |
| function Get-ModelName { | |
| param([string]$id) | |
| $map = @{ | |
| 'claude-haiku-4.5' = 'Haiku 4.5' | |
| 'claude-sonnet-4' = 'Sonnet 4' | |
| 'claude-sonnet-4.0' = 'Sonnet 4' | |
| 'claude-sonnet-4.5' = 'Sonnet 4.5' | |
| 'claude-sonnet-4.6' = 'Sonnet 4.6' | |
| 'claude-opus-4.5' = 'Opus 4.5' | |
| 'claude-opus-4.6' = 'Opus 4.6' | |
| 'claude-opus-4.7' = 'Opus 4.7' | |
| 'gpt-4.1' = 'GPT-4.1' | |
| 'gpt-5-mini' = 'GPT-5 mini' | |
| 'raptor-mini' = 'Raptor mini' | |
| 'gpt-5.2' = 'GPT-5.2' | |
| 'gpt-5.2-codex' = 'GPT-5.2-Codex' | |
| 'gpt-5.3-codex' = 'GPT-5.3-Codex' | |
| 'gpt-5.4' = 'GPT-5.4' | |
| 'gpt-5.4-mini' = 'GPT-5.4 mini' | |
| 'gpt-5.4-nano' = 'GPT-5.4 nano' | |
| 'gpt-5.5' = 'GPT-5.5' | |
| 'gemini-2.5-pro' = 'Gemini 2.5 Pro' | |
| 'gemini-3-flash' = 'Gemini 3 Flash' | |
| 'gemini-3.1-pro' = 'Gemini 3.1 Pro' | |
| 'grok-code-fast-1' = 'Grok Code Fast 1' | |
| 'goldeneye' = 'Goldeneye' | |
| } | |
| $normalized = Get-NormalizedModelId $id | |
| return $map[$normalized] ?? $id | |
| } | |
| function Get-PricingRates { | |
| # Returns [inputRate, cachedInputRate, cacheWriteRate, outputRate] per 1M tokens. | |
| # GitHub Copilot usage-based billing starts June 1, 2026. | |
| # Always validate against current Copilot pricing docs before using for billing. | |
| param([string]$modelId) | |
| $n = Get-NormalizedModelId $modelId | |
| $table = @{ | |
| 'gpt-4.1' = @(2.00, 0.50, 0, 8.00) | |
| 'gpt-5-mini' = @(0.25, 0.025, 0, 2.00) | |
| 'raptor-mini' = @(0.25, 0.025, 0, 2.00) | |
| 'gpt-5.2' = @(1.75, 0.175, 0, 14.00) | |
| 'gpt-5.2-codex' = @(1.75, 0.175, 0, 14.00) | |
| 'gpt-5.3-codex' = @(1.75, 0.175, 0, 14.00) | |
| 'gpt-5.4' = @(2.50, 0.25, 0, 15.00) | |
| 'gpt-5.4-mini' = @(0.75, 0.075, 0, 4.50) | |
| 'gpt-5.4-nano' = @(0.20, 0.02, 0, 1.25) | |
| 'gpt-5.5' = @(5.00, 0.50, 0, 30.00) | |
| 'claude-haiku-4.5' = @(1.00, 0.10, 1.25, 5.00) | |
| 'claude-sonnet-4' = @(3.00, 0.30, 3.75, 15.00) | |
| 'claude-sonnet-4.0' = @(3.00, 0.30, 3.75, 15.00) | |
| 'claude-sonnet-4.5' = @(3.00, 0.30, 3.75, 15.00) | |
| 'claude-sonnet-4.6' = @(3.00, 0.30, 3.75, 15.00) | |
| 'claude-opus-4.5' = @(5.00, 0.50, 6.25, 25.00) | |
| 'claude-opus-4.6' = @(5.00, 0.50, 6.25, 25.00) | |
| 'claude-opus-4.7' = @(5.00, 0.50, 6.25, 25.00) | |
| 'gemini-2.5-pro' = @(1.25, 0.125, 0, 10.00) | |
| 'gemini-3-flash' = @(0.50, 0.05, 0, 3.00) | |
| 'gemini-3.1-pro' = @(2.00, 0.20, 0, 12.00) | |
| 'grok-code-fast-1' = @(0.20, 0.02, 0, 1.50) | |
| } | |
| return $table[$n] # $null if unknown | |
| } | |
| function Get-ModelTokenCost { | |
| param( | |
| [string]$modelId, | |
| [double]$inputTokens, | |
| [double]$outputTokens, | |
| [double]$cacheReadTokens = 0, | |
| [double]$cacheWriteTokens = 0 | |
| ) | |
| $rates = Get-PricingRates $modelId | |
| if ($null -eq $rates) { return $null } | |
| return (($inputTokens * $rates[0]) + | |
| ($cacheReadTokens * $rates[1]) + | |
| ($cacheWriteTokens * $rates[2]) + | |
| ($outputTokens * $rates[3])) / 1000000 | |
| } | |
| function Get-FirstNumber { | |
| # Returns the first value in a list that looks like a number, or $null. | |
| param([object[]]$candidates) | |
| foreach ($c in $candidates) { | |
| if ($null -ne $c -and "$c" -match '^[0-9]+(\.[0-9]+)?$') { | |
| return [double]"$c" | |
| } | |
| } | |
| return $null | |
| } | |
| function Get-FirstString { | |
| param([object[]]$candidates) | |
| foreach ($c in $candidates) { | |
| if ($null -ne $c -and "$c" -ne '') { return "$c" } | |
| } | |
| return $null | |
| } | |
| # ── Extract context window fields ───────────────────────────────────────────── | |
| $cw = $payload.context_window | |
| $usedTokens = Get-FirstNumber @($cw.current_context_tokens, $payload.currentTokens) | |
| $limitTokens= Get-FirstNumber @($cw.displayed_context_limit, $cw.limit, $payload.contextWindow.displayedContextLimit) | |
| $usedPct = Get-FirstNumber @($cw.used_percentage, $payload.contextWindow.usedPercentage) | |
| $durationMs = [long](Get-FirstNumber @( | |
| $payload.cost.total_duration_ms, | |
| $payload.cost.totalDurationMs, | |
| $payload.total_duration_ms, | |
| $payload.totalDurationMs, | |
| 0)) | |
| # ── Extract model name ──────────────────────────────────────────────────────── | |
| $modelRaw = Get-FirstString @( | |
| $payload.currentModel, | |
| $payload.cost.model, | |
| $(if ($payload.model -is [string]) { $payload.model } else { $null }), | |
| $payload.model.display_name, | |
| $payload.model.displayName, | |
| $payload.model.name, | |
| $payload.model.id, | |
| $payload.selectedModel, | |
| $payload.session.selectedModel, | |
| $payload.data.currentModel | |
| ) | |
| if (-not $modelRaw) { | |
| $settingsFile = "$env:USERPROFILE\.copilot\settings.json" | |
| if (Test-Path $settingsFile) { | |
| $modelRaw = (Get-Content $settingsFile -Raw | ConvertFrom-Json).model | |
| } | |
| } | |
| # ── Extract cost / token fields ─────────────────────────────────────────────── | |
| # Priority (matching the original Bash script): | |
| # 1. Payload-provided USD total | |
| # 2. AI credits × $0.01 | |
| # 3. Per-model metrics breakdown (modelMetrics) | |
| # 4. Single-model token estimate (input/output/cache tokens) | |
| # 5. Last-resort: context tokens as a proxy for input tokens | |
| $payloadCostUsd = Get-FirstNumber @( | |
| $payload.cost.total_cost_usd, $payload.cost.totalCostUsd, | |
| $payload.cost.total_usd, $payload.cost.totalUsd, | |
| $payload.cost.usd, | |
| $payload.cost.amount_usd, $payload.cost.amountUsd, | |
| $payload.cost.aic_gross_amount, $payload.cost.aicGrossAmount, | |
| $payload.usage.cost_usd, $payload.usage.costUsd, | |
| $payload.usage.aic_gross_amount,$payload.usage.aicGrossAmount, | |
| $payload.aic_gross_amount, $payload.aicGrossAmount, | |
| $payload.total_cost_usd, $payload.totalCostUsd | |
| ) | |
| $payloadAiCredits = Get-FirstNumber @( | |
| $payload.cost.aiCredits, $payload.cost.ai_credits, | |
| $payload.cost.aic_quantity, $payload.cost.aicQuantity, | |
| $payload.usage.aiCredits, $payload.usage.ai_credits, | |
| $payload.usage.aic_quantity,$payload.usage.aicQuantity, | |
| $payload.aic_quantity, $payload.aicQuantity | |
| ) | |
| $inputTokens = Get-FirstNumber @($payload.usage.inputTokens, $payload.usage.input_tokens, $payload.token_usage.inputTokens, $payload.cost.usage.inputTokens, $payload.cost.inputTokens, $payload.inputTokens, $payload.data.usage.inputTokens) | |
| $outputTokens = Get-FirstNumber @($payload.usage.outputTokens, $payload.usage.output_tokens, $payload.token_usage.outputTokens, $payload.cost.usage.outputTokens, $payload.cost.outputTokens, $payload.outputTokens, $payload.data.usage.outputTokens) | |
| $cacheReadTokens = Get-FirstNumber @($payload.usage.cacheReadTokens, $payload.usage.cachedInputTokens, $payload.usage.cached_input_tokens, $payload.cost.cacheReadTokens, $payload.cacheReadTokens) | |
| $cacheWriteTokens = Get-FirstNumber @($payload.usage.cacheWriteTokens,$payload.usage.cache_write_tokens,$payload.cost.cacheWriteTokens, $payload.cacheWriteTokens) | |
| # Resolve modelMetrics — an object or array keyed by model id | |
| $rawMetrics = $null | |
| foreach ($candidate in @( | |
| $payload.modelMetrics, $payload.data.modelMetrics, | |
| $payload.cost.modelMetrics, $payload.usage.modelMetrics, | |
| $payload.session.modelMetrics, $payload.models, | |
| $payload.usage.models, $payload.token_usage.models, | |
| $payload.tokenUsage.models)) { | |
| if ($null -ne $candidate) { $rawMetrics = $candidate; break } | |
| } | |
| $metricsEntries = @() # list of [modelId, in, out, cacheRead, cacheWrite] | |
| if ($null -ne $rawMetrics) { | |
| if ($rawMetrics -is [System.Collections.IEnumerable] -and $rawMetrics -isnot [string]) { | |
| # Array form | |
| foreach ($entry in $rawMetrics) { | |
| $mId = Get-FirstString @($entry.model, $entry.modelId, $entry.model_id, $entry.name, $entry.id) | |
| if ($mId) { | |
| $u = $entry.usage ?? $entry.tokenUsage ?? $entry.token_usage ?? $entry | |
| $metricsEntries += ,@($mId, | |
| ([double]($u.inputTokens ?? $u.input_tokens ?? 0)), | |
| ([double]($u.outputTokens ?? $u.output_tokens ?? 0)), | |
| ([double]($u.cacheReadTokens ?? $u.cachedInputTokens ?? $u.cached_input_tokens ?? 0)), | |
| ([double]($u.cacheWriteTokens ?? $u.cache_write_tokens ?? 0))) | |
| } | |
| } | |
| } else { | |
| # Object / hashtable form — each key is a model id | |
| foreach ($key in ($rawMetrics | Get-Member -MemberType NoteProperty).Name) { | |
| $entry = $rawMetrics.$key | |
| $u = $entry.usage ?? $entry.tokenUsage ?? $entry.token_usage ?? $entry | |
| $metricsEntries += ,@($key, | |
| ([double]($u.inputTokens ?? $u.input_tokens ?? 0)), | |
| ([double]($u.outputTokens ?? $u.output_tokens ?? 0)), | |
| ([double]($u.cacheReadTokens ?? $u.cachedInputTokens ?? $u.cached_input_tokens ?? 0)), | |
| ([double]($u.cacheWriteTokens ?? $u.cache_write_tokens ?? 0))) | |
| } | |
| } | |
| } | |
| # ── Compute cost ────────────────────────────────────────────────────────────── | |
| $costUsd = $null | |
| $costEstimated = $false | |
| if ($null -ne $payloadCostUsd) { | |
| # 1. Payload already contains a USD total — use it as-is | |
| $costUsd = $payloadCostUsd | |
| } elseif ($null -ne $payloadAiCredits) { | |
| # 2. AI credits: 1 credit = $0.01 | |
| $costUsd = $payloadAiCredits * 0.01 | |
| } elseif ($metricsEntries.Count -gt 0) { | |
| # 3. Sum cost across every model that appears in modelMetrics | |
| $total = 0.0 | |
| $anyFound = $false | |
| foreach ($e in $metricsEntries) { | |
| $c = Get-ModelTokenCost $e[0] $e[1] $e[2] $e[3] $e[4] | |
| if ($null -ne $c) { $total += $c; $anyFound = $true } | |
| } | |
| if ($anyFound) { $costUsd = $total; $costEstimated = $true } | |
| } elseif ($modelRaw -and ($null -ne $inputTokens -or $null -ne $outputTokens -or $null -ne $cacheReadTokens -or $null -ne $cacheWriteTokens)) { | |
| # 4. Single-model token estimate | |
| $c = Get-ModelTokenCost $modelRaw ($inputTokens ?? 0) ($outputTokens ?? 0) ($cacheReadTokens ?? 0) ($cacheWriteTokens ?? 0) | |
| if ($null -ne $c) { $costUsd = $c; $costEstimated = $true } | |
| } elseif ($modelRaw -and $null -ne $usedTokens) { | |
| # 5. Last resort: treat current context tokens as a proxy for input tokens | |
| $c = Get-ModelTokenCost $modelRaw $usedTokens 0 0 0 | |
| if ($null -ne $c) { $costUsd = $c; $costEstimated = $true } | |
| } | |
| # ── Build gauge ─────────────────────────────────────────────────────────────── | |
| $pctInt = $null | |
| if ($null -ne $usedPct) { | |
| $pctInt = [int][Math]::Round([Math]::Max(0, [Math]::Min(100, [double]$usedPct))) | |
| } elseif ($null -ne $usedTokens -and $null -ne $limitTokens -and $limitTokens -gt 0) { | |
| $pctInt = [int][Math]::Round([Math]::Max(0, [Math]::Min(100, ($usedTokens / $limitTokens) * 100))) | |
| } | |
| $gauge = "" | |
| if ($null -ne $pctInt) { | |
| $filled = [int]($pctInt / 10) | |
| $empty = 10 - $filled | |
| $gauge = ("█" * $filled) + ("░" * $empty) + " $pctInt%" | |
| } | |
| # ── Assemble output ─────────────────────────────────────────────────────────── | |
| $parts = @() | |
| $ctx = "🧠" | |
| if ($gauge) { $ctx += " $gauge" } | |
| elseif ($usedTokens -and $limitTokens) { $ctx += " Context" } | |
| if ($usedTokens -and $limitTokens) { $ctx += " $(Format-Tokens $usedTokens)/$(Format-Tokens $limitTokens)" } | |
| $parts += $ctx | |
| $modelCount = $metricsEntries.Count | |
| if ($modelCount -gt 1) { | |
| $display = if ($modelRaw) { Get-ModelName $modelRaw } else { "" } | |
| $parts += if ($display) { "* $display +$($modelCount - 1)" } else { "* $modelCount models" } | |
| } elseif ($modelRaw) { | |
| $parts += "* $(Get-ModelName $modelRaw)" | |
| } | |
| if ($null -ne $costUsd) { | |
| $formatted = Format-Usd $costUsd | |
| if ($formatted) { $parts += if ($costEstimated) { "~$formatted" } else { $formatted } } | |
| } | |
| $parts += "⏱️ $(Format-Duration $durationMs)" | |
| Write-Host ($parts -join " | ") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment