Public/Get-FreeTierStatus.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Get-FreeTierStatus { <# .SYNOPSIS Reports live free-tier configuration and usage from a deployed server. .DESCRIPTION Queries GET /api/proxy/usage and reshapes the response into a single FreeTierStatus object. Turns "why is this anonymous user getting 429?" from a server-log investigation into one command. Background: diagnosing t/1061 (token budget exhaustion) was slowed because /health used to hardcode tokensPerDay instead of reading the runtime config. This cmdlet sources from /api/proxy/usage which already reads the live config + actual usage counter. AllowedRoutes is a hardcoded list mirroring the server's free-tier route table (the server doesn't expose it via API). If you change the free-tier route set server-side, also update this list. .PARAMETER BaseUrl Target server URL. Defaults to the current production URL. .PARAMETER TimeoutSec HTTP request timeout (1-30, default 10). .EXAMPLE Get-FreeTierStatus .EXAMPLE Get-FreeTierStatus -BaseUrl https://staging.example.com | Format-List .EXAMPLE (Get-FreeTierStatus).BudgetUtilizationPct .LINK Show-AITriadHelp .LINK Test-TaxEditorHealth .LINK Test-TaxEditorEndpoints .LINK Test-AnonymousDebateFlow .LINK Test-PersonaEndpoints .LINK Test-ServiceWorkerHealth .LINK Invoke-TaxEditorSmokeTest #> [CmdletBinding()] param( [Parameter(Position = 0)] [string]$BaseUrl = (Get-TaxEditorBaseUrl), [ValidateRange(1, 30)] [int]$TimeoutSec = 10 ) Set-StrictMode -Version Latest # Hardcoded mirror of the server's free-tier route table. # Update when the server-side free-tier route set changes. $AllowedRoutes = @( '/api/auth/anonymous', '/api/ai/generate', '/api/embeddings/compute', '/api/embeddings/query', '/api/debates/:id', '/api/flight-recorder/server-dump', '/api/flight-recorder/download-merged/:dumpId' ) $BaseUrl = $BaseUrl.TrimEnd('/') $Check = Invoke-RemoteCheck -BaseUrl $BaseUrl -Path '/api/proxy/usage' ` -TimeoutSec $TimeoutSec -AcceptableStatusCodes @(200) if (-not $Check.Success) { New-ActionableError ` -Goal 'Query free-tier status' ` -Problem ("GET /api/proxy/usage failed: HTTP {0} {1}" -f $Check.StatusCode, $Check.Error) ` -Location 'Get-FreeTierStatus' ` -NextSteps @( "Verify -BaseUrl is correct: $BaseUrl", 'Run Test-TaxEditorHealth to confirm the server is reachable', 'Check that the server version exposes /api/proxy/usage (server.ts:1420)' ) -Throw } $Body = $Check.Body if (-not $Body -or -not $Body.PSObject.Properties['limits'] -or -not $Body.PSObject.Properties['usage']) { New-ActionableError ` -Goal 'Parse free-tier status response' ` -Problem 'Response missing expected limits/usage fields' ` -Location 'Get-FreeTierStatus' ` -NextSteps @('Check server version compatibility', "Raw response: $($Body | ConvertTo-Json -Depth 3 -Compress)") ` -Throw } $Limits = $Body.limits $Usage = $Body.usage $DailyBudget = if ($Limits.PSObject.Properties['tokensPerDay']) { [int]$Limits.tokensPerDay } else { 0 } $TokensUsed = if ($Usage.PSObject.Properties['tokensToday']) { [int]$Usage.tokensToday } else { 0 } $RPMLimit = if ($Limits.PSObject.Properties['requestsPerMinute']) { [int]$Limits.requestsPerMinute } else { 0 } $Remaining = [Math]::Max(0, $DailyBudget - $TokensUsed) $UtilPct = if ($DailyBudget -gt 0) { [Math]::Round(($TokensUsed / $DailyBudget) * 100, 2) } else { 0 } # Milestone warning thresholds (absolute token counts at 50%/80%/95%) $Milestones = [ordered]@{ '50pct' = [Math]::Round($DailyBudget * 0.50) '80pct' = [Math]::Round($DailyBudget * 0.80) '95pct' = [Math]::Round($DailyBudget * 0.95) } # LastResetTime: server returns resetsAt = next UTC midnight; the last reset # was 24h before that. $LastReset = $null if ($Usage.PSObject.Properties['resetsAt'] -and $Usage.resetsAt) { try { $ResetsAt = [DateTimeOffset]::Parse([string]$Usage.resetsAt).UtcDateTime $LastReset = $ResetsAt.AddDays(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') } catch { } } $Tier = if ($Body.PSObject.Properties['tier']) { [string]$Body.tier } else { 'unknown' } $Result = [FreeTierStatus]::new() $Result.Tier = $Tier $Result.DailyTokenBudget = $DailyBudget $Result.TokensUsedToday = $TokensUsed $Result.TokensRemainingToday = $Remaining $Result.BudgetUtilizationPct = $UtilPct $Result.RPMLimit = $RPMLimit $Result.AllowedRoutes = $AllowedRoutes $Result.MilestoneWarnings = $Milestones $Result.LastResetTime = $LastReset $Result.BaseUrl = $BaseUrl $Result } |