Skip to content

Commit 94de456

Browse files
committed
Harden cx cc health output parity
1 parent 5479e0f commit 94de456

6 files changed

Lines changed: 420 additions & 81 deletions

File tree

Documents/PowerShell/Scripts/ai-env.ps1

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,9 @@ function Get-AiProbeModel {
11471147
[Parameter(Mandatory = $true)]$Profile
11481148
)
11491149

1150+
if ((Get-AiProfileMode -Profile $Profile) -ne "api") {
1151+
return "-"
1152+
}
11501153
$explicit = Get-AiProperty -Object $Profile -Name "probe_model"
11511154
if ($explicit) {
11521155
return [string]$explicit
@@ -1460,7 +1463,57 @@ function Test-AiProbeModelUnsupported {
14601463
param([string]$Detail)
14611464
if (-not $Detail) { return $false }
14621465
$l = $Detail.ToLowerInvariant()
1463-
return ($l -match "no available providers|model_not_found|model not found|model does not exist|unknown model|unsupported model|model .*not supported|not support.*model|invalid model|model_not_supported")
1466+
return ($l -match "no available providers|model_not_found|model not found|model does not exist|unknown model|unsupported model|model .*not supported|not support.*model|invalid model|model_not_supported|模型不存在|模型.*不存在|请检查模型代码")
1467+
}
1468+
1469+
function ConvertTo-AiHealthDisplayError {
1470+
param([string]$Detail)
1471+
if (-not $Detail) { return "" }
1472+
$text = ($Detail -replace "\s+", " ").Trim()
1473+
function ShortProbeDetail([string]$Item) {
1474+
$itemText = ($Item -replace "\s+", " ").Trim()
1475+
if (Test-AiProbeModelUnsupported $itemText) {
1476+
return "probe model unsupported; set probe_model"
1477+
}
1478+
if ($itemText -match "^(HTTP \d{3})(?:\s+(.+))?$") {
1479+
$code = $Matches[1]
1480+
$body = if ($Matches.Count -gt 2) { $Matches[2] } else { "" }
1481+
if (-not $body) { return $code }
1482+
try {
1483+
$json = $body | ConvertFrom-Json -ErrorAction Stop
1484+
$msg = $null
1485+
if ($json.error -and $json.error.message) { $msg = [string]$json.error.message }
1486+
elseif ($json.message) { $msg = [string]$json.message }
1487+
elseif ($json.error) { $msg = [string]$json.error }
1488+
elseif ($json.type) { $msg = [string]$json.type }
1489+
if ($msg) { return ($code + " " + (($msg -replace "\s+", " ").Trim())) }
1490+
} catch { }
1491+
if ($body -match '"message"\s*:\s*"([^"]+)"') {
1492+
return ($code + " " + (($Matches[1] -replace "\s+", " ").Trim()))
1493+
}
1494+
return $itemText
1495+
}
1496+
return $itemText
1497+
}
1498+
if ($text -match "^(POST\s+/\S+\s+->\s+)(.+?)(;\s+/\S+\s+->\s+)(.+)$") {
1499+
$prefix = $Matches[1]
1500+
$first = $Matches[2]
1501+
$middle = $Matches[3]
1502+
$second = $Matches[4]
1503+
return $prefix + (ShortProbeDetail $first) + $middle + (ShortProbeDetail $second)
1504+
}
1505+
if ($text -match "^(POST\s+/\S+\s+->\s+)(.+?)(;\s+but\s+/\S+\s+works\s+->\s+.+)$") {
1506+
$prefix = $Matches[1]
1507+
$first = $Matches[2]
1508+
$suffix = $Matches[3]
1509+
return $prefix + (ShortProbeDetail $first) + $suffix
1510+
}
1511+
if ($text -match "^(POST\s+/\S+\s+)(.+)$") {
1512+
$prefix = $Matches[1]
1513+
$detail = $Matches[2]
1514+
return $prefix + (ShortProbeDetail $detail)
1515+
}
1516+
return ShortProbeDetail $text
14641517
}
14651518

14661519
function Resolve-AiProfileHealth {
@@ -1472,10 +1525,10 @@ function Resolve-AiProfileHealth {
14721525
$st = if ($m.LatencyMs -gt $DegradedMs) { "degraded" } else { "healthy" }
14731526
return [pscustomobject]@{ Status = $st; LatencyMs = $m.LatencyMs; Method = "generation"; Error = $null }
14741527
}
1475-
$md = ConvertTo-AiProbeError $m.Detail
14761528
if (Test-AiProbeModelUnsupported $m.Detail) {
1477-
return [pscustomobject]@{ Status = "degraded"; LatencyMs = $m.LatencyMs; Method = "none"; Error = ("POST /v1/messages " + $md) }
1529+
return [pscustomobject]@{ Status = "degraded"; LatencyMs = $m.LatencyMs; Method = "none"; Error = ("POST /v1/messages " + $m.Detail) }
14781530
}
1531+
$md = ConvertTo-AiProbeError $m.Detail
14791532
if ($m.Code -eq 429 -or ($m.Code -ge 500 -and $m.Code -lt 600)) {
14801533
return [pscustomobject]@{ Status = "degraded"; LatencyMs = $m.LatencyMs; Method = "none"; Error = ("POST /v1/messages " + $md + " (transient)") }
14811534
}
@@ -1491,15 +1544,16 @@ function Resolve-AiProfileHealth {
14911544
}
14921545
$effD = ConvertTo-AiProbeError $eff.Detail
14931546
$altD = ConvertTo-AiProbeError $alt.Detail
1494-
$note = "POST /$($Plan.EffLabel) -> $effD"
1547+
$note = "POST /$($Plan.EffLabel) -> $($eff.Detail)"
14951548
if ($alt.Ok) {
14961549
$note += "; but /$($Plan.AltLabel) works -> set wire_api = `"$($Plan.AltLabel)`" in config.toml"
14971550
return [pscustomobject]@{ Status = "degraded"; LatencyMs = $eff.LatencyMs; Method = "none"; Error = $note }
14981551
}
14991552
if ((Test-AiProbeModelUnsupported $eff.Detail) -or (Test-AiProbeModelUnsupported $alt.Detail)) {
1500-
$note += "; /$($Plan.AltLabel) -> $altD"
1553+
$note += "; /$($Plan.AltLabel) -> $($alt.Detail)"
15011554
return [pscustomobject]@{ Status = "degraded"; LatencyMs = $eff.LatencyMs; Method = "none"; Error = $note }
15021555
}
1556+
$note = "POST /$($Plan.EffLabel) -> $effD"
15031557
if ($eff.Code -eq 429 -or ($eff.Code -ge 500 -and $eff.Code -lt 600)) {
15041558
$note += "; /$($Plan.AltLabel) -> $altD (transient)"
15051559
return [pscustomobject]@{ Status = "degraded"; LatencyMs = $eff.LatencyMs; Method = "none"; Error = $note }
@@ -2041,6 +2095,7 @@ function Write-CodexSwitchStatus {
20412095
Write-Host " Profile: <default> ($profilePath missing)"
20422096
}
20432097
Write-Host " Base URL: $(Get-CodexBaseUrl -Profile $Profile)"
2098+
Write-Host " Probe model: $(Get-AiProbeModel -Tool codex -Profile $Profile)"
20442099
Write-Host " Cached login: $(Get-CodexLoginStatusText)"
20452100

20462101
if ($mode -eq "api") {
@@ -2069,6 +2124,7 @@ function Write-ClaudeSwitchStatus {
20692124
Write-Host "Claude Code state switched: $(Get-AiProfileName -Profile $Profile)"
20702125
Write-Host " Run next: claude"
20712126
Write-Host " Registry: $script:AiRegistryPath"
2127+
Write-Host " Probe model: $(Get-AiProbeModel -Tool claude -Profile $Profile)"
20722128
if ($mode -eq "api") {
20732129
Write-Host " ANTHROPIC_BASE_URL: $env:ANTHROPIC_BASE_URL"
20742130
Write-Host " ANTHROPIC_API_KEY: $(Format-AiSecretPreview $env:ANTHROPIC_API_KEY)"
@@ -2080,6 +2136,8 @@ function Write-ClaudeSwitchStatus {
20802136
Write-Host " Subscription status: local Claude login is used if present"
20812137
}
20822138

2139+
$h = Get-AiProfileHealthCached -Tool "claude" -Profile $Profile -CacheOnly
2140+
Write-Host (" Health: " + (Format-AiHealthCell $h) + $(if ($h.Error) { " " + $h.Error } else { '' }))
20832141
Write-ClaudeExternalStatus
20842142
}
20852143

@@ -2088,13 +2146,13 @@ function Show-CxHelp {
20882146
cx - switch Codex state for this PowerShell session
20892147
20902148
Usage:
2091-
cx Cycle through enabled Codex profiles
2149+
cx Auto-select a cached healthy Codex profile (default fallback)
20922150
cx sub Use a named subscription profile
20932151
cx sub:work Use another subscription profile, if registered
20942152
cx api Use the default API profile
20952153
cx api:docker Use a named API profile
2096-
cx list List registry profiles and local file status
2097-
cx status Print current saved/process state; --refresh re-probes health
2154+
cx list List registry profiles and cached health
2155+
cx status Print current saved/process state; --fresh/--refresh re-probes selected profile
20982156
cx stats Summarize local rollout token usage
20992157
cx add-api NAME Register a Codex API profile that shares ~/.codex by default
21002158
Options: --base-url URL --env-key NAME --provider-name NAME
@@ -2103,11 +2161,11 @@ Usage:
21032161
cx add-sub NAME Register an isolated Codex subscription CODEX_HOME
21042162
cx remove NAME Remove a Codex profile registration
21052163
cx probe-model NAME [MODEL] Set/clear health-probe model override
2106-
cx default [NAME] Show/set the default (primary) profile
2107-
cx edit Open the profile registry (profiles.json) in $EDITOR
2108-
cx health Probe & report profile health (🟢🟡🔴, parallel); --fresh re-probes
2109-
cx doctor Run codex doctor full diagnostic (slow, on-demand)
2110-
cx health-clear Clear the health probe cache
2164+
cx default [NAME] Show/set the default (primary) profile
2165+
cx edit Open the profile registry (profiles.json) in EDITOR
2166+
cx health Probe & report profile health table (🟢🟡🔴, parallel); --fresh/--refresh re-probes
2167+
cx doctor Run codex doctor full diagnostic (slow, on-demand)
2168+
cx health-clear Clear the health probe cache
21112169
cx next Cycle to the next enabled profile
21122170
cx help Show this help
21132171
@@ -2126,6 +2184,7 @@ Notes:
21262184
API mode does not run codex login --with-api-key; it loads OPENAI_API_KEY only for this shell.
21272185
Multiple API profiles can share ~/.codex. Multiple subscription accounts need separate home values.
21282186
Add commands only write profile metadata and Codex config. Put real tokens in secrets.toml.
2187+
Without probe_model, Codex probes use runtime/global config.toml model, then a cheap fallback.
21292188
Legacy ~/.ai-secrets/*.ps1 files are still accepted as a fallback.
21302189
"@ | Write-Host
21312190
}
@@ -2135,23 +2194,23 @@ function Show-CcHelp {
21352194
cc - switch Claude Code state for this PowerShell session
21362195
21372196
Usage:
2138-
cc Cycle through enabled Claude Code profiles
2197+
cc Auto-select a cached healthy Claude Code profile (default fallback)
21392198
cc sub Clear Anthropic API env and use local Claude subscription login
21402199
cc sub:work Use another subscription profile, if registered
21412200
cc api Use the default API profile
21422201
cc api:docker Use a named API profile
2143-
cc list List registry profiles and local file status
2144-
cc status Print current saved/process state; --refresh re-probes health
2202+
cc list List registry profiles and cached health
2203+
cc status Print current saved/process state; --fresh/--refresh re-probes selected profile
21452204
cc add-api NAME Register a Claude Code API profile
21462205
Options: --base-url URL --env-key NAME --env KEY=VALUE (repeatable)
21472206
Prompts for missing base-url and the secret in a terminal.
21482207
cc add-sub NAME Register a Claude Code subscription label
21492208
cc remove NAME Remove a Claude Code profile registration
21502209
cc probe-model NAME [MODEL] Set/clear health-probe model override
2151-
cc default [NAME] Show/set the default (primary) profile
2152-
cc edit Open the profile registry (profiles.json) in $EDITOR
2153-
cc health Probe & report profile health (🟢🟡🔴, parallel); --fresh re-probes
2154-
cc health-clear Clear the health probe cache
2210+
cc default [NAME] Show/set the default (primary) profile
2211+
cc edit Open the profile registry (profiles.json) in EDITOR
2212+
cc health Probe & report profile health table (🟢🟡🔴, parallel); --fresh/--refresh re-probes
2213+
cc health-clear Clear the health probe cache
21552214
cc next Cycle to the next enabled profile
21562215
cc help Show this help
21572216
@@ -2170,6 +2229,7 @@ Notes:
21702229
CLAUDE_CODE_AUTO_COMPACT_WINDOW) stored in the registry; exported on switch and
21712230
cleared when switching to another profile so values do not leak.
21722231
Add commands only write profile metadata. Put real tokens in secrets.toml.
2232+
Without probe_model, Claude probes use ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL, then a cheap fallback.
21732233
Legacy ~/.ai-secrets/*.ps1 files are still accepted as a fallback.
21742234
"@ | Write-Host
21752235
}
@@ -2192,7 +2252,7 @@ function Format-AiHealthCell {
21922252
function Test-AiFreshFlag {
21932253
param([AllowNull()][string[]]$Tokens)
21942254
if (-not $Tokens) { return $false }
2195-
foreach ($t in $Tokens) { if ($t -in @("--fresh", "-f")) { return $true } }
2255+
foreach ($t in $Tokens) { if ($t -in @("--fresh", "-f", "--refresh", "-r")) { return $true } }
21962256
return $false
21972257
}
21982258

@@ -2475,15 +2535,15 @@ function Show-AiHealth {
24752535
$plan = Get-AiProfileProbePlan -Tool $Tool -Profile $p
24762536
if ($plan.ContainsKey("Early")) {
24772537
$e = $plan.Early
2478-
$display[$n] = @{ Health = (& $cellOf $e); Method = ($(if ($e.Method) { $e.Method } else { "-" })); Note = ($(if ($e.Error) { $e.Error } else { "" })); Pending = $false }
2538+
$display[$n] = @{ Health = (& $cellOf $e); Method = ($(if ($e.Method) { $e.Method } else { "-" })); Note = ($(if ($e.Error) { ConvertTo-AiHealthDisplayError $e.Error } else { "" })); Pending = $false }
24792539
continue
24802540
}
24812541
$plans[$n] = $plan
24822542
$useCache = $false
24832543
if (-not $Fresh) {
24842544
$cached = Get-AiProfileHealthCached -Tool $Tool -Profile $p -CacheOnly
24852545
if ($cached.Cached) {
2486-
$display[$n] = @{ Health = (& $cellOf $cached); Method = ($(if ($cached.Method) { $cached.Method } else { "-" })); Note = ($(if ($cached.Error) { $cached.Error } else { "" })); Pending = $false }
2546+
$display[$n] = @{ Health = (& $cellOf $cached); Method = ($(if ($cached.Method) { $cached.Method } else { "-" })); Note = ($(if ($cached.Error) { ConvertTo-AiHealthDisplayError $cached.Error } else { "" })); Pending = $false }
24872547
$useCache = $true
24882548
}
24892549
}
@@ -2552,7 +2612,7 @@ function Show-AiHealth {
25522612
Write-AiHealthCacheEntry -Key "$Tool.$n" -Entry ([pscustomobject]@{
25532613
status = $h.Status; latencyMs = $h.LatencyMs; method = $h.Method; error = $h.Error; probedAt = $now
25542614
})
2555-
$display[$n] = @{ Health = (Format-AiHealthCell $h); Method = ($(if ($h.Method) { $h.Method } else { "-" })); Note = ($(if ($h.Error) { $h.Error } else { "" })); Pending = $false }
2615+
$display[$n] = @{ Health = (Format-AiHealthCell $h); Method = ($(if ($h.Method) { $h.Method } else { "-" })); Note = ($(if ($h.Error) { ConvertTo-AiHealthDisplayError $h.Error } else { "" })); Pending = $false }
25562616
}
25572617

25582618
$footer = " (health " + ($(if ($Fresh) { "re-probed (fresh, parallel)" } else { "cached <=5min" })) + "; '" + $Tool + " health --fresh' re-probe, '" + $Tool + " health-clear' clears)"
@@ -2646,7 +2706,7 @@ foreach ($c in $Candidates) {
26462706
$h = Resolve-AiProfileHealth -Plan $runspaces[$n].Plan -Results $results -DegradedMs $DegradedMs
26472707
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
26482708
Write-AiHealthCacheEntry -Key "$Tool.$n" -Entry ([pscustomobject]@{ status = $h.Status; latencyMs = $h.LatencyMs; method = $h.Method; error = $h.Error; probedAt = $now })
2649-
$display[$n] = @{ Health = (Format-AiHealthCell $h); Method = ($(if ($h.Method) { $h.Method } else { "-" })); Note = ($(if ($h.Error) { $h.Error } else { "" })); Pending = $false }
2709+
$display[$n] = @{ Health = (Format-AiHealthCell $h); Method = ($(if ($h.Method) { $h.Method } else { "-" })); Note = ($(if ($h.Error) { ConvertTo-AiHealthDisplayError $h.Error } else { "" })); Pending = $false }
26502710
$done += $n
26512711
}
26522712
}
@@ -2705,6 +2765,7 @@ function Show-CodexStatus {
27052765
Write-Host " OPENAI_API_KEY: $(Format-AiSecretPreview $env:OPENAI_API_KEY)"
27062766
Write-Host " Cached login: $(Get-CodexLoginStatusText)"
27072767
if ($profile) {
2768+
Write-Host " Probe model: $(Get-AiProbeModel -Tool codex -Profile $profile)"
27082769
$h = Get-AiProfileHealthCached -Tool "codex" -Profile $profile -Fresh:$Fresh -CacheOnly:(-not $Fresh)
27092770
Write-Host (" Health: " + (Format-AiHealthCell $h) + $(if ($h.Error) { " " + $h.Error } else { '' }))
27102771
}
@@ -2735,6 +2796,7 @@ function Show-ClaudeStatus {
27352796
$cprofile = Get-AiProfileByName -Tool "claude" -Name ($env:AI_CLAUDE_LABEL ?? $saved)
27362797
if (-not $cprofile) { $cprofile = Get-AiProfileByName -Tool "claude" -Name (Get-AiDefaultProfileName -Tool "claude") }
27372798
if ($cprofile) {
2799+
Write-Host " Probe model: $(Get-AiProbeModel -Tool claude -Profile $cprofile)"
27382800
$h = Get-AiProfileHealthCached -Tool "claude" -Profile $cprofile -Fresh:$Fresh -CacheOnly:(-not $Fresh)
27392801
Write-Host (" Health: " + (Format-AiHealthCell $h) + $(if ($h.Error) { " " + $h.Error } else { '' }))
27402802
}

0 commit comments

Comments
 (0)