Skip to content

Instantly share code, notes, and snippets.

@wullemsb
Created May 22, 2026 08:54
Show Gist options
  • Select an option

  • Save wullemsb/740173f728a30712130afef39fe9cd3a to your computer and use it in GitHub Desktop.

Select an option

Save wullemsb/740173f728a30712130afef39fe9cd3a to your computer and use it in GitHub Desktop.
#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