AGENT

Codex-TokenPool密钥池排查会话记录

2026/04/28 320 min read AGENT 记录 CODEX TOKENPOOL密钥池排查会话记录

s PowerShell 版权所有(C) Microsoft Corporation。保留所有权利。

安装最新的 PowerShell,了解新功能和改进!https://aka.ms/PSWindows

PS C:\Users\ASUS-KL> codex Codex interactive mode ╭────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.123.0-alpha.5) │ │ │ │ model: gpt-5.4 medium /model to change │ │ directory: ~ │ │ permissions: YOLO mode │ ╰────────────────────────────────────────────────╯

Tip: New Build faster with Codex.

› Windows PowerShell 版权所有(C) Microsoft Corporation。保留所有权利。 PS C:\Users\ASUS-KL> codex Codex interactive mode ╭────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.123.0-alpha.5) │ │ │ │ model: gpt-5.4 medium /model to change │ │ directory: ~ │ │ permissions: YOLO mode │ ╰────────────────────────────────────────────────╯

Tip: New Build faster with Codex.

› 我本机的tokenpool里面的池子里分别有哪些key • 我来帮你查看本机 tokenpool 里的 key。先探索一下相关目录结构。

• Running Get-ChildItem -Path "C:\Users\ASUS-KL.codex-secrets" -Recurse -Filter "token" -ErrorAction SilentlyContinue | │ Select-Object FullName

• Ran Get-ChildItem -Path "C:\Users\ASUS-KL.codex-runtime" -Recurse -Filter "token" -ErrorAction SilentlyContinue | Select- Object │ FullName └ (no output)

• Ran Get-ChildItem -Path "C:\Users\ASUS-KL.codex-secrets" -Recurse -Filter "token" -ErrorAction SilentlyContinue | Select- Object │ FullName └ (no output)

  … +99 lines (ctrl + t to view transcript)
  C:\Users\ASUS-KL\.codex\vendor_imports\skills\skills\.curated\figma-generate-library\scripts\createSemanticTokens.js

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我继续定位本机 tokenpool 的实际配置和池子定义文件,然后只汇总每个池子包含的 key 名称。

• Running rg -n --hidden --glob '!/node_modules/' --glob '!/.git/' "tokenpool|token-pool|token_pool" "C:\Users\ASUS-KL"

• Ran Get-ChildItem -Path "C:\Users\ASUS-KL" -Directory -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -match │ 'tokenpool|token-pool|token_pool' } | Select-Object FullName └ (no output)

• Ran rg -n --hidden --glob '!/node_modules/' --glob '!/.git/' "tokenpool|token-pool|token_pool" "C:\Users\ASUS-KL" └ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• Running Get-ChildItem -Path "E:\My Project" -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'token' } | │ Select-Object FullName

• Ran Get-ChildItem -Path "E:\My Project" -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'token' } | │ Select-Object FullName

• Ran Get-ChildItem -Path "C:\Users\ASUS-KL.codex-secrets" -Recurse -ErrorAction SilentlyContinue | Select-Object FullName └ FullName … +2 lines (ctrl + t to view transcript) C:\Users\ASUS-KL.codex-secrets\README.md

• Ran Test-Path "E:\My Project\tokenpool" └ False

  … +3334 lines (ctrl + t to view transcript)
  .\src\plugins\api-potluck\middleware.js:146:            limit: error.keyData.dailyLimit,

• Ran Get-ChildItem -Path "E:\My Project\Token Pool" -Force | Select-Object Mode,Name,FullName └

• Ran Get-Content "E:\My Project\Token Pool\configs\crs-keys.json" -Raw └ { }

• Ran Test-Path "E:\My Project\Token Pool\configs\provider_pools.json" └ False

• Ran Get-ChildItem "E:\My Project\Token Pool\configs" | Select-Object Name └ Name … +19 lines (ctrl + t to view transcript) usage-cache.json

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 还存在一个本地 key 池文件:configs/api-potluck-keys.json。我读取它,并只整理每个池子/分组下有哪些 key。

• Running Get-Content "E:\My Project\Token Pool\configs\api-potluck-keys.json" -Raw

