Skip to content

Commit ff7396a

Browse files
Hugo-Grellierclaude
andcommitted
fix: use builtin rate_limits in PowerShell script
Use rate_limits data from Claude Code JSON input before falling back to OAuth API. Adds stampede lock and Format-EpochResetTime helper. Cherry-picked from upstream PR daniel3303#20. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f77666c commit ff7396a

1 file changed

Lines changed: 78 additions & 31 deletions

File tree

statusline.ps1

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,16 @@ function Get-OAuthToken {
188188
return $null
189189
}
190190

191-
# ===== Usage limits with caching =====
191+
# ===== Usage limits =====
192+
# First, try to use rate_limits data provided directly by Claude Code in the JSON input.
193+
$builtinFiveHourPct = $data.rate_limits.five_hour.used_percentage
194+
$builtinFiveHourReset = $data.rate_limits.five_hour.resets_at
195+
$builtinSevenDayPct = $data.rate_limits.seven_day.used_percentage
196+
$builtinSevenDayReset = $data.rate_limits.seven_day.resets_at
197+
198+
$useBuiltin = ($null -ne $builtinFiveHourPct) -or ($null -ne $builtinSevenDayPct)
199+
200+
# Fall back to cached API call only when Claude Code didn't supply rate_limits data
192201
$cacheDir = Join-Path $env:TEMP "claude"
193202
$cacheFile = Join-Path $cacheDir "statusline-usage-cache.json"
194203
$cacheMaxAge = 60 # seconds between API calls
@@ -198,37 +207,45 @@ if (-not (Test-Path $cacheDir)) { New-Item -ItemType Directory -Path $cacheDir -
198207
$needsRefresh = $true
199208
$usageData = $null
200209

201-
# Check cache
202-
if (Test-Path $cacheFile) {
203-
$cacheMtime = (Get-Item $cacheFile).LastWriteTime
204-
$cacheAge = ((Get-Date) - $cacheMtime).TotalSeconds
205-
if ($cacheAge -lt $cacheMaxAge) {
206-
$needsRefresh = $false
207-
$usageData = Get-Content $cacheFile -Raw
210+
if (-not $useBuiltin) {
211+
# Check cache — shared across all Claude Code instances to avoid rate limits
212+
if (Test-Path $cacheFile) {
213+
$cacheMtime = (Get-Item $cacheFile).LastWriteTime
214+
$cacheAge = ((Get-Date) - $cacheMtime).TotalSeconds
215+
if ($cacheAge -lt $cacheMaxAge) {
216+
$needsRefresh = $false
217+
$usageData = Get-Content $cacheFile -Raw
218+
}
208219
}
209-
}
210220

211-
# Fetch fresh data if cache is stale
212-
if ($needsRefresh) {
213-
$token = Get-OAuthToken
214-
if ($token) {
215-
try {
216-
$headers = @{
217-
"Accept" = "application/json"
218-
"Content-Type" = "application/json"
219-
"Authorization" = "Bearer $token"
220-
"anthropic-beta" = "oauth-2025-04-20"
221-
"User-Agent" = "claude-code/2.1.34"
222-
}
223-
$response = Invoke-RestMethod -Uri "https://api.anthropic.com/api/oauth/usage" `
224-
-Headers $headers -Method Get -TimeoutSec 10 -ErrorAction Stop
225-
$usageData = $response | ConvertTo-Json -Depth 10
226-
$usageData | Set-Content $cacheFile -Force
227-
} catch {}
228-
}
229-
# Fall back to stale cache
230-
if (-not $usageData -and (Test-Path $cacheFile)) {
231-
$usageData = Get-Content $cacheFile -Raw
221+
# Fetch fresh data if cache is stale
222+
if ($needsRefresh) {
223+
# Touch cache immediately (stampede lock)
224+
if (Test-Path $cacheFile) {
225+
(Get-Item $cacheFile).LastWriteTime = Get-Date
226+
} else {
227+
New-Item -ItemType File -Path $cacheFile -Force | Out-Null
228+
}
229+
$token = Get-OAuthToken
230+
if ($token) {
231+
try {
232+
$headers = @{
233+
"Accept" = "application/json"
234+
"Content-Type" = "application/json"
235+
"Authorization" = "Bearer $token"
236+
"anthropic-beta" = "oauth-2025-04-20"
237+
"User-Agent" = "claude-code/2.1.34"
238+
}
239+
$response = Invoke-RestMethod -Uri "https://api.anthropic.com/api/oauth/usage" `
240+
-Headers $headers -Method Get -TimeoutSec 10 -ErrorAction Stop
241+
$usageData = $response | ConvertTo-Json -Depth 10
242+
$usageData | Set-Content $cacheFile -Force
243+
} catch {}
244+
}
245+
# Fall back to stale cache
246+
if (-not $usageData -and (Test-Path $cacheFile)) {
247+
$usageData = Get-Content $cacheFile -Raw
248+
}
232249
}
233250
}
234251

@@ -245,9 +262,39 @@ function Format-ResetTime([string]$isoStr, [string]$style) {
245262
} catch { return $null }
246263
}
247264

265+
# Format Unix epoch reset time to compact local time
266+
function Format-EpochResetTime([object]$epoch, [string]$style) {
267+
if ($null -eq $epoch -or "$epoch" -eq "null" -or "$epoch" -eq "") { return $null }
268+
try {
269+
$dt = [DateTimeOffset]::FromUnixTimeSeconds([long]$epoch).LocalDateTime
270+
switch ($style) {
271+
"time" { return $dt.ToString("h:mmtt").ToLower() }
272+
"datetime" { return $dt.ToString("MMM d, h:mmtt").ToLower() }
273+
default { return $dt.ToString("MMM d").ToLower() }
274+
}
275+
} catch { return $null }
276+
}
277+
248278
$sep = " ${dim}|${reset} "
249279

250-
if ($usageData) {
280+
if ($useBuiltin) {
281+
# ---- Use rate_limits data provided directly by Claude Code in JSON input ----
282+
if ($null -ne $builtinFiveHourPct) {
283+
$fiveHourPct = [math]::Floor([double]$builtinFiveHourPct)
284+
$fiveHourColor = Get-UsageColor $fiveHourPct
285+
$out += "${sep}${white}5h${reset} ${fiveHourColor}${fiveHourPct}%${reset}"
286+
$fiveHourReset = Format-EpochResetTime $builtinFiveHourReset "time"
287+
if ($fiveHourReset) { $out += " ${dim}@${fiveHourReset}${reset}" }
288+
}
289+
290+
if ($null -ne $builtinSevenDayPct) {
291+
$sevenDayPct = [math]::Floor([double]$builtinSevenDayPct)
292+
$sevenDayColor = Get-UsageColor $sevenDayPct
293+
$out += "${sep}${white}7d${reset} ${sevenDayColor}${sevenDayPct}%${reset}"
294+
$sevenDayReset = Format-EpochResetTime $builtinSevenDayReset "datetime"
295+
if ($sevenDayReset) { $out += " ${dim}@${sevenDayReset}${reset}" }
296+
}
297+
} elseif ($usageData) {
251298
try {
252299
$usage = if ($usageData -is [string]) { $usageData | ConvertFrom-Json } else { $usageData }
253300

0 commit comments

Comments
 (0)