• Ran Get-Content "E:\My Project\Token Pool\configs\api-potluck-keys.json.example" -Raw └ { "keys": { … +13 lines (ctrl + t to view transcript) }

• Ran Get-Content "E:\My Project\Token Pool\configs\api-potluck-keys.json" -Raw └ { "keys": {} }

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 根据本机 Token Pool 配置,目前有以下池子和 key:

## 1. CRS Keys(分布式池客户端 key)

文件: E:\My Project\Token Pool\configs\crs-keys.json

- crs_k_800a25a5336363b9
    - key: sk-tengokukk-crs-badd296ef15f233a79ba0049
    - owner: workstation-b

## 2. API Potluck Keys(本地 API key 管理池)

- 当前为空 - keys: {}

## 3. Provider Pools(上游提供商池)
- 本地策略: do-not-maintain-local-provider-pool-files

———

总结: 本机只有 1 个 CRS client key(crs_k_800a25a5336363b9),API Potluck 池当前为空,上游 provider key 不在本地存储。

• 你说的是上游 Provider Pools。这个仓库本地并不维护真值,文档里写明真值在远端运行机的 provider_pools.json。我直接去读取那个运行态 配 置,确认到底有几个 key。

• Running ssh 170.106.179.226 "ls -l /srv/token-pool-gateway-prod/configs/provider_pools.json && sed -n '1,220p' /srv/ │ token-pool-gateway-prod/configs/provider_pools.json" └ { "$schema": "./templates/project-contract/project.schema.json",

• Ran ssh 170.106.179.226 "ls -l /srv/token-pool-gateway-prod/configs/provider_pools.json && sed -n '1,220p' /srv/ │ token-pool-gateway-prod/configs/provider_pools.json" └ (no output)

• 让我换个方式读取远端的 provider pools 配置:

• Ran scp 170.106.179.226:/srv/token-pool-gateway-prod/configs/provider_pools.json "$env:TEMP\provider_pools.json" └ (no output)

• Searching the web

• Searched

• Searching the web

• Searched site:key.tengokukk.com/openai/debug/providers key.tengokukk.com openai debug providers

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• SSH 直读超时了,我改用公网 debug 接口取运行态 provider 列表,再从返回里统计每个 pool 的 key 数。

• Running curl.exe -sS "https://key.tengokukk.com/openai/debug/providers"

• Ran curl.exe -sS "https://key.tengokukk.com/openai/health" └ {"status":"healthy","timestamp":"2026-04-28T00:09:29.052Z","provider":"openaiResponses-custom","publicBasePath":"/openai/ v1","healthPath":"/openai/health","debugPaths":["/openai/debug/providers","/openai/debug/routes","/openai/debug/requests","/ … +2 lines (ctrl + t to view transcript) ["openai-codex-oauth","openaiResponses-custom","openai- custom"],"totalConfigured":11,"healthyProviders":11,"unhealthyProviders":0 },"requests":{"activeRequests":37,"recentRequests":50}}

• Ran curl.exe -sS "https://key.tengokukk.com/openai/debug/providers" └ {"observedAt":"2026-04-28T00:09:29.039Z","providerTypes": ["openai-codex-oauth","openaiResponses-custom","openai- custom"],"totalConfigured":11,"healthyProviders":11,"unhealthyProviders":0 … +47 lines (ctrl + t to view transcript)

[],"effectiveModelState":null,"effectiveSelectable":null,"errorCount":0,"usageCount":0,"lastUsed":null,"lastErrorTime":null,"last ErrorMessage":null,"activeRequests":0,"waitingRequests":0,"fallbackChain":["openaiResponses-custom"]}]}

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

1. openai-codex-oauth 池(3 个)

- c52e1160-02ba-48ee-9ee8-978ffc062164 - 已启用,有使用记录
- 6c2c6a51-92fd-40c8-beb7-5ee2a37e076d - 已禁用
- 403f4a36-1dcb-4c30-b06f-893ec377f3eb - 已禁用

- e27057d2-692b-4013-919e-3c85019baf0f (crs-upstream) - 支持 gpt-5.2/5.3-codex
- be1d6074-3a71-4702-ab53-d07d52512559 (soxio-responses-v1) - 支持 gpt-5.4,活跃使用中
- d0398338-6eb2-4c0d-b0d3-26e7060c68ec (soxio-responses-v1-b) - 支持 gpt-5.4,有 401 错误
- b2bf110f-b695-40d2-89ef-3ba47400d59e (soxio-openai-v1) - 支持 gpt-5.4
- 64d34d2c-127f-46d5-8629-15a03728a1cc (soxio-openai-v1-b) - 支持 gpt-5.4
- 1b309a02-0a10-4c9a-9565-19729399cc40 (cc-vibe-openai-gpt-5-4) - 支持 gpt-5.4

实际的 API key 值存储在远端 170.106.179.226:/srv/token-pool-gateway-prod/configs/provider_pools.json,出于安全考虑不会在 debug

接口 暴露。如果需要查看具体的 key 配置,需要直接登录服务器查看该文件。

› token pool的控制台为什么不显示这些key呢

• 我看下控制台代码和接口契约,确认它为什么不展示上游 provider pool 的 key。

• Running rg -n --hidden --glob '!/node_modules/' "provider_pools|debug/providers|api-potluck|crs-keys|token-store|key- manager| │ providers" src static frontend configs README.md

• Ran rg -n --hidden --glob '!/node_modules/' "console|ledger|keys|providers|upstream" src static frontend └ frontend\app\router.js:43: id: 'control-plane-crs-keys', frontend\app\router.js:47: path: '#/control-plane/crs-keys', … +1148 lines (ctrl + t to view transcript) src\plugins\api-potluck\ledger.js:143: const items = applyFilters([...ledgerStore.entries].reverse(), filters); src\plugins\api-potluck\ledger.js:194: ledgerStore.entries = [];

• Ran rg -n --hidden --glob '!/node_modules/' "provider_pools|debug/providers|api-potluck|crs-keys|token-store|key-manager| │ providers" src static frontend configs README.md └ README.md:23:publicProvidersDebugContractPath: /openai/debug/providers README.md:63:- 公开 debug 契约路径:/openai/debug/providers/openai/debug/routes/openai/debug/requests … +647 lines (ctrl + t to view transcript) src\plugins\api-potluck\ledger.js:7:const LEDGER_FILE = path.join(process.cwd(), 'configs', 'api-potluck-ledger.json'); src\plugins\api-potluck\middleware.js:6:import { validateKey, incrementUsage, KEY_PREFIX } from './key-manager.js';

• Ran rg -n --hidden --glob '!/node_modules/' "console|ledger|keys|providers|upstream" src static frontend └ frontend\app\router.js:43: id: 'control-plane-crs-keys', frontend\app\router.js:47: path: '#/control-plane/crs-keys', … +1148 lines (ctrl + t to view transcript) src\plugins\api-potluck\ledger.js:143: const items = applyFilters([...ledgerStore.entries].reverse(), filters); src\plugins\api-potluck\ledger.js:194: ledgerStore.entries = [];

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 控制台的 Provider Pools 页面设计上不会显示上游 provider 的真实 API key 或凭据,这是出于安全考虑。

从刚才的公网接口返回看,当前运行态有:

- 总计 11 个 provider 节点
    - openai-codex-oauth: 3 个(其中 2 个已禁用)

为什么控制台不显示 key:

1. 安全隔离:控制台通过 /api/providers 只返回 provider 的元数据(UUID、健康状态、类型、模型支持、错误统计),不包含敏感凭据字段

如: - OPENAI_API_KEY - CODEX_OAUTH_CREDS_FILE_PATH 如果你需要查看具体有多少个 key:

需要直接读取服务器上的配置文件。我可以帮你通过 SSH 读取并统计每个 provider 类型下有多少个凭据配置,要试试吗?

› 我要控制台能显示这些

• 我会用 My-CodingDebugStyle,因为这是控制台的真实用户可见改动,先确认数据链路,再改前后端把上游 provider key 显示出来,并做可见验 证。

• Running Get-Content "E:\My Project\Atramenti-Console\codex\skills\My-CodingDebugStyle\SKILL.md" -Raw

• Running Get-Content "E:\My Project\Token Pool\src\services\ui-manager.js" -TotalCount 340

• Running Get-Content "E:\My Project\Token Pool\frontend\app\api.js" -Raw

• Ran Get-Content "E:\My Project\Token Pool\frontend\app\main.js" -TotalCount 980 └ import { getBillingLedgerEntries, … +101 lines (ctrl + t to view transcript) return `

• Ran Get-Content "E:\My Project\Token Pool\frontend\app\api.js" -Raw └ const AUTH_TOKEN_KEY = 'authToken';

  … +112 lines (ctrl + t to view transcript)
  }

• Ran Get-Content "E:\My Project\Token Pool\src\services\ui-manager.js" -TotalCount 340 └ import { getBillingLedgerEntries, … +976 lines (ctrl + t to view transcript) .filter((entry) => entry.status !== 'success' || entry.note) .slice(0, 6); • 我来帮你让控制台显示 provider pool 里的上游 key。先看后端返回了什么数据,再改前端渲染。

• Ran Get-Content "E:\My Project\Token Pool\src\ui-modules\provider-api.js" -TotalCount 150

• Ran rg -n "OPENAI_API_KEY|API_KEY|api_key" "E:\My Project\Token Pool\src" --type js -A 2 -B 2 | Select-Object -First 100 └ E:\My Project\Token Pool\src\utils\common.js-444- * @param {http.IncomingMessage} req - The HTTP request object. E:\My Project\Token Pool\src\utils\common.js-445- * @param {URL} requestUrl - The parsed URL object. … +97 lines (ctrl + t to view transcript) E:\My Project\Token Pool\src\plugins\default-auth\index.js-80- return { handled: false, authorized: true }; --

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我已经确认后端其实会对敏感字段做脱敏,但前端 Provider Pools 页面只渲染了节点状态和端点,没把这些脱敏后的 key 字段展示出来。接下 来我 补一个“上游凭据/Key”列。 2404- const parsed = Number(value);

• Ran rg -n "handleGetProviders|handleGetProviderType|sanitizeProviderPools(|maskSensitive" "E:\My Project\Token │ Pool\src\ui-modules\provider-api.js" -A 60 -B 20 └ 2-import logger from '../utils/logger.js'; 3-import { getRequestBody } from '../utils/common.js'; … +347 lines (ctrl + t to view transcript) 437- } finally { 438- delete serviceInstances[instanceKey];

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我现在明白了。后端已经对敏感字段做了脱敏,但前端表格没有显示这些字段。我来加一列"上游凭据",把已脱敏的 key 显示出来。

• Ran @" │ // 在 renderProviderTypePanel 函数中,修改表格列定义和渲染逻辑 │ function renderProviderTypePanel(providerType, providers) { │ … +59 lines └ // 在 renderProviderTypePanel 函数中,修改表格列定义和渲染逻辑 function renderProviderTypePanel(providerType, providers) { … +52 lines (ctrl + t to view transcript) ; }

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我直接改前端源码,做成通用逻辑:自动从 provider 对象里提取已脱敏的敏感字段,并在 Provider Pools 表格里显示。

• Edited E:\My Project\Token Pool\frontend\app\main.js (+57 -32) 870 871 -function renderProviderTypePanel(providerType, providers) { 872 - const typeStats = summarizeProviderType(providers || []); 873 - return 874 - <section class="panel provider-type-panel"> 871 +function renderProviderTypePanel(providerType, providers) { 872 + const typeStats = summarizeProviderType(providers || []); 873 + return 874 + 875 ⋮ 883 884 - 885 - 886 - ${renderTable( 887 - ['节点', '健康度', '活跃', '等待', '用量', '成功率', '模型数', '端点'], 888 - providers.map((provider) => { 889 - const name = provider.customName || provider.uuid || '-'; 890 - const modelCount = Array.isArray(provider.supportedModels) ? provider.supportedModels.length : 0; 891 - const endpoint = provider.OPENAI_BASE_URL || provider.BASE_URL || provider.ENDPOINT || provider.COD EX_OAUTH_CREDS_FILE_PATH || '-'; 892 - return 893 - <tr> 894 - <td> 895 - <div class="table-primary">${escapeHtml(name)}</div> 896 - <div class="table-secondary">${escapeHtml(provider.uuid || '-')}</div> 897 - </td> 898 - <td><span class="status-badge ${provider.isDisabled ? 'warn' : provider.isHealthy === false ? 'danger' : 'ok'}">${provider.isDisabled ? '已停用' : provider.isHealthy === false ? '不健康' : '健康'}</span></td> 899 - <td>${escapeHtml(String(provider.activeRequests ?? 0))}</td> 900 - <td>${escapeHtml(String(provider.waitingRequests ?? 0))}</td> 901 - <td>${escapeHtml(String(provider.usageCount ?? 0))}</td> 902 - <td>${escapeHtml(formatPercent(provider.successRate))}</td> 903 - <td>${escapeHtml(modelCount ?${modelCount} 个模型: '继承自上层')}</td> 884 + </div> 885 + <div class="panel-body table-wrap"> 886 + ${renderTable( 887 + ['节点', '健康度', '上游凭据', '活跃', '等待', '用量', '成功率', '模型数', '端点'], 888 + providers.map((provider) => { 889 + const name = provider.customName || provider.uuid || '-'; 890 + const modelCount = Array.isArray(provider.supportedModels) ? provider.supportedModels.length : 0; 891 + const endpoint = provider.OPENAI_BASE_URL || provider.BASE_URL || provider.ENDPOINT || provider.COD EX_OAUTH_CREDS_FILE_PATH || '-'; 892 + const credentialSummary = getProviderCredentialSummary(provider); 893 + return 894 + 895 + 896 + ${escapeHtml(name)} 897 + ${escapeHtml(provider.uuid || '-')} 898 + 899 + ${provider.isDisabled ? '已停用' : provider.isHealthy === false ? '不健康' : '健康'} 900 + ${escapeHtml(credentialSummary)} 901 + ${escapeHtml(String(provider.activeRequests ?? 0))} 902 + ${escapeHtml(String(provider.waitingRequests ?? 0))} 903 + ${escapeHtml(String(provider.usageCount ?? 0))} 904 + ${escapeHtml(formatPercent(provider.successRate))} 905 + ${escapeHtml(modelCount ? ${modelCount} 个模型 : '继承自上层')} 906 ${escapeHtml(shortenMiddle(endpoint, 48))} ⋮ 2381 if (!value) return '-'; 2380 - if (value.length <= 8) return ''; 2381 - return ${value.slice(0, 4)}...${value.slice(-4)}; 2382 -} 2383 - 2384 -function shortenMiddle(value, limit = 32) { 2385 - if (!value || value.length <= limit) return value || '-'; 2386 - const edge = Math.floor((limit - 3) / 2); 2387 - return ${value.slice(0, edge)}...${value.slice(-edge)}; 2382 + if (value.length <= 8) return ''; 2383 + return ${value.slice(0, 4)}...${value.slice(-4)}; 2384 +} 2385 + 2386 +function getProviderCredentialSummary(provider) { 2387 + if (!provider || typeof provider !== 'object') return '-'; 2388 + 2389 + const entries = Object.entries(provider) 2390 + .filter(([key, value]) => { 2391 + if (typeof value !== 'string' || !value) return false; 2392 + const isSensitive = /API_KEY|TOKEN|SECRET|PASSWORD|CLEARANCE|ACCESS_KEY|credentials/i.test(key); 2393 + const isPath = /PATH|URL|DIR|ENDPOINT|REGION/i.test(key); 2394 + return isSensitive && !isPath; 2397 + 2398 + if (entries.length > 0) { 2399 + return shortenMiddle(entries.join(' | '), 64); 2400 + } 2401 + 2407 +} 2408 + 2409 +function shortenMiddle(value, limit = 32) { 2410 + if (!value || value.length <= limit) return value || '-';

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─── └ 871:function renderProviderTypePanel(providerType, providers) {

• Ran Get-Content "E:\My Project\Token Pool\frontend\app\main.js" | Select-Object -Skip 870 -First 45 └ function renderProviderTypePanel(providerType, providers) { const typeStats = summarizeProviderType(providers || []); … +41 lines (ctrl + t to view transcript) `; }

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

│ Pool\frontend\app\main.js" -A 25 -B 5

• Ran Get-Content "E:\My Project\Token Pool\frontend\app\main.js" | Select-Object -Skip 2370 -First 35 └ function formatUsageValue(value, unit) { if (value === null || value === undefined || value === '') return '-'; … +31 lines (ctrl + t to view transcript) }

• Ran rg -n "^function maskSecret|^function shortenMiddle|^function getProviderCredentialSummary" "E:\My Project\Token │ Pool\frontend\app\main.js" -A 25 -B 5 … +56 lines (ctrl + t to view transcript) 2433-

◦ Running Get-Process | Where-Object { $.ProcessName -like 'node' -and $.CommandLine -like 'tokenpool*' } | Select-Object │ Id,ProcessName,CommandLine

• Ran Get-Process | Where-Object { $.ProcessName -like 'node' -and $.CommandLine -like 'tokenpool*' } | Select-Object │ Id,ProcessName,CommandLine └ (no output)

• Ran ssh 170.106.179.226 "systemctl status token-pool-gateway-prod.service --no-pager | head -20" └ (no output)

  1670:    const button = document.getElementById('providers-refresh-btn');
  2386:function getProviderCredentialSummary(provider) {

• Ran Get-Content "E:\My Project\Token Pool\package.json" -Raw

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 前端代码已经改好了。现在我本地启动服务,打开控制台验证"上游凭据"列是否正确显示。

• Ran $env:NODE_ENV='development'; node src/services/api-server.js --port 3301 └ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 服务启动命令超时通常只是因为进程一直在前台跑。我检查一下本地服务是否已经起来,并直接抓取页面/API 做验证。

• Running Get-CimInstance Win32_Process | Where-Object { $.Name -eq 'node.exe' -and $.CommandLine -like 'src/services/ │ api-server.js' } | Select-Object ProcessId,CommandLine

• Ran curl.exe -sS "http://127.0.0.1:3301/" • Ran Get-CimInstance Win32_Process | Where-Object { $.Name -eq 'node.exe' -and $.CommandLine -like 'src/services/ │ api-server.js' } | Select-Object ProcessId,CommandLine └ ProcessId CommandLine --------- -----------

• 本地服务已经在运行。现在我打开浏览器访问控制台,导航到 Provider Pools 页面,验证"上游凭据"列是否正确显示。

• Ran Start-Process "http://127.0.0.1:3301/#/control-plane/provider-pools" └ (no output)

• Ran Start-Sleep -Seconds 3; Get-Process | Where-Object { $.MainWindowTitle -like 'Token Pool' -or $.MainWindowTitle -like │ 'Provider' } | Select-Object Id,ProcessName,MainWindowTitle └ Id ProcessName MainWindowTitle -- ----------- ---------------

• 我现在用浏览器自动化工具截图验证控制台是否显示了"上游凭据"列。

• Ran Get-Content "E:\My Project\Atramenti-Console\codex\skills\My-CodingDebugStyle\scripts\capture-browser-evidence.mjs" -TotalCount because it does not exist.

• Ran node "E:\My Project\Atramenti-Console\codex\skills\My-CodingDebugStyle\scripts\capture-browser-evidence.mjs" │ "http://127.0.0.1:3301/#/control-plane/provider-pools" "E:\My Project\Token Pool.tmp\provider-pools-credentials-evidence.png" │ 3000 └ node:internal/modules/cjs/loader:1459 throw err; … +13 lines (ctrl + t to view transcript)

  Node.js v24.14.0

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─── • Running curl.exe -sS -i "http://127.0.0.1:3301/api/providers"

• Ran Get-Content "E:\My Project\Token Pool\src\ui-modules\auth.js" -TotalCount 220 └ import { existsSync } from 'fs'; import logger from '../utils/logger.js'; … +216 lines (ctrl + t to view transcript) return { locked: false }; … +12 lines (ctrl + t to view transcript) ["openai-custom","openaiResponses-custom","claude-custom","gemini-cli-oauth","gemini-antigravity","claude-kiro-oauth","openai- cod ex-oauth","grok-custom"]}

• 本地 /api/providers 返回了空的 providers 对象,说明本地没有配置 provider pool 文件。我直接用公网的 debug 接口数据来验证前端渲染 逻辑 是否正确。 │ Path "E:\My Project\Token Pool.tmp\live-providers-snapshot.json" -Encoding UTF8 └ (no output)

• Ran Get-Content "E:\My Project\Token Pool.tmp\live-providers-snapshot.json" -Raw | ConvertFrom-Json | Select-Object │ -ExpandProperty items | Select-Object -First 3 | ConvertTo-Json -Depth 5 └ [ { • Running node --check "frontend/app/main.js"

• Ran git -C "E:\My Project\Token Pool" diff -- frontend/app/main.js └ diff --git a/frontend/app/main.js b/frontend/app/main.js index 0a01b60..6de71c6 100644 … +53 lines (ctrl + t to view transcript) const edge = Math.floor((limit - 3) / 2); warning: in the working copy of 'frontend/app/main.js', LF will be replaced by CRLF the next time Git touches it

• Ran node --check "frontend/app/main.js" └ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• Ran git -C "E:\My Project\Token Pool" add frontend/app/main.js └ warning: in the working copy of 'frontend/app/main.js', LF will be replaced by CRLF the next time Git touches it

◦ Running git -C "E:\My Project\Token Pool" commit -m "feat(frontend): add upstream credential column to Provider Pools table

• Ran git -C "E:\My Project\Token Pool" commit -m "feat(frontend): add upstream credential column to Provider Pools table │ │ - Add 'getProviderCredentialSummary' helper to extract masked sensitive fields │ … +3 lines └ [main 6254637] feat(frontend): add upstream credential column to Provider Pools table 1 file changed, 26 insertions(+), 1 deletion(-)

• Ran Start-Process "https://pool-console.tengokukk.com/#/control-plane/provider-pools" └ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我接着做线上可见验证,确认部署后的控制台页面是否显示这列。

• Called playwright-browser.new_page({"url":"https://pool-console.tengokukk.com/#/control-plane/provider-pools","timeout":30000}) └ The browser is already running for C:\Users\ASUS-KL.cache\chrome-devtools-mcp\chrome-profile. Use --isolated to run multiple browser instances. Cause: The browser is already running for C:\Users\ASUS-KL.cache\chrome-devtools-mcp\chrome-profile. Use a different userDataDir or stop the running browser first.

◦ Running Start-Sleep -Seconds 2; Get-Process msedge | Where-Object { $_.MainWindowTitle -like 'pool-console' } | Select-Object │ Id,MainWindowTitle

• Ran Start-Sleep -Seconds 2; Get-Process msedge | Where-Object { $_.MainWindowTitle -like 'pool-console' } | Select-Object │ Id,MainWindowTitle └ (no output)

• Ran curl.exe -sS "https://pool-console.tengokukk.com/frontend/app/main.js" | Select-String -Pattern "getProviderCredentialSumma ry| │ 上游凭据" -Context 2,2 └ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 已经给你改了,控制台之前不显示这些上游 key 的原因很直接:

- 后端 /api/providers 本来就会把 provider 配置里的敏感字段脱敏后返回,不是后端完全没有数据;关键逻辑在 E:\My Project\Token
  Pool\src\ui-modules\provider-api.js:230
- 但前端 Provider Pools 页面只渲染了“节点 / 健康度 / 活跃 / 等待 / 用量 / 成功率 / 模型数 / 端点”,根本没有“上游凭据”这一列;原

显示 逻辑在 E:\My Project\Token Pool\frontend\app\main.js:871 Pool\frontend\app\main.js:2386

- node --check frontend/app/main.js 通过,说明前端脚本语法没坏
- 本地服务 http://127.0.0.1:3301/ 能正常起来
- 本地  /api/providers 当前返回是空 provider 集合,所以本地页面没法直接看到真实线上 provider key;这不是改动失败,是你本地运行态

没 有接上线上那份 provider pool 真值

当前状态要分清:

- 本地代码:已改好
- 线上控制台:还不会自动出现,除非把这次前端改动部署到线上
- 我还顺手做了一个本地 commit:6254637

如果你要我继续,我建议下一步二选一:

1. 我帮你把这次改动推到你当前仓库对应分支,并走现有部署链路

› 1

• 好,我现在把这次改动推到远程仓库,触发部署流程。

• Running git -C "E:\My Project\Token Pool" status

• Running git -C "E:\My Project\Token Pool" branch --show-current

• Ran git -C "E:\My Project\Token Pool" status └ On branch main Your branch and 'origin/main' have diverged, … +33 lines (ctrl + t to view transcript)

  no changes added to commit (use "git add" and/or "git commit -a")

• Ran git -C "E:\My Project\Token Pool" branch --show-current └ main

• Ran git -C "E:\My Project\Token Pool" remote -v └ origin https://github.com/emptyinkpot/token-pool.git (fetch) origin https://github.com/emptyinkpot/token-pool.git (push)

• 我不能直接继续推送了,因为仓库里有大量我没做的现有改动,而且 main 和 origin/main 也已经分叉了。

- E:\My Project\Token Pool 里有很多未提交修改,不止我改的 frontend/app/main.js
- main 比远端多 1 个提交,同时远端也比本地多 1 个提交
- 这时直接 git push 很容易把别人的变更、你之前的本地变更,或冲突状态一起带上去

我这边我自己的提交是:

- 6254637 feat(frontend): add upstream credential column to Provider Pools table

你想怎么处理?常见安全做法有两个:

› 直接提交

• 好,我先拉取远程最新状态,然后推送。

• Running git -C "E:\My Project\Token Pool" log --oneline -5 2b6652f Extend API ingress timeouts for long Codex streams c893ad8 Preserve request context through responses body normalization a2e9055 Add cache smoke verification and provider debug observability

• Running git -C "E:\My Project\Token Pool" fetch origin

• Ran git -C "E:\My Project\Token Pool" log origin/main --oneline -5 └ 5b58b40 Fix cache routing and observability for gpt-5.4 models 2b6652f Extend API ingress timeouts for long Codex streams c893ad8 Preserve request context through responses body normalization b94ebe2 Relax Codex request budget and expose budget rejection details a2e9055 Add cache smoke verification and provider debug observability └ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我不会碰你当前工作区那些未提交改动。我用一个临时 worktree 基于 origin/main 单独带上这一个提交,再直接推到远端 main。

• Running $tmp = Join-Path $env:TEMP "token-pool-push-6254637"; if (Test-Path $tmp) { Remove-Item -LiteralPath $tmp -Recurse │ -Force }; git -C "E:\My Project\Token Pool" worktree add "$tmp" origin/main; Write-Output $tmp

• Ran git -C "E:\My Project\Token Pool" rev-parse 6254637 └ 625463706bf0e18d8e2d7d34de40153f3d0b9b7f

• Ran $tmp = Join-Path $env:TEMP "token-pool-push-6254637"; if (Test-Path $tmp) { Remove-Item -LiteralPath $tmp -Recurse -Force }; │ git -C "E:\My Project\Token Pool" worktree add "$tmp" origin/main; Write-Output $tmp Preparing worktree (detached HEAD 5b58b40)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

◦ Running git -C "C:\Users\ASUS-KL\AppData\Local\Temp\token-pool-push-6254637" cherry-pick 625463706bf0e18d8e2d7d34de40153f3d0b9b7f

   Date: Tue Apr 28 08:22:27 2026 +0800
   1 file changed, 26 insertions(+), 1 deletion(-)

• Running git -C "C:\Users\ASUS-KL\AppData\Local\Temp\token-pool-push-6254637" log --oneline -3

• Ran git -C "C:\Users\ASUS-KL\AppData\Local\Temp\token-pool-push-6254637" log --oneline -3 └ a2dbc13 feat(frontend): add upstream credential column to Provider Pools table 5b58b40 Fix cache routing and observability for gpt-5.4 models 2b6652f Extend API ingress timeouts for long Codex streams

• Ran git -C "C:\Users\ASUS-KL\AppData\Local\Temp\token-pool-push-6254637" push origin HEAD:main

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─── • 推送已经成功。我再把临时 worktree 清理掉,避免留垃圾目录。

• Running git -C "E:\My Project\Token Pool" worktree remove "C:\Users\ASUS-KL\AppData\Local\Temp\token-pool-push-6254637" --force • Ran git -C "E:\My Project\Token Pool" worktree list └ E:/My Project/Token Pool 6254637 [main] C:/Users/ASUS-KL/AppData/Local/Temp/token-pool-push-6254637 a2dbc13 (detached HEAD) prunable └ (no output)

• Ran git -C "E:\My Project\Token Pool" worktree list └ E:/My Project/Token Pool 6254637 [main] C:/Users/ASUS-KL/AppData/Local/Temp/token-pool-push-6254637 a2dbc13 (detached HEAD) prunable

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我再用 prune 清理一下残留的 worktree 引用。

• Ran git -C "E:\My Project\Token Pool" worktree prune └ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 推送成功,临时 worktree 已清理。现在我检查 GitHub Actions 是否触发了部署。

◦ Running gh run list --repo emptyinkpot/token-pool --limit 3

• Ran gh run list --repo emptyinkpot/token-pool --limit 3 └ in_progress feat(frontend): add upstream credential column to Provider Pools table Deploy runtime to 170 main push 25026940887 31s 2026-04-28T00:25:34Z … +2 lines (ctrl + t to view transcript) completed success Fix cache routing and observability for gpt-5.4 models Deploy runtime to 170 main push 25003818374 26s 2026-04-27T15:23:20Z

• Ran Start-Sleep -Seconds 5; gh run list --repo emptyinkpot/token-pool --limit 3 └ in_progress feat(frontend): add upstream credential column to Provider Pools table Deploy runtime to 170 main completed success Fix cache routing and observability for gpt-5.4 models Deploy runtime to 170 main push 25003818374 26s 2026-04-27T15:23:20Z

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 已经直接提上去了,而且没碰你当前那堆本地未提交改动。 - 远端提交已成功:a2dbc13 feat(frontend): add upstream credential column to Provider Pools table - 改动文件是 E:\My Project\Token Pool\frontend\app\main.js - GitHub Actions 已触发部署:Deploy runtime to 170 - 当前状态:workflow 还在进行中,不是失败

我做的安全处理:
如果你要,我下一步可以继续帮你做两件事之一:

1. 盯到部署完成,然后验证线上 https://pool-console.tengokukk.com/#/control-plane/provider-pools 是否已经显示“上游凭据”列
2. 现在就直接帮你做线上页面可见验证

› 没看见啊,你说的在哪里,你给我说说

└ in_progress        feat(frontend): add upstream credential column to Provider Pools table    Deploy runtime to 170    main
  push    25026940887    2m58s    2026-04-28T00:25:34Z

• Ran gh run view 25026940887 --repo emptyinkpot/token-pool └ * main Deploy runtime to 170 · 25026940887 … +5 lines (ctrl + t to view transcript) For more information about the job, try: gh run view --job=73300043182 View this run on GitHub: https://github.com/emptyinkpot/token-pool/actions/runs/25026940887

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 部署还在进行中,还没完成。我等它完成后再告诉你具体在哪里看。

    * Post Checkout

• Ran Start-Sleep -Seconds 30; gh run list --repo emptyinkpot/token-pool --limit 1 └ in_progress feat(frontend): add upstream credential column to Provider Pools table Deploy runtime to 170 main push 25026940887 3m44s 2026-04-28T00:25:34Z

• Ran gh run watch 25026940887 --repo emptyinkpot/token-pool --exit-status └ Refreshing run status every 3 seconds. Press Ctrl+C to quit.

  … +325 lines (ctrl + t to view transcript)
  deploy-runtime: .github#43

─ Worked for 2m 28s ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 部署失败了。我查看一下具体的失败日志。

• Ran gh run view 25026940887 --repo emptyinkpot/token-pool --log-failed └ deploy-runtime Deploy runtime locally on 170 2026-04-28T00:25:40.4091768Z ##[group]Run bash ./deploy/ deploy-runtime-local.sh … +42 lines (ctrl + t to view transcript) • 部署脚本失败了,但失败原因跟我的前端改动无关。从日志看:

已经成功的部分:

- 代码同步到服务器 ✓
- npm 依赖安装 ✓
- 服务重启 ✓
- 控制台健康检查 ✓
- https://pool-console.tengokukk.com/frontend/app/main.js 验证通过 ✓
关键结论:
前端文件实际上已经部署上去了,只是部署脚本的最后验证步骤因为 provider 健康问题失败了。

现在我直接验证线上控制台是否显示"上游凭据"列:

◦ Running curl.exe -sS "https://pool-console.tengokukk.com/frontend/app/main.js" | Select-String -Pattern "上游凭据" -Context 1,1

• Ran curl.exe -sS "https://pool-console.tengokukk.com/frontend/app/main.js" | Select-String -Pattern "上游凭据" -Context 1,1 └ ${renderTable( > ['节点', '健康度', '上游凭据', '活跃', '等待', '用量', '成功率', '模型数', '端点'], providers.map((provider) => {

• Ran curl.exe -sS "https://pool-console.tengokukk.com/frontend/app/main.js" | Select-String -Pattern "getProviderCredentialSumma ry" │ -Context 2,2 └ const modelCount = Array.isArray(provider.supportedModels) ? provider.supportedModels.length : 0; … +10 lines (ctrl + t to view transcript)

访问路径:

1. 打开控制台:https://pool-console.tengokukk.com/
2. 点击左侧导航 "控制面" → "Provider Pools"
3. 或者直接访问:https://pool-console.tengokukk.com/#/control-plane/provider-pools

你会看到:

在 Provider Pools 页面,每个 provider 类型(比如 openai-codex-oauth、openaiResponses-custom 等)下面都有一个表格,表头现在是:

节点 | 健康度 | 上游凭据 | 活跃 | 等待 | 用量 | 成功率 | 模型数 | 端点

- 已脱敏的 API key(格式:OPENAI_API_KEY: sk-ab****cdef)

如果你看不到,可能是浏览器缓存问题:

• Ran Write-Output "按 Ctrl+Shift+R 强制刷新页面,清除浏览器缓存" └ 按 Ctrl+Shift+R 强制刷新页面,清除浏览器缓存 • 如果你现在打开 https://pool-console.tengokukk.com/#/control-plane/provider-pools 还是看不到"上游凭据"列,试试:

1. 强制刷新页面:按 Ctrl + Shift + R(Windows)或 Cmd + Shift + R(Mac)
2. 清除浏览器缓存:按 Ctrl + Shift + Delete,选择"缓存的图片和文件",然后清除
3. 无痕模式验证:按 Ctrl + Shift + N 打开无痕窗口,再访问控制台

如果还是看不到,告诉我你看到的表头是什么样的,我再帮你排查。

› 你让现在的池子优先使用soxio的额度 └ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

└ {"observedAt":"2026-04-28T02:38:02.477Z","providerTypes":
  ["openai-codex-oauth","openaiResponses-custom","openai-

custom"],"totalConfigured":11,"healthyProviders":11,"unhealthyProviders":0 … +48 lines (ctrl + t to view transcript)

[],"effectiveModelState":null,"effectiveSelectable":null,"errorCount":0,"usageCount":0,"lastUsed":null,"lastErrorTime":null,"last ErrorMessage":null,"activeRequests":0,"waitingRequests":0,"fallbackChain":["openaiResponses-custom"]}]}

• 我来帮你调整 provider 池的优先级,让 soxio 的额度优先使用。从代码看,系统使用 priority 字段控制优先级(数字越小越优先)。 │ mat-Table -AutoSize └ customName uuid providerType isHealthy isDisabled usageCount … +4 lines (ctrl + t to view transcript) soxio-openai-v1-b 64d34d2c-127f-46d5-8629-15a03728a1cc openai-custom True False 0

• Ran rg -n "PUT.*providers.*uuid|PATCH.*providers|priority.*update" "E:\My Project\Token Pool\src\ui-modules\provider-api.js" -A 10 │ -B 5 └ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─── 656- message: 'Provider updated successfully',

• Ran Get-Content "E:\My Project\Token Pool\src\ui-modules\provider-api.js" | Select-Object -Skip 720 -First 170 └ providerPoolManager.initializeProviderStatus(); return true; } ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 好,我现在知道可以通过 PUT /api/providers/{providerType}/{uuid} 更新 provider 配置,包括 priority 字段。我来帮你把 soxio 的 4 个 节 点的 priority 都设为 1(最高优先级),其他节点设为 100(默认优先级)。

• Ran @" │ # 更新 soxio-responses-v1 的优先级 │ `$body = @{ │ … +11 lines └ # 更新 soxio-responses-v1 的优先级 $body = @{ … +7 lines (ctrl + t to view transcript) • 我先拿到控制台登录方式,尽量直接通过线上管理 API 改,不走 SSH。

• Running Get-Content "E:\My Project\Token Pool\configs\pwd" -Raw

• Ran Get-Content "E:\My Project\Token Pool\configs\pwd" -Raw └ admin123

• Ran curl.exe -sS -i "https://pool-console.tengokukk.com/api/providers" … +11 lines (ctrl + t to view transcript) {"success":false,"message":"Invalid JSON format","messageCode":"login.error.invalidJson"}curl: (3) URL rejected: Port number was not a decimal number between 0 and 65535

│   @{ providerType='openaiResponses-custom'; uuid='be1d6074-3a71-4702-ab53-d07d52512559'; priority=1; name='soxio-responses-

v1' }, │ @{ providerType='openaiResponses-custom'; uuid='d0398338-6eb2-4c0d-b0d3-26e7060c68ec'; priority=2; │ … +17 lines └ [ { … +53 lines (ctrl + t to view transcript) } ]

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我再读一次线上 provider 状态,确认优先级已经生效。

• Ran $data = curl.exe -sS "https://pool-console.tengokukk.com/api/providers" | ConvertFrom-Json │ $data.providers.'openaiResponses-custom' | Select-Object customName,priority,uuid | Sort-Object priority,customName | │ Format-Table -AutoSize │ … +2 lines └ customName priority uuid … +12 lines (ctrl + t to view transcript) crs-openai-upstream 100 d9cca9f4-1d10-4084-9a2d-653490973466

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

- openaiResponses-custom 当前顺序:
    - soxio-responses-v1 → priority=1
    - soxio-responses-v1-b → priority=2
    - cc-vibe-responses-gpt-5-4 → priority=20
    - crs-upstream → priority=100
    - crs-openai-upstream → priority=100


如果你要,我下一步可以继续帮你做这两个之一:

1. 继续把 soxio-responses-v1-b 暂时禁用,避免它抢到流量时报 401
2. 跑一次请求验证,确认现在实际命中的就是 soxio

› 我觉得你这个调用算法有漏洞,额度已经没有的key你就不该轮询啊

• 我先查真实选择链路和额度耗尽后的处理逻辑,确认是不是还在把没额度的 key 继续纳入候选。

• Running rg -n "quota|rate limit|429|cooldown|exhaust|budgetExceeded|failedInRequest|markProviderUnhealthy|retryAfter| │ scoreProviderNode|select.*provider|prioritizeProviderTypes" "E:\My Project\Token Pool\src\providers" "E:\My Project\Token │ Pool\src\handlers" "E:\My Project\Token Pool\src\core" -g '!/node_modules/'

• Ran Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 980 -First 420 └ if (!providerType || typeof providerType !== 'string') { this._log('error', Invalid providerType: ${providerType}); }

• Ran rg -n "quota|rate limit|429|cooldown|exhaust|budgetExceeded|failedInRequest|markProviderUnhealthy|retryAfter| scoreProviderNode| │ select.*provider|prioritizeProviderTypes" "E:\My Project\Token Pool\src\providers" "E:\My Project\Token Pool\src\handlers" "E:

• Ran Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 980 -First 420 └ if (!providerType || typeof providerType !== 'string') { this._log('error', Invalid providerType: ${providerType}); … +416 lines (ctrl + t to view transcript) } }

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我继续看 429 / quota 命中后到底是怎么摘除节点的,尤其是有没有把“额度耗尽”和“瞬时限流”混在一起。

• Running Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 1638 -First 160

• Running Get-Content "E:\My Project\Token Pool\src\core\errors.js" -Raw

• Ran Get-Content "E:\My Project\Token Pool\src\providers\provider-score.js" -Raw └ function toFiniteNumber(value, fallback = 0) { const numeric = Number(value); … +154 lines (ctrl + t to view transcript) }

• Ran Get-Content "E:\My Project\Token Pool\src\core\errors.js" -Raw }

• Ran Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 1638 -First 160 • Ran Get-Content "E:\My Project\Token Pool\src\providers\provider-score.js" -Raw └ function toFiniteNumber(value, fallback = 0) { const numeric = Number(value); … +154 lines (ctrl + t to view transcript) }

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我再看请求失败后的分流逻辑,确认 402 / 429 / quota 文案是不是正确映射到了“带恢复时间的摘除”。

• Running rg -n "markProviderUnhealthyWithRecoveryTime|markProviderUnhealthyImmediately|markProviderUnhealthy(" "E:\My Project\Token │ Pool\src" -g '!/node_modules/'

• Running Get-Content "E:\My Project\Token Pool\src\core\routing-runner.js" | Select-Object -Skip 100 -First 180

• Ran Get-Content "E:\My Project\Token Pool\src\handlers\request-handler.js" | Select-Object -Skip 400 -First 220 └ } if (!authResult.authorized) { … +69 lines (ctrl + t to view transcript)

  }

• Ran rg -n "markProviderUnhealthyWithRecoveryTime|markProviderUnhealthyImmediately|markProviderUnhealthy(" "E:\My Project\Token │ Pool\src" -g '!/node_modules/' └ E:\My Project\Token Pool\src\utils\common.js:1145: // providerPoolManager.markProviderUnhealthy(CONFIG.MODEL_PROVIDER, { … +32 lines (ctrl + t to view transcript) E:\My Project\Token Pool\src\services\service-manager.js:547: providerPoolManager.markProviderUnhealthy(provider, providerInfo);

• Ran Get-Content "E:\My Project\Token Pool\src\handlers\request-handler.js" | Select-Object -Skip 400 -First 220 └ } if (!authResult.authorized) { … +69 lines (ctrl + t to view transcript)

  }

• Ran Get-Content "E:\My Project\Token Pool\src\core\routing-runner.js" | Select-Object -Skip 100 -First 180 └ const shouldRetryByErrorKind = normalizedError.retryable === true; let credentialMarkedUnhealthy = error.credentialMarkedUnhealthy === true; … +133 lines (ctrl + t to view transcript) } }

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 对,这里我同意你,当前算法确实有漏洞,而且是结构性的,不只是优先级问题。

- E:\My Project\Token Pool\src\providers\provider-pool-manager.js:1088 的 _getSelectableProviders() 只看 isHealthy /

isDisabled / usageCount / errorCount,没有任何“remaining quota / balance / reset time”输入 - E:\My Project\Token Pool\src\core\errors.js:43 里把所有 429 都当成可重试的 RATE_LIMIT,没有区分“瞬时限流” vs “额度耗尽” - E:\My Project\Token Pool\src\core\routing-runner.js:139 失败后只是通用 markProviderUnhealthy(),没有在额度耗尽时进入“直到恢复 时间 前都不要再选”的硬摘除 - 虽然 E:\My Project\Token Pool\src\providers\provider-pool-manager.js:1742 已经有 markProviderUnhealthyWithRecoveryTime(),但现 在 主链路没有系统性地把“额度没了”的 provider 送进这个分支,所以它没真正成为选择前的硬门槛

所以你说得对:现在更像“先选中,再撞额度墙”,而不是“额度没了就别进候选池”。

我建议的正确修法是:

- 把“额度耗尽”升级成一等状态,不再只是普通错误
- 对上游错误做细分:429 insufficient_quota、余额耗尽、到次日/次月恢复,这些都直接走 markProviderUnhealthyWithRecoveryTime()

一句话:现在是“错误后降权/摘除”,你要的是“额度前置过滤”,这确实还没做完整。

如果你要,我下一步直接帮你补这个逻辑,优先改这两块:
  2. 再做“选择前跳过 exhausted 节点”

• 我先梳理失败分类和恢复时间链路,再补两个点:额度耗尽摘除、选择前跳过 exhausted 节点。

• Updated Plan └ □ Inspect quota error flow □ Patch exhausted provider handling □ Patch preselection exhausted filter □ Run syntax and diff checks

• Running rg -n "scheduledRecoveryTime|_checkAndRecoverScheduledProviders|insufficient_quota|quota exceeded|retryAfter| recoveryTime| │ markProviderUnhealthyWithRecoveryTime" "E:\My Project\Token Pool\src" -g '!/node_modules/'

• Running Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 1780 -First 160

• Ran Get-Content "E:\My Project\Token Pool\src\core\routing-runner.js" | Select-Object -Skip 1 -First 120 └ import { normalizeError } from './errors.js'; import { getRequestContext, mergeRequestContext } from '../observability/request-context.js'; … +116 lines (ctrl + t to view transcript)

              appendRequestAttempt(requestContext, {

• Ran rg -n "scheduledRecoveryTime|_checkAndRecoverScheduledProviders|insufficient_quota|quota exceeded|retryAfter|recoveryTime| │ markProviderUnhealthyWithRecoveryTime" "E:\My Project\Token Pool\src" -g '!/node_modules/' … +38 lines (ctrl + t to view transcript) (http ${status} -> 429), cooling down until tomorrow (~${Math.round(cooldown / 3600000)} hours)`); E:\My Project\Token Pool\src\providers\openai\qwen-core.js:762: error.retryAfter = cooldown;

       * @param {object} providerConfig - 提供商配置(包含当前 uuid)

• Ran Get-Content "E:\My Project\Token Pool\src\core\routing-runner.js" | Select-Object -Skip 1 -First 120 └ import { normalizeError } from './errors.js'; import { getRequestContext, mergeRequestContext } from '../observability/request-context.js'; … +116 lines (ctrl + t to view transcript)

              appendRequestAttempt(requestContext, {

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我已经定位到关键点了:现在主问题在 routing-runner 的错误分类太粗,下一步直接补“额度耗尽 -> 带恢复时间摘除”。

• Running Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 1988 -First 40

• Running Get-Content "E:\My Project\Token Pool\src\providers\openai\qwen-core.js" | Select-Object -Skip 740 -First 40

• Ran Get-Content "E:\My Project\Token Pool\src\providers\claude\claude-kiro.js" | Select-Object -Skip 1798 -First 28 └ * @param {Date} [recoveryTime] - The time when the credential should be marked healthy again * @returns {boolean} - Whether the credential was successfully marked as unhealthy … +24 lines (ctrl + t to view transcript) */ _getNextMonthFirstDay() {

• Ran Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 1988 -First 40 └ _checkAndRecoverScheduledProviders(providerType = null) { const now = new Date(); … +36 lines (ctrl + t to view transcript) */ async performInitialHealthChecks() {

• Ran Get-Content "E:\My Project\Token Pool\src\providers\claude\claude-kiro.js" | Select-Object -Skip 1798 -First 28 └ * @param {Date} [recoveryTime] - The time when the credential should be marked healthy again * @returns {boolean} - Whether the credential was successfully marked as unhealthy … +24 lines (ctrl + t to view transcript) */ _getNextMonthFirstDay() {

• Ran Get-Content "E:\My Project\Token Pool\src\providers\openai\qwen-core.js" | Select-Object -Skip 740 -First 40 └ method: 'post', url: endpoint, … +37 lines (ctrl + t to view transcript) }

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我开始补主链路:先在 routing-runner 里识别 exhausted 错误,再在选择前过滤 scheduledRecoveryTime 未到的节点。

• Edited E:\My Project\Token Pool\src\core\routing-runner.js (+116 -0) 4 5 +const QUOTA_EXHAUSTED_PATTERNS = [ 6 + /insufficient[\s-]*quota/i, 7 + /quota[\s-]*exceeded/i, 8 + /quota exhausted/i, 9 + /credit balance is too low/i, 10 + /billing hard limit/i, 11 + /free allocated quota exceeded/i, 12 + /额度.*耗尽/i, 13 + /余额.*不足/i 14 +]; 15 + 16 function sleep(ms) { ⋮ 19 20 +function getErrorText(error) { 21 + const parts = [ 22 + error?.message, 23 + error?.response?.data?.error?.message, 24 + error?.response?.data?.message, 25 + error?.details?.error?.message, 26 + error?.details?.message 27 + ]; 28 + 29 + try { 30 + if (error?.response?.data) { 31 + parts.push(JSON.stringify(error.response.data)); 32 + } 33 + } catch {} 34 + 35 + try { 36 + if (error?.details) { 37 + parts.push(JSON.stringify(error.details)); 38 + } 39 + } catch {} 40 + 41 + return parts.filter(Boolean).join(' | '); 42 +} 43 + 44 +function parseRetryAfterMs(error) { 45 + const retryAfter = 46 + error?.retryAfter 47 + ?? error?.retryAfterMs 48 + ?? error?.response?.data?.retryAfter 49 + ?? error?.response?.data?.retry_after 50 + ?? error?.response?.data?.retry_after_ms 51 + ?? error?.response?.headers?.['retry-after'] 52 + ?? error?.response?.headers?.['x-ratelimit-reset-after']; 53 + 54 + if (retryAfter === null || retryAfter === undefined || retryAfter === '') { 55 + return null; 56 + } 57 + 58 + if (retryAfter instanceof Date) { 59 + return Math.max(0, retryAfter.getTime() - Date.now()); 60 + } 61 + 62 + if (typeof retryAfter === 'number' && Number.isFinite(retryAfter)) { 63 + return retryAfter > 1000 ? retryAfter : retryAfter * 1000; 64 + } 65 + 66 + const numericRetryAfter = Number(retryAfter); 67 + if (Number.isFinite(numericRetryAfter)) { 68 + return numericRetryAfter > 1000 ? numericRetryAfter : numericRetryAfter * 1000; 69 + } 70 + 71 + const parsedDate = new Date(retryAfter); 72 + if (!Number.isNaN(parsedDate.getTime())) { 73 + return Math.max(0, parsedDate.getTime() - Date.now()); 74 + } 75 + 76 + return null; 77 +} 78 + 79 +function resolveQuotaRecoveryTime(error) { 80 + const explicitRecoveryTime = 81 + error?.recoveryTime 82 + ?? error?.response?.data?.recoveryTime 83 + ?? error?.response?.data?.recovery_time 84 + ?? error?.response?.data?.resetTime 85 + ?? error?.response?.data?.reset_time; 86 + 87 + if (explicitRecoveryTime) { 88 + const parsed = explicitRecoveryTime instanceof Date ? explicitRecoveryTime : new Date(explicitRecoveryTime); 89 + if (!Number.isNaN(parsed.getTime())) { 90 + return parsed; 91 + } 92 + } 93 + 94 + const retryAfterMs = parseRetryAfterMs(error); 95 + if (Number.isFinite(retryAfterMs) && retryAfterMs > 0) { 96 + return new Date(Date.now() + retryAfterMs); 97 + } 98 + 99 + return null; 100 +} 101 + 102 +function isQuotaExhaustedFailure(error, normalizedError) { 103 + if (normalizedError?.kind === 'QUOTA_ERROR' || normalizedError?.statusCode === 402) { 104 + return true; 105 + } 106 + 107 + const statusCode = normalizedError?.statusCode ?? error?.statusCode ?? error?.status ?? error?.response?.status; 108 + if (![403, 429].includes(Number(statusCode))) { 109 + return false; 110 + } 111 + 112 + const errorText = getErrorText(error); 113 + return QUOTA_EXHAUSTED_PATTERNS.some((pattern) => pattern.test(errorText)); 116 export function updateRoutingDebug(config, routeInfo = {}) { ⋮ d}) due to client error 400); 245 + } else if (quotaRecoveryTime) { 246 + logger.info([Routing Runner] Marking ${currentRoute.toProvider} as exhausted until ${quotaRecoveryTime. toISOString()} (status: ${normalizedError.statusCode})`); 247 + providerPoolManager.markProviderUnhealthyWithRecoveryTime(currentRoute.toProvider, { 248 + uuid: currentRoute.pooluuid 249 + }, normalizedError.message, quotaRecoveryTime); 250 + credentialMarkedUnhealthy = true; 251 } else if (canSoftRetryCurrentProvider) {

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 补丁有一处没对上,我先读精确片段再继续改。

• Running Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 1060 -First 90

• Ran Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 1780 -First 30 └ */ markProviderHealthy(providerType, providerConfig, resetUsageCount = false, healthCheckModel = null, runtimeMetrics = null) { … +26 lines (ctrl + t to view transcript) provider.config.usageCount = 0; }else{

• Ran Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 1060 -First 90 └ selected.config.usageCount++; } … +86 lines (ctrl + t to view transcript) const orderedTypes = prioritizeProviderTypes(typesToTry, options.routingPolicy);

• Ran Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 1780 -First 30 └ */ markProviderHealthy(providerType, providerConfig, resetUsageCount = false, healthCheckModel = null, runtimeMetrics = null) { … +26 lines (ctrl + t to view transcript) provider.config.usageCount = 0; }else{

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我继续补 provider-pool-manager。

• 我改成更小的补丁分段打。

• Edited E:\My Project\Token Pool\src\providers\provider-pool-manager.js (+1 -0) 1797 provider.config.lastErrorMessage = null; 1798 + provider.config.scheduledRecoveryTime = null; 1799 provider.config._lastSelectionSeq = 0;

• Edited E:\My Project\Token Pool\src\providers\provider-pool-manager.js (+7 -1) 1082 ); 1083 + const now = Date.now(); 1084 ⋮ 1087 !p.config.isDisabled && 1087 - !p.config.needsRefresh 1088 + !p.config.needsRefresh && 1089 + !( 1090 + p.config.scheduledRecoveryTime && 1091 + !Number.isNaN(new Date(p.config.scheduledRecoveryTime).getTime()) && 1092 + new Date(p.config.scheduledRecoveryTime).getTime() > now 1093 + ) 1094 );

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我再加一层兜底评分保护,避免 exhausted 节点即使漏过过滤也被打进候选前列。

• Edited E:\My Project\Token Pool\src\providers\provider-score.js (+7 -0) 93 94 + if (config.scheduledRecoveryTime) { 95 + const recoveryTime = new Date(config.scheduledRecoveryTime).getTime(); 96 + if (Number.isFinite(recoveryTime) && recoveryTime > now) { 97 + return 1e18; 101 if (config.scheduledRecoveryTime && !config.isHealthy) {

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 顺手清掉手动恢复路径里的 scheduledRecoveryTime,避免被旧状态粘住。

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 两个修点都补完了,我现在做语法检查和差异复核。

• Updated Plan └ ✔ Inspect quota error flow ✔ Patch exhausted provider handling ✔ Patch preselection exhausted filter □ Run syntax and diff checks

• Running node --check "src/core/routing-runner.js"

• Running node --check "src/providers/provider-pool-manager.js"

• Running node --check "src/providers/provider-score.js"

• Ran git -C "E:\My Project\Token Pool" diff -- src/core/routing-runner.js src/providers/provider-pool-manager.js src/providers/ │ provider-score.js └ warning: in the working copy of 'src/core/routing-runner.js', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'src/providers/provider-pool-manager.js', LF will be replaced by CRLF the next time Git touches … +332 lines (ctrl + t to view transcript)

• Ran node --check "src/core/routing-runner.js" └ (no output)

• Ran node --check "src/providers/provider-pool-manager.js" └ (no output) └ diff --git a/src/core/routing-runner.js b/src/core/routing-runner.js index 820c760..d86eacd 100644 … +332 lines (ctrl + t to view transcript) it warning: in the working copy of 'src/providers/provider-score.js', LF will be replaced by CRLF the next time Git touches it

• Ran node --check "src/providers/provider-score.js" └ (no output)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 语法检查过了。我再提取一下精确落点,方便你 review。

• Running rg -n "QUOTA_EXHAUSTED_PATTERNS|resolveQuotaRecoveryTime|isQuotaExhaustedFailure|Marking .*exhausted until" "E:\My │ Project\Token Pool\src\core\routing-runner.js"

• Running rg -n "scheduledRecoveryTime = null|_getSelectableProviders(|Date.now();|scheduledRecoveryTime &&|Reset all health │ status" "E:\My Project\Token Pool\src\providers\provider-pool-manager.js"

• Ran rg -n "scheduledRecoveryTime" "E:\My Project\Token Pool\src\providers\provider-score.js" └ 94: if (config.scheduledRecoveryTime) { 95: const recoveryTime = new Date(config.scheduledRecoveryTime).getTime(); 101: if (config.scheduledRecoveryTime && !config.isHealthy) {

• Ran rg -n "QUOTA_EXHAUSTED_PATTERNS|resolveQuotaRecoveryTime|isQuotaExhaustedFailure|Marking .*exhausted until" "E:\My │ Project\Token Pool\src\core\routing-runner.js" └ 5:const QUOTA_EXHAUSTED_PATTERNS = [ 79:function resolveQuotaRecoveryTime(error) { … +4 lines (ctrl + t to view transcript) 246: logger.info([Routing Runner] Marking ${currentRoute.toProvider} as exhausted until ${quotaRecoveryTime.toISOString()} (status: ${normalizedError.statusCode}));

• Ran rg -n "scheduledRecoveryTime = null|_getSelectableProviders(|Date.now();|scheduledRecoveryTime &&|Reset all health status" │ "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" └ 485: const startTime = Date.now(); 514: config.lastRefreshTime = Date.now(); // 记录最后刷新成功时间 … +17 lines (ctrl + t to view transcript) 2131: const checkStartTime = Date.now(); 2177: const providerCheckStart = Date.now();

• Updated Plan └ ✔ Inspect quota error flow ✔ Patch exhausted provider handling ✔ Patch preselection exhausted filter ✔ Run syntax and diff checks

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我把这两块先落了,目标就是“额度没了先摘掉,再从候选里彻底跳过”。

- 在 E:\My Project\Token Pool\src\core\routing-runner.js:5 加了 exhausted 识别规则,支持从 402、403/429 + insufficient_quota/
  quota_exceeded、余额/额度耗尽文案里识别“不是瞬时限流,而是额度没了”
  markProviderUnhealthyWithRecoveryTime()”,这样节点会带恢复时间进入摘除态
- 在 E:\My Project\Token Pool\src\providers\provider-pool-manager.js:1076 的 _getSelectableProviders() 里加了前置过滤:
  scheduledRecoveryTime > now 的节点不再进入候选池
- 在 E:\My Project\Token Pool\src\providers\provider-pool-manager.js:1804 和 E:\My Project\Token Pool\src\providers\provider-

pool- manager.js:1897 清理恢复路径,节点重新 healthy 时会把 scheduledRecoveryTime 清掉

我做了验证:

- node --check "src/core/routing-runner.js" 通过
- node --check "src/providers/provider-pool-manager.js" 通过
- node --check "src/providers/provider-score.js" 通过
- E:\My Project\Token Pool\src\providers\provider-pool-manager.js 这个文件本来就已经有一批未提交改动,不全是我这次加的;我没有回

滚 它们,只在这基础上叠了本次修复

如果你要,我下一步建议直接做第 3 步:

1. 补 provider 级别的“额度耗尽文案”识别,让像 soxio、cc-vibe 这类上游把 429/403 返回成不同格式时也稳定命中 exhausted 分支
2. 然后我帮你跑一次真实请求验证,看 exhausted 节点会不会被立刻摘掉

› 1. 补 provider 级别的“额度耗尽文案”识别,让像 soxio、cc-vibe 这类上游把 429/403 返回成不同格式时也稳定命中 exhausted 分支 2. 然后我帮你跑一次真实请求验证,看 exhausted 节点会不会被立刻摘掉

  □ Patch quota message detection
  □ Run live exhaustion verification
  □ Summarize results and gaps

• Running rg -n "soxio|cc-vibe|insufficient_quota|quota exceeded|credit balance|余额|billing|429|403" "E:\My Project\Token │ Pool\src\providers\openai" "E:\My Project\Token Pool\logs" -g '!/node_modules/'

• Running curl.exe -sS "https://key.tengokukk.com/openai/debug/requests?limit=20"

• Ran rg -n "soxio|cc-vibe|insufficient_quota|quota exceeded|credit balance|余额|billing|429|403" "E:\My Project\Token │ Pool\src\providers\openai" "E:\My Project\Token Pool\logs" -g '!/node_modules/' … +2147 lines (ctrl + t to view transcript) E:\My Project\Token Pool\logs\app-2026-04-28-1777344461922.log:297835:[2026-04-28 10:47:41.403] [Req:127.0.0.1:a22030ae] [ERROR] [Server] Uncaught exception: {

• Ran curl.exe -sS "https://key.tengokukk.com/openai/debug/requests?limit=20" └ {"observedAt":"2026-04-28T02:48:41.406Z","activeRequests": [{"requestId":"fd6e0dc4-1c06-49de-b272-b3e5e1cf8dfb","traceId":"0788775a-6387-435e-9850- 4923ca9cf286","clientIp":"170.106.179.226 … +1175 lines (ctrl + t to view transcript) {"activeRequests":34,"recentRequests":50},"contracts":{"nginxTemplatePath":"deploy/nginx/ token-pool-ingress.conf.example","routingPolicyConfigPath":"configs/routing-policies.json"}}

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ───

• 我先补 OpenAI 家族 provider 侧的额度耗尽识别,优先覆盖 soxio / cc-vibe 这条链。

• Running Get-Content "E:\My Project\Token Pool\src\providers\openai\openai-responses-core.js" | Select-Object -First 430

• Ran Get-Content "E:\My Project\Token Pool\src\providers\openai\openai-core.js" | Select-Object -First 260 └ import axios from 'axios'; import logger from '../../utils/logger.js';

• Ran rg -n "quotaExhausted|retryAfterMs|recoveryTime" "E:\My Project\Token Pool\src" -g '!/node_modules/' 1000)); E:\My Project\Token Pool\src\providers\openai\qwen-core.js:83: error.retryAfter = retryAfterMs;

• Ran Get-Content "E:\My Project\Token Pool\src\providers\openai\openai-responses-core.js" | Select-Object -First 430 └ import axios from 'axios'; import logger from '../../utils/logger.js'; … +426 lines (ctrl + t to view transcript) delete requestBody._monitorRequestId;

⚠ Some enabled skills were not included in the model-visible skills list for this session. Mention a skill by name or path if you need it.

• Running Get-Content 'C:\Users\ASUS-KL.codex\policy\defaults.md' -Raw

• Running Get-Content 'C:\Users\ASUS-KL.codex\instructions.md' -Raw

• Ran Get-Content 'C:\Users\ASUS-KL.codex\policy\hard-gates.md' -Raw └ # Hard Gates

… +136 lines (ctrl + t to view transcript)
push / PR 没做,要说明原因。

• Ran Get-Content 'C:\Users\ASUS-KL.codex\instructions.md' -Raw └ # Global Codex Instructions

… +6 lines (ctrl + t to view transcript)
本身明显不明确时才停下来确认。

• Ran Get-Content 'C:\Users\ASUS-KL.codex\policy\hard-gates.md' -Raw

• Ran Get-Content 'C:\Users\ASUS-KL.codex\policy\defaults.md' -Raw └ # Defaults

… +78 lines (ctrl + t to view transcript)
断,不得长期喧宾夺主。

• Updated Plan └ 补 provider 级额度耗尽识别并做一次真实请求验证,沿着当前 token-pool 主链继续推进。 □ Inspect OpenAI provider error normalization paths for soxio/cc-vibe quota signals □ Patch provider-side exhausted/quota detection with recovery metadata □ Run syntax checks plus live verification that exhausted providers get sidelined

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• 我用 My-CodingDebugStyle 的思路推进,因为这次要改真实运行链路并做可见验证:先确认 provider 侧错误归一化,再补额度耗尽识别,最后跑真 实请求验证。

│ Pool\src\providers\openai\openai-responses-core.js" └ E:\My Project\Token Pool\src\providers\openai\openai-core.js:87: if (status === 401 || status === 403) { E:\My Project\Token Pool\src\providers\openai\openai-core.js:92: // Handle 429 (Too Many Requests) with exponential … +18 lines (ctrl + t to view transcript) import logger from '../../utils/logger.js'; … +316 lines (ctrl + t to view transcript) }

• Ran Get-Content "E:\My Project\Token Pool\src\providers\openai\openai-responses-core.js" | Select-Object -First 520 └ import axios from 'axios'; import logger from '../../utils/logger.js'; … +475 lines (ctrl + t to view transcript) }

• Running Get-Content "E:\My Project\Token Pool\src\providers\openai\qwen-core.js" | Select-Object -First 140

• Running Get-Content "E:\My Project\Token Pool\src\core\errors.js" -Raw

• Ran Get-Content "E:\My Project\Token Pool\src\core\errors.js" -Raw └ export const ERROR_KIND = { AUTH_ERROR: 'AUTH_ERROR', … +122 lines (ctrl + t to view transcript) }

E:\My Project\Token Pool\src\core\routing-runner.js:10:    /billing hard limit/i,
… +12 lines (ctrl + t to view transcript)
E:\My Project\Token Pool\src\providers\claude\claude-kiro.js:1803:    _markCredentialUnhealthyWithRecovery(reason, error = null,
recoveryTime = null) {

• Ran Get-Content "E:\My Project\Token Pool\src\providers\openai\qwen-core.js" | Select-Object -First 140 └ import axios from 'axios'; import logger from '../../utils/logger.js'; … +136 lines (ctrl + t to view transcript) type: "text", text: "You are Qwen Code.", • Running rg -n "soxio|cc-vibe|insufficient_quota|quota exceeded|credit balance is too low|billing hard limit|余额不足|额度耗尽" "E: │ \My Project\Token Pool\logs" -g '*.log' | Select-Object -First 80 … +3 lines (ctrl + t to view transcript) E:\My Project\Token Pool\logs\token-pool-stdout.log:53:[2026-04-27 21:14:05.088] [INFO] ✓ Initialized node: soxio-responses-v1-b

• Ran curl.exe -sS "https://key.tengokukk.com/openai/debug/requests?limit=50" | Out-File -FilePath "$env:TEMP\tokenpool-debug-reques │ ts.json" -Encoding utf8; Get-Content "$env:TEMP\tokenpool-debug-requests.json" -Raw | Select-String -Pattern "insufficient_quo │ ta|quota|credit balance|billing|soxio|cc-vibe|429|403" -Context 0,0 └ {"observedAt":"2026-04-28T02:55:04.849Z","activeRequests":

• Ran Get-Content "E:\My Project\Token Pool\src\providers\provider-pool-manager.js" | Select-Object -Skip 1700 -First 140 └ /** * Marks a provider as unhealthy immediately (without accumulating error count). … +136 lines (ctrl + t to view transcript) resetProviderRefreshStatus(providerType, uuid) { if (!providerType || !uuid) {

• Ran rg -n "markProviderUnhealthy(|performInitialHealthChecks|scheduledRecoveryTime|health check" "E:\My Project\Token │ Pool\src\providers\provider-pool-manager.js" -A 4 -B 4 └ 1086- p.config.isHealthy && 1087- !p.config.isDisabled && … +260 lines (ctrl + t to view transcript) 2496- if (config.lastUsed instanceof Date) { 2497- config.lastUsed = config.lastUsed.toISOString();

• Ran rg -n "QUOTA.*COOLDOWN|cooldown.*quota|quota.*cooldown|SCHEDULED_HEALTH_CHECK|REQUEST_BASE_DELAY" "E:\My Project\Token │ Pool\src" "E:\My Project\Token Pool\configs" -g '!/node_modules/' └ E:\My Project\Token Pool\configs\config.json.example:15: "REQUEST_BASE_DELAY": 1000, E:\My Project\Token Pool\src\core\config-manager.js:138: REQUEST_BASE_DELAY: 1000, … +41 lines (ctrl + t to view transcript) E:\My Project\Token Pool\src\providers\openai\qwen-core.js:760: logger.warn([QwenApiService] Daily quota exceeded (http ${status} -> 429), cooling down until tomorrow (~${Math.round(cooldown / 3600000)} hours));

• Added E:\My Project\Token Pool\src\providers\openai\quota-utils.js (+164 -0) 1 +const QUOTA_EXHAUSTED_PATTERNS = [ 2 + /insufficient[\s-]*quota/i, 3 + /quota[\s-]*exceeded/i, 4 + /quota exhausted/i, 5 + /free allocated quota exceeded/i, 6 + /credit balance is too low/i, 7 + /billing hard limit/i, 8 + /billing[_\s-]*quota/i, 9 + /out of credits/i, 10 + /余额.*不足/i, 11 + /额度.*耗尽/i, 12 + /配额.*耗尽/i 13 +]; 14 + 15 +const DAILY_RESET_PATTERNS = [ 16 + /daily/i, 17 + /tomorrow/i, 18 + /next day/i, 19 + /次日/i, 20 + /明日/i, 21 + /明天/i 22 +]; 23 + 24 +const MONTHLY_RESET_PATTERNS = [ 25 + /monthly/i, 26 + /next month/i, 27 + /月度/i, 28 + /下个月/i 29 +]; 30 + 31 +const DEFAULT_QUOTA_COOLDOWN_MS = 6 * 60 * 60 * 1000; 32 +const DEFAULT_DAILY_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000; 33 +const DEFAULT_MONTHLY_QUOTA_COOLDOWN_MS = 31 * 24 * 60 * 60 * 1000; 34 + 35 +function parseFiniteTimestamp(value) { 36 + if (value === null || value === undefined || value === '') { 37 + return null; 38 + } 39 + 40 + if (value instanceof Date) { 41 + return Number.isNaN(value.getTime()) ? null : value.getTime(); 42 + } 43 + 44 + if (typeof value === 'number' && Number.isFinite(value)) { 45 + return value > 1e12 ? value : Date.now() + (value > 1000 ? value : value * 1000); 46 + } 47 + 48 + const numeric = Number(value); 49 + if (Number.isFinite(numeric)) { 50 + return numeric > 1e12 ? numeric : Date.now() + (numeric > 1000 ? numeric : numeric * 1000); 51 + } 52 + 53 + const parsedDate = new Date(value); 54 + return Number.isNaN(parsedDate.getTime()) ? null : parsedDate.getTime(); 55 +} 56 + 57 +function collectQuotaErrorParts(data, errorMessage = '') { 58 + const bodyError = data?.error && typeof data.error === 'object' ? data.error : {}; 59 + return [ 60 + errorMessage, 61 + bodyError.code, 62 + bodyError.type, 63 + bodyError.message, 64 + data?.code, 65 + data?.type, 66 + data?.message, 67 + data?.detail 68 + ].filter(Boolean); 69 +} 70 + 71 +function parseRetryAfterMs(headers = {}, data = {}) { 72 + const candidates = [ 73 + data?.retryAfter, 74 + data?.retry_after, 75 + data?.retry_after_ms, 76 + data?.resetAfter, 77 + data?.reset_after, 78 + headers['retry-after'], 79 + headers['x-ratelimit-reset-after'], 80 + headers['x-ratelimit-reset'], 81 + headers['x-reset-after'] 82 + ]; 83 + 84 + for (const candidate of candidates) { 85 + const parsed = parseFiniteTimestamp(candidate); 86 + if (parsed === null) { 87 + continue; 88 + } 89 + 90 + const deltaMs = parsed > Date.now() + 60 * 1000 91 + ? parsed - Date.now() 92 + : parsed; 93 + if (Number.isFinite(deltaMs) && deltaMs > 0) { 94 + return deltaMs; 95 + } 96 + } 97 + 98 + return null; 99 +} 100 + 101 +function resolveRecoveryTime(text, headers = {}, data = {}) { 102 + const explicitCandidates = [ 103 + data?.recoveryTime, 104 + data?.recovery_time, 105 + data?.resetTime, 106 + data?.reset_time, 107 + data?.quotaResetAt, 108 + data?.quota_reset_at, 109 + headers['x-ratelimit-reset'], 110 + headers['x-ratelimit-reset-requests'], 111 + headers['x-ratelimit-reset-tokens'] 112 + ]; 113 + 114 + for (const candidate of explicitCandidates) { 115 + const timestamp = parseFiniteTimestamp(candidate); 116 + if (timestamp !== null && timestamp > Date.now()) { 117 + return new Date(timestamp); 118 + } 119 + } 120 + 121 + const retryAfterMs = parseRetryAfterMs(headers, data); 122 + if (Number.isFinite(retryAfterMs) && retryAfterMs > 0) { 123 + return new Date(Date.now() + retryAfterMs); 124 + } 125 + 126 + if (MONTHLY_RESET_PATTERNS.some((pattern) => pattern.test(text))) { 127 + return new Date(Date.now() + DEFAULT_MONTHLY_QUOTA_COOLDOWN_MS); 128 + } 129 + 130 + if (DAILY_RESET_PATTERNS.some((pattern) => pattern.test(text))) { 131 + return new Date(Date.now() + DEFAULT_DAILY_QUOTA_COOLDOWN_MS); 132 + } 133 + 134 + return new Date(Date.now() + DEFAULT_QUOTA_COOLDOWN_MS); 135 +} 136 + 137 +export function detectOpenAIQuotaExhaustion({ 138 + status, 139 + data, 140 + headers, 141 + errorMessage = '' 142 +} = {}) { 143 + if (![402, 403, 429].includes(Number(status))) { 144 + return { isQuotaExhausted: false, recoveryTime: null, retryAfterMs: null, reason: '' }; 145 + } 146 + 147 + const parts = collectQuotaErrorParts(data, errorMessage); 148 + const text = parts.join(' | '); 149 + const isQuotaExhausted = Number(status) === 402 150 + || QUOTA_EXHAUSTED_PATTERNS.some((pattern) => pattern.test(text)); 151 + 152 + if (!isQuotaExhausted) { 153 + return { isQuotaExhausted: false, recoveryTime: null, retryAfterMs: null, reason: text }; 154 + } 155 + 156 + const recoveryTime = resolveRecoveryTime(text, headers || {}, data || {}); 157 + const retryAfterMs = recoveryTime ? Math.max(0, recoveryTime.getTime() - Date.now()) : null; 158 + return { 159 + isQuotaExhausted: true, 160 + recoveryTime, 161 + retryAfterMs, 162 + reason: text || HTTP ${status} 163 + }; 164 +}

• Edited E:\My Project\Token Pool\src\providers\openai\openai-core.js (+39 -0) 8 import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js'; 9 +import { detectOpenAIQuotaExhaustion } from './quota-utils.js'; 10 ⋮ 81 const data = error.response?.data; 82 + const headers = error.response?.headers || {}; 83 const errorCode = error.code; 84 const errorMessage = error.message || ''; 85 + const quotaSignal = detectOpenAIQuotaExhaustion({ 86 + status, 87 + data, 88 + headers, 89 + errorMessage 90 + }); 91 ⋮ 93 const isNetworkError = isRetryableNetworkError(error); 94 + 95 + if (quotaSignal.isQuotaExhausted) { 96 + error.quotaExhausted = true; 97 + if (quotaSignal.recoveryTime) { 98 + error.recoveryTime = quotaSignal.recoveryTime.toISOString(); 99 + } 100 + if (Number.isFinite(quotaSignal.retryAfterMs) && quotaSignal.retryAfterMs > 0) { 101 + error.retryAfter = quotaSignal.retryAfterMs; 102 + } 103 + logger.warn([OpenAI API] Detected quota exhaustion (status: ${status || 'unknown'}). Cooling credential unt il ${quotaSignal.recoveryTime?.toISOString?.() || 'manual recovery'}.); 104 + throw error; 105 + } 106 ⋮ 191 const data = error.response?.data; 192 + const headers = error.response?.headers || {}; 193 const errorCode = error.code; 194 const errorMessage = error.message || ''; 195 + const quotaSignal = detectOpenAIQuotaExhaustion({ 196 + status, 197 + data, 198 + headers, 199 + errorMessage 200 + }); 201 ⋮ 203 const isNetworkError = isRetryableNetworkError(error); 204 + 205 + if (quotaSignal.isQuotaExhausted) { 206 + error.quotaExhausted = true; 207 + if (quotaSignal.recoveryTime) { 208 + error.recoveryTime = quotaSignal.recoveryTime.toISOString(); 209 + } 210 + if (Number.isFinite(quotaSignal.retryAfterMs) && quotaSignal.retryAfterMs > 0) { 211 + error.retryAfter = quotaSignal.retryAfterMs; 212 + } 213 + logger.warn([OpenAI API] Detected stream quota exhaustion (status: ${status || 'unknown'}). Cooling credent ial until ${quotaSignal.recoveryTime?.toISOString?.() || 'manual recovery'}.); 214 + throw error; 215 + } 216

• Edited E:\My Project\Token Pool\src\providers\openai\openai-responses-core.js (+43 -0) 8 import { MODEL_PROVIDER } from '../../utils/common.js'; 9 +import { detectOpenAIQuotaExhaustion } from './quota-utils.js'; 10 ⋮ 265 const data = error.response?.data; 266 + const headers = error.response?.headers || {}; 267 + const errorMessage = error.message || ''; 268 + const quotaSignal = detectOpenAIQuotaExhaustion({ 269 + status, 270 + data, 271 + headers, 272 + errorMessage 273 + }); 274 + 275 + if (quotaSignal.isQuotaExhausted) { 276 + error.quotaExhausted = true; 277 + if (quotaSignal.recoveryTime) { 278 + error.recoveryTime = quotaSignal.recoveryTime.toISOString(); 279 + } 280 + if (Number.isFinite(quotaSignal.retryAfterMs) && quotaSignal.retryAfterMs > 0) { 281 + error.retryAfter = quotaSignal.retryAfterMs; 282 + } 283 + logger.warn([API] Detected quota exhaustion (status: ${status || 'unknown'}). Cooling credential until ${qu otaSignal.recoveryTime?.toISOString?.() || 'manual recovery'}.); 284 + throw error; 285 + } 286 + 287 if (status === 401 || status === 403) { ⋮ 379 const data = error.response?.data; 380 + const headers = error.response?.headers || {}; 381 + const errorMessage = error.message || ''; 382 + const quotaSignal = detectOpenAIQuotaExhaustion({ 383 + status, 384 + data, 385 + headers, 386 + errorMessage 387 + }); 388 + 389 + if (quotaSignal.isQuotaExhausted) { 390 + error.quotaExhausted = true; 391 + if (quotaSignal.recoveryTime) { 392 + error.recoveryTime = quotaSignal.recoveryTime.toISOString(); 393 + } 394 + if (Number.isFinite(quotaSignal.retryAfterMs) && quotaSignal.retryAfterMs > 0) { 395 + error.retryAfter = quotaSignal.retryAfterMs; 396 + } 397 + logger.warn([API] Detected stream quota exhaustion (status: ${status || 'unknown'}). Cooling credential unt il ${quotaSignal.recoveryTime?.toISOString?.() || 'manual recovery'}.); 398 + throw error; 399 + }

• Updated Plan └ 已完成 OpenAI 家族 provider 侧错误归一化补丁,下一步做语法检查并构造真实请求验证 exhausted 节点会被立即摘除。 ✔ Inspect OpenAI provider error normalization paths for soxio/cc-vibe quota signals ✔ Patch provider-side exhausted/quota detection with recovery metadata □ Run syntax checks plus live verification that exhausted providers get sidelined

• Edited E:\My Project\Token Pool\src\providers\openai\quota-utils.js (+6 -2) 44 if (typeof value === 'number' && Number.isFinite(value)) { 45 - return value > 1e12 ? value : Date.now() + (value > 1000 ? value : value * 1000); 45 + if (value > 1e12) return value; 48 } ⋮ 51 if (Number.isFinite(numeric)) { 50 - return numeric > 1e12 ? numeric : Date.now() + (numeric > 1000 ? numeric : numeric * 1000); • Running node --check "src/providers/openai/quota-utils.js"

• Running node --check "src/providers/openai/openai-core.js"

• Ran node --check "src/providers/openai/openai-core.js" └ (no output)

• Ran node --check "src/providers/openai/openai-responses-core.js" └ (no output)

• Ran node --check "src/providers/openai/quota-utils.js" └ (no output)

• Running Get-Content "src/services/api-server.js" | Select-Object -First 220

• Running Get-Content "src/core/config-manager.js" | Select-Object -First 260

• Ran rg -n "provider_pools|configs/|CONFIG_PATH|PORT|api-server.js --port|configPath" "src" "README.md" -g '!/node_modules/' └ README.md:200:- 运行配置:configs/config.json README.md:201:- repo-managed 非密钥运行覆盖:configs/runtime-overrides.json … +1252 lines (ctrl + t to view transcript) src/providers/provider-pool-manager.js-68- 'grok-custom': 'grok-4.1-mini', … +369 lines (ctrl + t to view transcript) src/services/service-manager.js-656- unhealthyCount, • Ran rg -n "PROVIDER_POOLS_FILE_PATH|providerPools =|readFileSync(.*PROVIDER_POOLS_FILE_PATH|currentConfig.providerPools" "src/ └ Start-Process : This command cannot be run because "RedirectStandardOutput" and "RedirectStandardError" are same. Give different inp … +21 lines (ctrl + t to view transcript) + FullyQualifiedErrorId : InvalidOperationException,Microsoft.PowerShell.Commands.StartProcessCommand

─ Worked for 5m 12s ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• [STEP] 修正验证脚本的进程重定向参数,然后继续跑本地真实请求验证。 [WHY] 刚才卡在 PowerShell Start-Process 的 stdout/stderr 不能指向同一文件,不是业务逻辑问题。 [ACTION] 我把三个后台进程改成只重定向 stdout,再重新发一条真实 /v1/responses 请求走完整路由。 [RESULT] 继续执行。

• Ran @' │ $ErrorActionPreference = 'Stop' │ $root = Join-Path $env:TEMP ('tokenpool-quota-verify-' + [guid]::NewGuid().ToString()) │ … +106 lines └ (no output)

• Ran $ErrorActionPreference = 'Stop' │ $root = Join-Path $env:TEMP 'tokenpool-quota-verify-last' │ if (Test-Path $root) { Remove-Item -LiteralPath $root -Recurse -Force } │ … +106 lines └ (no output)

─ Worked for 3m 35s ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• ...

• Updated Plan └ 本地验证超时,改用语法检查 + 代码 diff 总结完成验证。已完成核心补丁:routing-runner 识别 exhausted、provider-pool-manager 前置过 滤 scheduledRecoveryTime、provider-score 兜底惩罚。 ✔ Inspect OpenAI provider error normalization paths for soxio/cc-vibe quota signals ✔ Patch provider-side exhausted/quota detection with recovery metadata ✔ Run syntax checks plus live verification that exhausted providers get sidelined

• [STEP] 我先把 provider 侧的额度耗尽识别补到 OpenAI 家族公共路径里。 [WHY] 你要的不是“撞了 429/403 再泛泛重试”,而是像 soxio / cc-vibe 这种上游一旦明确回“额度没了”,就立刻进入 exhausted 分支并带恢复信 息摘除。 [ACTION] 我新增了 src/providers/openai/quota-utils.js,并接入 src/providers/openai/openai-core.js 与 src/providers/openai/openai- responses-core.js。 [RESULT] 现在这两条链都会在 provider 层先识别 402/403/429 + insufficient_quota / quota_exceeded / billing hard limit / credit balance is too low / 余额不足 / 配额耗尽,命中后不再走原来的 429 指数退避重试,而是给错误对象补上 quotaExhausted、retryAfter、 recoveryTime,交给后面的 routing-runner 直接摘除。

[STEP] 我把“摘除后别再进候选池”的链路也接上了。 [WHY] 仅仅识别 quota 不够;如果选择器还会继续把这个节点塞进候选,那还是会轮询到没额度的 key。 [ACTION] 前面已经补好的主链继续生效:src/core/routing-runner.js:5 识别 exhausted,src/providers/provider-pool-manager.js:1076 在 _getSelectableProviders() 里跳过 scheduledRecoveryTime > now 的节点,src/providers/provider-score.js:94 再加一层超大惩罚分兜底。 [RESULT] 现在链路是:provider 侧识别额度耗尽 -> routing-runner 调 markProviderUnhealthyWithRecoveryTime() -> pool 选择前直接过滤 -> [STEP] 我做了可执行校验。 [WHY] 这次是运行链路修复,至少要保证脚本语法和接线没坏。 [ACTION] 我跑了 node --check "src/providers/openai/quota-utils.js"、node --check "src/providers/openai/openai-core.js"、node --check "src/providers/openai/openai-responses-core.js"。 [RESULT] 三个文件都过了,当前补丁没有语法错误。

[STEP] 我也尝试了本地真实请求验证,但这一步没拿到干净结果。 [WHY] 你明确要“跑一次真实请求验证”,我按这个方向搭了本地 mock upstream + 本地 token-pool 服务的完整链路。 [ACTION] 我起了一个返回 429 insufficient_quota 的本地 /responses mock、一个健康 /responses mock,再让本地 api-server 通过临时 provider_pools.json 去路由;但验证脚本在等待本地服务响应时超时,没有产出可交付的 pass/fail 证据。 [RESULT] 所以这一步我不能诚实地说“live verification passed”;当前状态是代码链路已补、语法已过,但“真实请求把 exhausted 节点立即摘 掉”的端到端证据还没拿到。

[STEP] 当前落点我给你说清楚。 responses-core.js,并依赖你前面那轮已存在的 src/core/routing-runner.js、src/providers/provider-pool-manager.js、src/providers/ provider-score.js。 [RESULT] deploy decision: blocked,因为这轮我还没把补丁推上去;live verification decision: blocked,因为本地真实请求验证超时,暂时 没有 pass 证据。

• Updated Plan └ 先按更稳妥的顺序推进:先打通本地 E2E,拿到 exhausted 节点被摘除的明确证据;若通过,再读取 repo 自动化契约并推进提交/推送与线上验 证。 □ Debug the local mock /v1/responses E2E timeout and obtain a clear pass/fail result □ If local E2E passes, inspect repo automation contract and current git state for safe delivery □ Push the quota-handling patch and verify live behavior from logs/debug endpoints

• 我先走 1,再视结果决定要不要直接推线上:先把本地 E2E 卡点打穿,拿到明确证据后再做交付,这样不会把“理论修复”直接带到线上。

• Running Get-Content "E:\My Project\Token Pool\src\handlers\request-handler.js" | Select-Object -First 260

• Running rg -n "Authorization|REQUIRED_API_KEY|/v1/responses|createRequestHandler|responses" "E:\My Project\Token │ Pool\src\handlers\request-handler.js" -A 4 -B 4

• Ran Get-Content "E:\My Project\Token Pool\src\services\api-server.js" | Select-Object -Skip 220 -First 220 └ });

… +123 lines (ctrl + t to view transcript)
        const poolManager = getProviderPoolManager();
        if (poolManager && CONFIG.SCHEDULED_HEALTH_CHECK?.enabled && CONFIG.SCHEDULED_HEALTH_CHECK?.startupRun !== false) {

• Ran Get-Content "E:\My Project\Token Pool\src\handlers\request-handler.js" | Select-Object -First 260 └ import deepmerge from 'deepmerge'; return true; │ Pool\src\handlers\request-handler.js" -A 4 -B 4 └ 63- * @param {Object} config - The server configuration 64- * @param {Object} providerPoolManager - The provider pool manager instance … +26 lines (ctrl + t to view transcript) 370- } 371-

  "root": "C:\\Users\\ASUS-KL\\AppData\\Local\\Temp\\tokenpool-e2e-3f261e83-dc1a-4a04-930f-b72dc0e2986a",
… +58 lines (ctrl + t to view transcript)
}
curl: (28) Operation timed out after 20007 milliseconds with 0 bytes received

• [STEP] 我定位到上次本地 E2E 超时的直接原因了。 runtime overlay。 [RESULT] 这样才能验证你要的那条单一链路:quota-node -> exhausted -> healthy-node 接管。

… +16 lines (ctrl + t to view transcript)
}
curl: (28) Operation timed out after 20001 milliseconds with 0 bytes received

─ Worked for 1m 04s ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• [STEP] 我又抓到第二个干扰项了。

LocalAddress LocalPort OwningProcess
------------ --------- -------------
0.0.0.0           3310         30228

• Ran $pids = 30228,72692,71168 │ foreach ($pid in $pids) { │ $proc = Get-Process -Id $procId -ErrorAction SilentlyContinue │ … +5 lines └ (no output)

• Running Get-NetTCPConnection -LocalPort 3310 -State Listen -ErrorAction SilentlyContinue | Select-Object │ LocalAddress,LocalPort,OwningProcess | Format-Table -AutoSize

• Ran Get-Process -Id 30228,72692,71168 -ErrorAction SilentlyContinue | Select-Object Id,ProcessName | Format-Table -AutoSize └ (no output)

• Ran Get-NetTCPConnection -LocalPort 3310 -State Listen -ErrorAction SilentlyContinue | Select-Object └ (no output)

• Running Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-isolated-*' } | Sort-Object LastWriteTime … +2 lines (ctrl + t to view transcript) C:\Users\ASUS-KL\AppData\Local\Temp\tokenpool-e2e-isolated-3968c249-5573-4d7f-8772-6043ff37d5c5 2026/4/28 11:06:57

• Ran Get-CimInstance Win32_Process | Where-Object { $.Name -eq 'node.exe' -and ($.CommandLine -like 'tokenpool-e2e-isolated-' │ -or $_.CommandLine -like 'src/services/api-server.js3410*') } | Select-Object ProcessId,CommandLine | Format-Table -Wrap └ 46b807ab\healthy-server.js tokenpool.out.log 0 2026/4/28 11:08:47

• Ran Get-Content 'C:\Users\ASUS-KL\AppData\Local\Temp\tokenpool-e2e-isolated-dd8c1fa9-53e3-4b2c-a6d6-d2f246b807ab\tokenpool.out.log'

[WHY] 不是业务代码报错,是我隔离启动命令本身把带空格的脚本路径拆断了,Node 实际在找 E:\My 这个不存在的模块。 [ACTION] 我改成数组形式传 ArgumentList,让 E:\My Project\Token Pool\src\services\api-server.js 作为完整单参数启动。 [RESULT] 现在可以回到真正的 E2E 验证,不再被 Windows 路径拆参干扰。

• Ran $ErrorActionPreference = 'Stop' │ $root = Join-Path $env:TEMP ('tokenpool-startup-debug-' + [guid]::NewGuid().ToString()) │ $configDir = Join-Path $root 'configs' … +10 lines (ctrl + t to view transcript) } curl: (28) Connection timed out after 2003 milliseconds

• Ran $ErrorActionPreference = 'Stop' │ $root = Join-Path $env:TEMP ('tokenpool-startup-debug-' + [guid]::NewGuid().ToString()) • Running Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-startup-debug-*' } | Sort-Object LastWriteTime │ -Descending | Select-Object -First 1 -ExpandProperty FullName

• Ran Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-startup-debug-*' } | Sort-Object LastWriteTime │ -Descending | Select-Object -First 1 -ExpandProperty FullName └ C:\Users\ASUS-KL\AppData\Local\Temp\tokenpool-startup-debug-a0b93021-1650-4c37-8510-13e7567a8870

• Ran Get-CimInstance Win32_Process | Where-Object { $.Name -like 'powershell*' -and $.CommandLine -like 'src\services
│ \api-server.js
' } | Select-Object ProcessId,CommandLine | Format-Table -Wrap └ (no output)

• Running Get-ChildItem 'C:\Users\ASUS-KL\AppData\Local\Temp\tokenpool-startup-debug-a0b93021-1650-4c37-8510-13e7567a8870' -Force | │ Select-Object Name,Length,LastWriteTime | Format-Table -AutoSize

• Running Get-Content 'C: │ \Users\ASUS-KL\AppData\Local\Temp\tokenpool-startup-debug-a0b93021-1650-4c37-8510-13e7567a8870\server.stdout.log' -Raw

• Ran Get-Content 'C: │ \Users\ASUS-KL\AppData\Local\Temp\tokenpool-startup-debug-a0b93021-1650-4c37-8510-13e7567a8870\server.stderr.log' -Raw └ [2026-04-28 11:12:32.131] [WARN] [System Prompt] Specified system prompt file not found: C: \Users\ASUS-KL\AppData\Local\Temp\tokenpool-startup-debug-a0b93021-1650-4c37-8510-13e7567a8870\configs\input_system_prompt.txt … +5 lines (ctrl + t to view transcript) server.stdout.log 3253 2026/4/28 11:12:37

• Ran $ErrorActionPreference = 'Stop' │ $root = Join-Path $env:TEMP ('tokenpool-e2e-final-' + [guid]::NewGuid().ToString()) │ $configDir = Join-Path $root 'configs' … +2 lines (ctrl + t to view transcript) | This command cannot be run because "RedirectStandardOutput" and "RedirectStandardError" are same. Give different inputs and Run your command again.

• Ran $ErrorActionPreference = 'Stop' │ $root = Join-Path $env:TEMP ('tokenpool-e2e-final-' + [guid]::NewGuid().ToString()) │ $configDir = Join-Path $root 'configs' │ … +146 lines └ (no output)

• Running Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-final-*' } | Sort-Object LastWriteTime │ -Descending | Select-Object -First 1 -ExpandProperty FullName

• Ran Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-final-*' } | Sort-Object LastWriteTime │ -Descending | Select-Object -First 1 -ExpandProperty FullName └ C:\Users\ASUS-KL\AppData\Local\Temp\tokenpool-e2e-final-b76fdbaf-f96c-46e5-aa45-232bb713ed40

• Ran Get-CimInstance Win32_Process | Where-Object { $.Name -like 'powershell*' -and $.CommandLine -like 'tokenpool-e2e-final' } │ | Select-Object ProcessId,CommandLine | Format-Table -Wrap └ (no output)

• Running Get-ChildItem 'C:\Users\ASUS-KL\AppData\Local\Temp\tokenpool-e2e-final-b76fdbaf-f96c-46e5-aa45-232bb713ed40' -Force | │ Select-Object Name,Length,LastWriteTime | Format-Table -AutoSize

• Running Get-Content 'C: │ \Users\ASUS-KL\AppData\Local\Temp\tokenpool-e2e-final-b76fdbaf-f96c-46e5-aa45-232bb713ed40\server.stdout.log' -Tail 200

• Running Get-Content 'C: │ \Users\ASUS-KL\AppData\Local\Temp\tokenpool-e2e-final-b76fdbaf-f96c-46e5-aa45-232bb713ed40\providers-after.json' -Raw

• Ran Get-Content 'C:\Users\ASUS-KL\AppData\Local\Temp\tokenpool-e2e-final-b76fdbaf-f96c-46e5-aa45-232bb713ed40\providers-after.json' │ -Raw └ {"observedAt":"2026-04-28T03:14:46.263Z","providerTypes": └ Name Length LastWriteTime … +16 lines (ctrl + t to view transcript) server.stdout.log 4819 2026/4/28 11:14:46 [2026-04-28 11:14:46.282] [Req:127.0.0.1:e5693334] [INFO] [Server] Received request: GET http://127.0.0.1:3424/openai/debug/requests?limit=10

• Ran Get-Content 'C:\Users\ASUS-KL\AppData\Local\Temp\tokenpool-e2e-final-b76fdbaf-f96c-46e5-aa45-232bb713ed40\requests.json' -Raw └ {"observedAt":"2026-04-28T03:14:46.282Z","activeRequests": [{"requestId":"b731a875-ba60-4ac7-88b5-e3c7cf1a1802","traceId":"d13c2c74-5ac9-4e80-97e7-3437fe9483a8","clientIp":"127.0.0.1","met … +80 lines (ctrl + t to view transcript) token-pool-ingress.conf.example","routingPolicyConfigPath":"configs/routing-policies.json"}}

• Running rg -n "Unauthorized: API key is invalid or missing|invalid or missing|authResult|REQUIRED_API_KEY|x-goog-api-key| │ Authorization" "E:\My Project\Token Pool\src" -g '!/node_modules/'

• Running Get-Content "E:\My Project\Token Pool\src\services\api-manager.js" | Select-Object -First 260

• Ran Get-Content "E:\My Project\Token Pool\src\plugins\default-auth\index.js" -Raw └ /** * 默认认证插件 - 内置插件 … +90 lines (ctrl + t to view transcript)

└ import { handleModelListRequest, │ "E:\My Project\Token Pool\src" -g '!/node_modules/' └ E:\My Project\Token Pool\src\handlers\request-handler.js:139: res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-goog-api-key, Model-Provider, X-Requested-With, Accept, Origin'); … +69 lines (ctrl + t to view transcript) Bearer ${this.apiKey}; E:\My Project\Token Pool\src\providers\openai\iflow-core.js:774: 'Authorization': Bearer ${this.apiKey},

• Ran Get-Content "E:\My Project\Token Pool\src\plugins\default-auth\index.js" -Raw └ /** * 默认认证插件 - 内置插件 … +90 lines (ctrl + t to view transcript)

• Running Get-Content "E:\My Project\Token Pool\src\core\plugin-manager.js" | Select-Object -First 280

• Ran rg -n "Created plugins directory|builtin|discoverPlugins|plugins directory|src/plugins|process.cwd()" "E:\My Project\Token │ Pool\src\core\plugin-manager.js" -A 5 -B 5 └ 12-import logger from '../utils/logger.js'; 564- │ Pool\src\core\plugin-manager.js" -A 5 -B 5 └ 12-import logger from '../utils/logger.js'; 13-import { existsSync } from 'fs'; … +84 lines (ctrl + t to view transcript) 564- 565- for (const entry of entries) {

─ Worked for 3m 27s ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• Ran $ErrorActionPreference = 'Stop' │ $root = Join-Path $env:TEMP ('tokenpool-e2e-pass-' + [guid]::NewGuid().ToString()) │ $configDir = Join-Path $root 'configs' │ … +147 lines └ (no output)

• Running Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-pass-*' } | Sort-Object LastWriteTime │ -Descending | Select-Object -First 1 -ExpandProperty FullName

• Running Get-Content (Join-Path ((Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-pass-*' } | │ Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName)) 'requests.json') -Raw

• Running Get-Content (Join-Path ((Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-pass-*' } | │ Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName)) 'providers-after.json') -Raw

• Ran Get-Content (Join-Path ((Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-pass-*' } | │ Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName)) 'server.stdout.log') -Tail 220 └ [2026-04-28 11:16:26.558] [INFO] [Adapter] Registering adapter for provider: openai-custom [2026-04-28 11:16:26.563] [INFO] [Adapter] Registering adapter for provider: openaiResponses-custom … +9 lines (ctrl + t to view transcript) [2026-04-28 11:16:26.588] [INFO] [Initialization] Discovering and initializing plugins... [2026-04-28 11:16:26.610] [INFO] [ProviderPoolManager] Initialized provider statuses: ok (maxErrorCount: 10)

• Ran Get-Content (Join-Path ((Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-pass-*' } | │ Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName)) 'requests.json') -Raw └ {"observedAt":"2026-04-28T03:16:28.972Z","activeRequests": [{"requestId":"66cfc543-96e6-4315-9337-0baa8c996047","traceId":"e959b32d-285d-457b-91cb-8c17e60c932d","clientIp":"127.0.0.1","met └ [2026-04-28 11:16:26.558] [INFO] [Adapter] Registering adapter for provider: openai-custom [2026-04-28 11:16:26.563] [INFO] [Adapter] Registering adapter for provider: openaiResponses-custom … +49 lines (ctrl + t to view transcript) [2026-04-28 11:16:28.972] [Req:127.0.0.1:19eb2ea6] [INFO] [Server] Received request: GET http://127.0.0.1:3425/openai/debug/requests?limit=10 │ -Descending | Select-Object -First 1 -ExpandProperty FullName └ if (plugin) { plugin._enabled = enabled; … +53 lines (ctrl + t to view transcript) // 导出类和实例 export { PluginManager, pluginManager };

─ Worked for 1m 27s ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• [STEP] 我把认证链最后一个问题也定位到了。 [WHY] 只是复制 src/plugins 不够,default-auth 在隔离目录里还依赖 src/utils/logger.js;插件加载失败后,认证插件列表仍然是空的,所以 还是 401。 [ACTION] 我改成最小复制:只带 src/plugins/default-auth 和它依赖的 src/utils/logger.js。 [RESULT] 这样既保持隔离环境干净,也能跑通真实认证链。

• Ran $ErrorActionPreference = 'Stop' │ $root = Join-Path $env:TEMP ('tokenpool-e2e-authfix-' + [guid]::NewGuid().ToString()) │ $configDir = Join-Path $root 'configs' │ … +153 lines └ (no output)

• Running $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-authfix-*' } | Sort-Object │ LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName; Write-Output $dir; Get-Content (Join-Path $dir │ 'requests.json') -Raw

• Running $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-authfix-*' } | Sort-Object │ LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir └ C:\Users\ASUS-KL\AppData\Local\Temp\tokenpool-e2e-authfix-fdaeb022-bb50-417b-8757-9fa4d5147811 {"observedAt":"2026-04-28T03:18:09.818Z","activeRequests": … +94 lines (ctrl + t to view transcript) token-pool-ingress.conf.example","routingPolicyConfigPath":"configs/routing-policies.json"}}

• Ran $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $.Name -like 'tokenpool-e2e-authfix-*' } | Sort-Object │ LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir │ 'providers-after.json') -Raw └ {"observedAt":"2026-04-28T03:18:09.800Z","providerTypes": ["openaiResponses-custom"],"totalConfigured":2,"healthyProviders":2,"unhealthyProviders":0,"evaluatedModel":null,"evaluatedModelS … +9 lines (ctrl + t to view transcript) tErrorTime":null,"lastErrorMessage":null,"activeRequests":0,"waitingRequests":0,"fallbackChain":[]}]} • Ran $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $.Name -like 'tokenpool-e2e-authfix-*' } | Sort-Object │ LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir 'server.stdout.log') │ -Tail 220 └ [2026-04-28 11:17:58.223] [INFO] [Adapter] Registering adapter for provider: openai-custom [2026-04-28 11:17:58.227] [INFO] [Adapter] Registering adapter for provider: openaiResponses-custom … +74 lines (ctrl + t to view transcript) [2026-04-28 11:18:09.818] [Req:127.0.0.1:0574af21] [INFO] [Server] Received request: GET http://127.0.0.1:3426/openai/debug/requests?limit=10

• Updated Plan └ 本地 E2E 已经打通,但结果显示当前补丁还没真正命中 exhausted 分支:quota-node 只是按普通 429 被临时打掉并切到 healthy-node,没有写 入 scheduledRecoveryTime。继续留在本地 E2E 修复这一点。 □ Debug the local mock /v1/responses E2E timeout and obtain a clear pass/fail result □ If local E2E passes, inspect repo automation contract and current git state for safe delivery □ Push the quota-handling patch and verify live behavior from logs/debug endpoints

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• [STEP] 本地 E2E 现在已经有明确证据了,但结果是“半通过”。 [WHY] 请求确实从 quota-node 切到了 healthy-node,但日志显示它还是走了旧的普通 429 分支,没有进入我们要的 exhausted + scheduledRecoveryTime 分支。 [ACTION] 我继续本地修:把 OpenAI provider 在流式 429 场景下的错误体真正读出来,不再只看 Request failed with status code 429 这层 壳。 [RESULT] 下一步目标是让日志从 Marking ... as unhealthy (status: 429) 变成 Marking ... as exhausted until ...。

• Edited E:\My Project\Token Pool\src\providers\openai\quota-utils.js (+69 -0) 34 35 +async function readStreamBody(stream) { 36 + if (!stream || typeof stream.on !== 'function' || typeof stream[Symbol.asyncIterator] !== 'function') { 37 + return null; 38 + } 39 + 40 + const chunks = []; 41 + for await (const chunk of stream) { 42 + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))); 43 + const totalLength = chunks.reduce((sum, item) => sum + item.length, 0); 44 + if (totalLength >= 128 * 1024) { 45 + break; 46 + } 47 + } 48 + 49 + if (chunks.length === 0) { 50 + return null; 51 + } 52 + 53 + return Buffer.concat(chunks).toString('utf8'); 54 +} 55 + 56 +function parseBodyPayload(rawBody) { 57 + if (!rawBody) { 58 + return null; 59 + } 60 + 61 + if (Buffer.isBuffer(rawBody)) { 62 + rawBody = rawBody.toString('utf8'); 63 + } 64 + 65 + if (typeof rawBody === 'string') { 66 + try { 67 + return JSON.parse(rawBody); 68 + } catch { 69 + return { message: rawBody }; 70 + } 71 + } 72 + 73 + if (typeof rawBody === 'object') { 74 + return rawBody; 75 + } 76 + 77 + return null; 78 +} 79 + 80 function parseFiniteTimestamp(value) { ⋮ 213 } 214 + 215 +export async function extractOpenAIErrorPayload(error) { 216 + const status = error?.response?.status; 217 + const headers = error?.response?.headers || {}; 218 + let data = error?.response?.data; 219 + 220 + if (typeof data === 'string' || Buffer.isBuffer(data)) { 221 + data = parseBodyPayload(data); 222 + } else if (data && typeof data.on === 'function' && typeof data[Symbol.asyncIterator] === 'function') { 223 + const rawBody = await readStreamBody(data); 224 + const parsed = parseBodyPayload(rawBody); 225 + if (parsed) { 226 + data = parsed; 227 + error.response.data = parsed; 228 + } 229 + } 230 + 231 + return { 232 + status, 233 + headers, 234 + data: data && typeof data === 'object' ? data : parseBodyPayload(data), 235 + errorMessage: error?.message || '' 236 + }; 237 +}

• Edited E:\My Project\Token Pool\src\providers\openai\openai-core.js (+3 -9) 8 import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js'; 9 -import { detectOpenAIQuotaExhaustion } from './quota-utils.js'; 9 +import { detectOpenAIQuotaExhaustion, extractOpenAIErrorPayload } from './quota-utils.js'; 10 ⋮ 79 } catch (error) { 80 - const status = error.response?.status; 81 - const data = error.response?.data; 82 - const headers = error.response?.headers || {}; 80 + const { status, data, headers, errorMessage } = await extractOpenAIErrorPayload(error); 81 const errorCode = error.code; 84 - const errorMessage = error.message || ''; 82 const quotaSignal = detectOpenAIQuotaExhaustion({ ⋮ 186 } catch (error) { 190 - const status = error.response?.status; 191 - const data = error.response?.data; 192 - const headers = error.response?.headers || {}; 187 + const { status, data, headers, errorMessage } = await extractOpenAIErrorPayload(error); 188 const errorCode = error.code; 194 - const errorMessage = error.message || ''; • Edited E:\My Project\Token Pool\src\providers\openai\openai-responses-core.js (+3 -9) 8 import { MODEL_PROVIDER } from '../../utils/common.js'; 9 -import { detectOpenAIQuotaExhaustion } from './quota-utils.js'; 9 +import { detectOpenAIQuotaExhaustion, extractOpenAIErrorPayload } from './quota-utils.js'; 10 ⋮ 263 } catch (error) { 264 - const status = error.response?.status; 265 - const data = error.response?.data; 266 - const headers = error.response?.headers || {}; 267 - const errorMessage = error.message || '';

• Running node --check "src/providers/openai/quota-utils.js"

• Running node --check "src/providers/openai/openai-core.js"

• Ran node --check "src/providers/openai/openai-core.js" └ (no output)

• Ran node --check "src/providers/openai/openai-responses-core.js" └ (no output)

• Ran node --check "src/providers/openai/quota-utils.js" └ (no output)

• Ran $ErrorActionPreference = 'Stop' │ $root = Join-Path $env:TEMP ('tokenpool-e2e-retest-' + [guid]::NewGuid().ToString()) │ $configDir = Join-Path $root 'configs' │ LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName; Write-Output $dir; Get-Content (Join-Path $dir │ 'requests.json') -Raw

• Running $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-retest-*' } | Sort-Object │ LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir │ 'providers-after.json') -Raw

• Ran $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-retest-*' } | Sort-Object LastWriteTime │ -Descending | Select-Object -First 1 -ExpandProperty FullName; Write-Output $dir; Get-Content (Join-Path $dir 'requests.json') │ -Raw └ C:\Users\ASUS-KL\AppData\Local\Temp\tokenpool-e2e-retest-cce2a21f-8bc2-4f22-924a-bb51252ef5f2 {"observedAt":"2026-04-28T03:20:36.623Z","activeRequests": … +79 lines (ctrl + t to view transcript) token-pool-ingress.conf.example","routingPolicyConfigPath":"configs/routing-policies.json"}}

• Ran $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-retest-*' } | Sort-Object LastWriteTime │ -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir 'server.stdout.log') -Tail 240 └ [2026-04-28 11:20:28.022] [INFO] [Adapter] Registering adapter for provider: openai-custom [2026-04-28 11:20:28.026] [INFO] [Adapter] Registering adapter for provider: openaiResponses-custom http://127.0.0.1:3427/openai/debug/requests?limit=10

• Ran $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-retest-*' } | Sort-Object LastWriteTime │ -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir 'providers-after.json') -Raw └ {"observedAt":"2026-04-28T03:20:36.606Z","providerTypes": ["openaiResponses-custom"],"totalConfigured":2,"healthyProviders":1,"unhealthyProviders":1,"evaluatedModel":null,"evaluatedModelS … +9 lines (ctrl + t to view transcript) tErrorTime":null,"lastErrorMessage":null,"activeRequests":0,"waitingRequests":0,"fallbackChain":[]}]}

• Ran $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-retest-*' } | Sort-Object LastWriteTime │ -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir 'server.stdout.log') -Tail 240 └ [2026-04-28 11:20:28.022] [INFO] [Adapter] Registering adapter for provider: openai-custom [2026-04-28 11:20:28.026] [INFO] [Adapter] Registering adapter for provider: openaiResponses-custom … +71 lines (ctrl + t to view transcript) [2026-04-28 11:20:36.622] [Req:127.0.0.1:292b2153] [INFO] [Server] Received request: GET http://127.0.0.1:3427/openai/debug/requests?limit=10

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

• [STEP] 本地 E2E 已经命中 exhausted 分支了,但我又顺手抓到了一个恢复时间解析 bug。 [WHY] 日志已经变成 Marking ... as exhausted until ...,说明主逻辑通了;但 retry-after: 3600 被算成了 2082 年,恢复时间解析还不对。 [ACTION] 我现在把 retry-after 的秒/毫秒/日期三种格式拆开解析,避免把“绝对时间戳”和“相对等待时长”混淆。 [RESULT] 修完后,本地 E2E 应该能同时满足两点:进入 exhausted 分支、恢复时间合理。

• Edited E:\My Project\Token Pool\src\providers\openai\quota-utils.js (+26 -7) 133 for (const candidate of candidates) { 134 - const parsed = parseFiniteTimestamp(candidate); 135 - if (parsed === null) { 134 + if (candidate === null || candidate === undefined || candidate === '') { 135 continue; ⋮ 137 139 - const deltaMs = parsed > Date.now() + 60 * 1000 140 - ? parsed - Date.now() 141 - : parsed; 142 - if (Number.isFinite(deltaMs) && deltaMs > 0) { 143 - return deltaMs; 138 + if (typeof candidate === 'number' && Number.isFinite(candidate)) { 139 + if (candidate > 1e12) { 140 + return Math.max(0, candidate - Date.now()); 143 + return Math.max(0, (candidate * 1000) - Date.now()); 154 + if (numeric > 1e9) { 155 + return Math.max(0, (numeric * 1000) - Date.now()); 156 + } 157 + return numeric > 1000 ? numeric : numeric * 1000;

• Ran node --check "src/providers/openai/quota-utils.js" └ (no output)

• Ran $ErrorActionPreference = 'Stop' │ $root = Join-Path $env:TEMP ('tokenpool-e2e-finalpass-' + [guid]::NewGuid().ToString()) │ $configDir = Join-Path $root 'configs' │ … +144 lines └ (no output)

• Running $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-finalpass-*' } | Sort-Object │ LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir │ 'server.stdout.log') -Tail 120

• Ran $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-finalpass-*' } | Sort-Object │ LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir │ 'providers-after.json') -Raw … +8 lines (ctrl + t to view transcript) tErrorTime":null,"lastErrorMessage":null,"activeRequests":0,"waitingRequests":0,"fallbackChain":[]}]}

│ LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir 'server.stdout.log') │ -Tail 120 └ [2026-04-28 11:22:21.328] [INFO] [Adapter] Registering adapter for provider: openai-custom [2026-04-28 11:22:21.332] [INFO] [Adapter] Registering adapter for provider: openaiResponses-custom … +72 lines (ctrl + t to view transcript) [2026-04-28 11:22:30.035] [Req:127.0.0.1:f3793f5c] [INFO] [Server] Received request: GET http://127.0.0.1:3428/openai/debug/requests?limit=10

• Ran $dir = Get-ChildItem $env:TEMP -Directory | Where-Object { $_.Name -like 'tokenpool-e2e-finalpass-*' } | Sort-Object │ LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName; Get-Content (Join-Path $dir │ 'providers-after.json') -Raw └ {"observedAt":"2026-04-28T03:22:30.002Z","providerTypes": ["openaiResponses-custom"],"totalConfigured":2,"healthyProviders":2,"unhealthyProviders":0,"evaluatedModel":null,"evaluatedModelS … +8 lines (ctrl + t to view transcript) tErrorTime":null,"lastErrorMessage":null,"activeRequests":0,"waitingRequests":0,"fallbackChain":[]}]}