feat: preserve parallel sub2api diagnostics work
This commit is contained in:
@@ -6,6 +6,11 @@
|
||||
- PK01 路径不创建 k8s Secret、不部署 sentinel 资源,也不触发 `sub2api apply`、Docker compose、Caddy reload 或容器重启;
|
||||
- `runtime list|get` 查询 pool group 内全部账号或指定账号的实际状态;`get` 同时披露脱敏 `modelMapping` 和完整临时不可调度规则;
|
||||
- `runtime models --account` 通过 Sub2API 原生账号模型接口同时读取配置模型和上游实时模型,不把配置映射误当作真实上游能力;
|
||||
- `runtime infrastructure --account` 只读关联账号 proxy 绑定与基础设施状态:
|
||||
- 同时披露账号 proxy 记录和容器代理环境,不能因容器没有 `HTTP_PROXY` 就把账号误判为直连;
|
||||
- 采集容器网络模式、DNS、默认路由、接口计数器、proxy socket、监听进程、服务状态和近期错误日志;
|
||||
- 不发送上游账号测试或网络探针,不修改账号、proxy、调度或 YAML;
|
||||
- socket、接口计数器和服务状态是查询时快照,不能单独证明历史故障窗口内网络正常;
|
||||
- `runtime errors` 优先使用 Sub2API 原生 admin/ops 能力:
|
||||
- `--all-groups` 按稳定 group ID 游标分页返回所有分组概要;
|
||||
- `--group <name-or-id>` 返回指定分组的账号根因、策略效果和 request 索引;
|
||||
@@ -17,6 +22,17 @@
|
||||
- 客户错误画像提供模型、账号、状态码、同步/流式、阶段、入口和精确请求路径分布;
|
||||
- 原生接口没有账号维度 TTFT 时禁止从错误账号分布推导账号 TTFT;
|
||||
- 默认只给有界 request ID 索引,使用 `trace` 做精确下钻;
|
||||
- 指定分组和 `--account` 下钻提供上游账号质量评分:
|
||||
- 成功请求分母和账号级 TTFT 来自原生 admin usage;
|
||||
- 客户错误来自原生 request-errors;
|
||||
- 切号、同账号重试、临时不可调度和 `forward_failed` 来自原生 system-log 索引;
|
||||
- 默认权重为可靠性 60、TTFT 25、当前可用性 15,并同时输出评分组成、等级、置信度和扣分原因;
|
||||
- TTFT 少于 5 个样本时不进入分母,不能把缺失时延当成零时延;
|
||||
- 至少 10 次账号尝试且至少 5 个 TTFT 样本时,才允许把高分映射为 A-D 可比较等级;样本不足的偶然高分标记为 `insufficient`;
|
||||
- 至少 10 次账号尝试且评分低于 60 时,即使 TTFT 缺失也保留 E 级,避免明确的高失败率被“证据不足”掩盖;
|
||||
- 请求证据不足时使用低置信度,不能据此自动调整 priority、capacity 或 schedulable;
|
||||
- 账号名称是人类可读运维标识,应与账号 ID 一起完整输出;API Key、token 和 credentials 仍必须脱敏;
|
||||
- 只有带 `group_id` 的 failover 和 `forward_failed` request ID 才进入分组账号失败率;账号级临时不可调度和同账号重试可以作为共享运维信号展示,但不加入分组失败分子;
|
||||
- 账号被多个分组共享时,账号级 upstream-errors 和缺少 `group_id` 的 runtime 事件属于共享证据,禁止跨分组相加;
|
||||
- dashboard overview 提供请求分母和错误率;
|
||||
- dashboard overview 的 `ttft` 提供成功流式请求的首 token 分位数,`duration` 提供成功请求总耗时分位数;二者不能互相替代;
|
||||
@@ -42,6 +58,7 @@
|
||||
- 批量操作先校验全部 selector 存在且账号类型可修改,再顺序调用 Sub2API admin API:
|
||||
- 选择错误不会产生部分写入;
|
||||
- 上游 admin API 在批次中途失败时仍需逐账号 `get` 对账;
|
||||
- runtime 模板必须按可复用错误语义设计,不得包含账号名称、账号 ID、provider URL 或分组特例;非 auth 错误使用精确状态码和稳定短语,auth 与账号状态错误保留专门处理路径;
|
||||
- 写操作不带 `--confirm` 时只返回 dry-run,带 `--confirm` 才通过 Sub2API admin API 写入;
|
||||
- 批量默认输出账号 ID、名称、change、mode、mutation 和汇总计数;runtime 命令不输出 API key 或完整 credentials,使用 `get`、`--full` 或 `--raw` 下钻;
|
||||
- 文档中的 runtime action 或参数被当前分支 CLI 拒绝时,按 CLI 集成漂移处理:
|
||||
@@ -58,6 +75,7 @@ bun scripts/cli.ts platform-infra sub2api codex-pool validate --target D601
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime list --target PK01
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime get --target PK01 --account <name-or-id>
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime models --target PK01 --account <name-or-id>
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime infrastructure --target PK01 --account <name-or-id> --since 30m
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors --target PK01 --since 24h
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors --target PK01 --since 24h --all-groups
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors --target PK01 --since 24h --group <name-or-id>
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
- 没有账号真实支持目标模型时,保持池不声明该模型并快速失败,不映射到名称相近的其他模型。
|
||||
- 上游模型同步失败只能标记证据不足,不能据此补映射。
|
||||
5. 调优按所有权下发:
|
||||
- 非 auth 上游错误优先抽象为通用临时不可调度模板:
|
||||
- 规则同时使用真实状态码和已证实的稳定响应短语;
|
||||
- 先从当前 Sub2API 源码确认规则命中会在响应提交前触发当前请求 failover;
|
||||
- 禁止在模板、CLI 或源码中硬编码账号名称、账号 ID、provider URL 或分组特例;
|
||||
- 禁止用通用 400、通用 `invalid request` 或宽泛关键词覆盖真实客户参数错误;
|
||||
- auth、token revoked、余额不足、分组删除和凭据失效使用专门的认证与账号状态路径,不混入通用非 auth 模板;
|
||||
- 通用模板先修改 owning YAML,并保持冷却保守;默认从一分钟开始,没有新证据和明确授权不得超过三分钟。
|
||||
- YAML-managed 账号走 `sync`;runtime-manual 账号走显式 `runtime apply|delete`。
|
||||
- 多账号使用 `--accounts` 精准集合;例外账号从 selector 中显式排除,不使用隐式全量操作。
|
||||
|
||||
@@ -16,6 +16,13 @@ description: UniDesk Web 开发与浏览器验证技能。用户处理 UniDesk/H
|
||||
- 调试 HWLAB Cloud Web/Workbench 业务或功能 bug 时,使用 YAML 声明的 `--origin internal`;验收 public exposure、DNS、FRP、Caddy 或公网用户入口时,显式使用 `--origin public`。
|
||||
- `--url` 只是 custom/local 一次性逃生口,与 `--origin` 互斥;禁止通过手写 URL 或 IP 选择内网/公网运行面。本地 fake-server、localhost、dist 静态服务和 custom URL 只能作为开发 preflight 证据。
|
||||
- 禁止在本地或 master server 直接跑 `vue-tsc` / 前端全量 typecheck 作为默认验证;本地只做语法级检查和真实入口复测,完整类型检查交给 CI、PipelineRun 或明确指定的受控构建运行面。
|
||||
- HWLAB Cloud Web 的 Vitest 防误用规则:
|
||||
- 以 HWLAB `docs/reference/web-test-resource-guard.md` 为权威;
|
||||
- 配置必须默认关闭 watch、固定单 worker、限制 worker V8 堆;
|
||||
- 测试、hook 和 teardown 必须设置有界 timeout;
|
||||
- 正式 `test` 入口必须使用 `vitest run`、限制父进程 V8 堆并设置整体执行时限;
|
||||
- Master server 只允许解析配置和检查 diff,不运行测试;
|
||||
- Vitest 配置不能替代系统 cgroup `MemoryMax`。
|
||||
- 桌面版 Web 截图、视觉复测和 web-probe 采样默认使用 `1920x1080` 视口;只有用户、issue 或 SPEC 明确指定其他尺寸时才覆盖,并在证据中写明覆盖原因。
|
||||
- Web probe 命令和历史判定口径见 [references/web-probe.md](references/web-probe.md);Playwright/fake-server 复现见 [references/playwright.md](references/playwright.md)。
|
||||
- 前端改动遵循仓库既有设计系统、[references/design.md](references/design.md) 和 `$frontend-design` 全局 UI 规则;不要做营销式落地页替代真实工具页面。
|
||||
|
||||
@@ -17,9 +17,13 @@ runtime:
|
||||
enabled: true
|
||||
rules:
|
||||
- statusCode: 429
|
||||
keywords: [concurrency limit exceeded]
|
||||
keywords: [concurrency limit exceeded, upstream rate limit exceeded]
|
||||
durationMinutes: 1
|
||||
description: 明确命中上游并发限制时短时冷却当前账号。
|
||||
description: 明确命中上游并发或速率限制时短时冷却当前账号。
|
||||
- statusCode: 400
|
||||
keywords: [input must be a list]
|
||||
durationMinutes: 1
|
||||
description: 明确命中 Responses input 列表兼容错误时短时冷却并切换账号。
|
||||
- statusCode: 500
|
||||
keywords: [failed to validate api key, do request failed]
|
||||
durationMinutes: 1
|
||||
@@ -32,6 +36,10 @@ runtime:
|
||||
keywords: [upstream service temporarily unavailable, upstream request failed, service temporarily unavailable, temporarily unavailable, overloaded, concurrency limit exceeded]
|
||||
durationMinutes: 1
|
||||
description: 明确命中 503 上游暂时不可用、过载或并发限制时短时冷却当前账号。
|
||||
- statusCode: 504
|
||||
keywords: [concurrency limit exceeded, upstream service temporarily unavailable, input must be a list]
|
||||
durationMinutes: 1
|
||||
description: 明确命中 504 上游并发限制、暂时不可用或 Responses input 列表兼容错误时短时冷却当前账号。
|
||||
- statusCode: 524
|
||||
keywords: [upstream request failed, service temporarily unavailable, overloaded, concurrency limit exceeded]
|
||||
durationMinutes: 1
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# R2.2.10 任务报告
|
||||
|
||||
## 结论
|
||||
|
||||
- 最近 30 分钟复取时,账号 `lyon9801`(ID 15)共 37 次尝试、37 次成功、0 次失败,TTFT P95 为 9040 ms;“自用”分组唯一客户错误来自账号 27 的流式 504。因此 `lyon9801` 的 503 是更早的孤立事件,不是当前持续不稳定。
|
||||
- `lyon9801` 不是 Sub2API 直连账号。它绑定 proxy 3:`platform-infra-sub2api-pk01-local-egress-proxy`,HTTP endpoint 为 `127.0.0.1:10809`。实际路径是 PK01 `sub2api-app` → PK01 本地 Hysteria 客户端 → NC01 Hysteria 服务端 → OpenAI。
|
||||
- 代表请求 `da415526-b124-4e23-a4a7-ea48154c851e` 在请求开始约 1556485 ms 后收到上游 HTTP 503。Sub2API 随即进入 failover,但 1 ms 后选号因 `context canceled` 失败,最终返回 502。客户影响的主要放大器是错误发现过晚,切号时已无可执行上下文,而不是 failover 未实现。
|
||||
- 另一请求 `4bba7f9b-15e3-4891-b063-bd65fb6e3704` 在 HTTP 200 后出现上游 `response.failed`,现由 trace 正确标记为 `degraded / forward-failed-http-success`。这是响应已提交后的流内失败,不能安全切号,也不等同于 proxy 连接失败。
|
||||
- 当前证据更支持 OpenAI/Codex 上游返回 HTTP 503 或应用层流失败,不支持 PK01 DNS、proxy connect、TCP reset、容器重启、OOM 或 NC01 Hysteria 重启为主因。历史瞬时丢包仍不能凭当前快照绝对排除。
|
||||
|
||||
## 基础设施证据
|
||||
|
||||
- PK01 `sub2api-app`:running,restart 0,OOM false,host network。
|
||||
- 账号 proxy 3:active,`127.0.0.1:10809`;当前有 4 条 established 本地 TCP socket。
|
||||
- PK01 proxy owner:`hysteria`,`host-proxy-hysteria.service` active/running,restart 0;最近两小时 journal 未命中 reconnect、disconnect、timeout 或 error。
|
||||
- PK01 DNS 为 `127.0.0.53`,默认路由经 `eth0 / 10.0.8.1`;可见接口 RX/TX error/drop 均为 0。
|
||||
- NC01 服务端容器:`vpn-hysteria2`,镜像 `tobyxdd/hysteria:v2.9.3`,running,restart 0,OOM false,host network;运行文件实际位于 `/root/vpn-server`。故障时刻附近没有对应 tunnel disconnect/reconnect 日志。
|
||||
- 账号 15 的近期 Sub2API 网络错误索引没有 DNS failure、proxy connect failure、connection reset、unexpected EOF、broken pipe、connection refused、network unreachable 或 transport timeout。
|
||||
|
||||
## Sub2API v0.1.153 源码归因
|
||||
|
||||
- 账号存在 `ProxyID` 时,OpenAI forward 把账号 proxy URL 传给 HTTP upstream;HTTP proxy 通过 `http.Transport.Proxy` 生效。因此容器没有 `HTTP_PROXY` 环境变量不能证明账号直连。
|
||||
- HTTP 503 属于已收到 HTTP response 的路径;所有 5xx 会构造 `UpstreamFailoverError` 并触发账号切换。
|
||||
- proxy、DNS、TCP 或 TLS 在 HTTP round trip 前失败时走 transport-error 路径,Ops status 为 0,最终 failover error 为 502。账号 15 的代表证据是 upstream status 503,不符合该传输故障路径。
|
||||
- OpenAI response-header timeout 在 v0.1.153 默认是 0;PK01 运行实例也显示 disabled。它允许请求长时间等待,但现有 Ops 只有端到端耗时,不能把 1556485 ms 全部断言为等待响应头。
|
||||
- 客户 context 已取消时,源码明确不再 failover;这解释了 503 后 1 ms 即 `account_select_failed: context canceled`。
|
||||
|
||||
## CLI 增强
|
||||
|
||||
新增只读入口:
|
||||
|
||||
```bash
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime infrastructure \
|
||||
--target PK01 --group 3 --account lyon9801 --since 30m
|
||||
```
|
||||
|
||||
默认 Kubernetes 风格文本输出账号名称、proxy 绑定、容器状态、代理环境摘要、DNS、默认路由、接口计数器、socket、监听进程、服务状态和近期网络错误分类。该命令不发上游请求或网络探针,不修改账号、proxy、runtime、调度或 YAML;机器读取仍需显式 `--json`。
|
||||
|
||||
## 建议
|
||||
|
||||
- 暂不因这次孤立 503 降权或摘除 `lyon9801`;最近 30 分钟为 37/37 成功。
|
||||
- 下一步源头优化优先研究 OpenAI response-header timeout 与客户 deadline/failover 预算的配合,使 5xx 在客户上下文取消前暴露;这属于配置候选,当前未实验、未下发。
|
||||
- 对 HTTP 200 后 `response.failed`,应继续作为 degraded 客户可见错误单列;响应已提交后不能依靠换号掩盖。
|
||||
- 若再次发生,直接用 request ID 同时抓 trace 与 infrastructure,并保留故障时刻的 Hysteria per-session 指标;不要用故障后的 socket 快照替代历史证据。
|
||||
|
||||
## 验证
|
||||
|
||||
- `bun --check scripts/src/platform-infra-sub2api-codex/runtime.ts`
|
||||
- `bun --check scripts/src/platform-infra-sub2api-codex/remote-python-sync.ts`
|
||||
- `bun --check scripts/src/output.ts`
|
||||
- `bun test scripts/src/output.test.ts`:2 pass,0 fail。
|
||||
- `git diff --check`
|
||||
- `runtime.ts`:2970 行,低于 3000 行硬上限。
|
||||
@@ -0,0 +1,68 @@
|
||||
# R2.2.11 任务报告
|
||||
|
||||
## 范围与口径
|
||||
|
||||
- 只读复取 PK01 Sub2API 最近2小时所有分组,并以最近30分钟作为趋势对照。
|
||||
- 请求分母、客户错误和上游错误以 Sub2API 原生 admin/ops 为主;system-log 缺失时才使用 target runtime 日志。
|
||||
- 分组与账号数据在滚动窗口内持续变化,报告使用本轮最后一次完整分组快照;共享账号的 upstream cause 不跨组相加。
|
||||
- 未发送探针,未修改 runtime、YAML、proxy、账号或调度配置。
|
||||
|
||||
## 所有分组概要
|
||||
|
||||
最近2小时初始总览:1391请求、37个客户错误,客户错误率2.66%;74个上游错误,上游错误率5.32%。滚动窗口下钻时请求数增长到约1403,但客户错误总数仍为37。
|
||||
|
||||
- `unidesk-codex-pool`:约763请求、26客户错误,错误率3.41%;上游错误率约7.86%。
|
||||
- `自用`:约640请求、11客户错误,错误率1.72%;上游错误率约2.19%。
|
||||
- `grok`:0请求、0错误。
|
||||
- 两个活跃组均无排队,查询时并发约9至10/218;没有证据把主因归为容量不足或需要降低并发。
|
||||
- 账号29 `https://api.lwylink.xyz pro 0.1` 与账号30 `https://api.lwylink.xyz plus 0.05` 因余额不足403不可用;其余7个OpenAI账号当前可用。
|
||||
|
||||
最近30分钟对照:412请求、19客户错误,错误率4.61%;27个上游错误,上游错误率6.55%。其中:
|
||||
|
||||
- `unidesk-codex-pool`:251请求、17客户错误,错误率6.77%。
|
||||
- `自用`:160请求、2客户错误,错误率1.25%。
|
||||
- 错误没有整体消退,而是更集中到 `unidesk-codex-pool`。
|
||||
|
||||
## 主因一:账号27的Responses兼容性400
|
||||
|
||||
账号27 `https://sub.yjxm1221.top plus 0.025` 是当前客户错误主因:
|
||||
|
||||
- 2小时:139次尝试、91成功、48失败,失败率34.53%,评分38/E;26个分组客户错误中占24个。
|
||||
- 最近30分钟:48次尝试、30成功、18失败,失败率37.50%;17个客户错误全部来自账号27且全部是400。
|
||||
- 2小时客户错误状态分布中400有21个,另有404、503、524;模型以 `gpt-5.6-terra`、`gpt-5.6-luna`、`gpt-5.6-sol` 为主。
|
||||
- 代表请求 `1f3fe800-a72a-4383-88ba-e7311abb8534` 在630 ms内失败,原文为 `Input must be a list`。
|
||||
- 代表请求 `ee991d2f-08fe-4251-a509-c3d1a4bd8e45` 先经历账号33的524和账号28的503,切到账号27后返回 `Invalid 'input[213].id'`,最终502。
|
||||
- 另有 `invalid_id_prefix` 类流式 `response.failed`。这些更符合该上游对Responses长历史/input item格式的兼容缺陷,不是并发、网络或普通5xx不稳定。
|
||||
- 通用400不能直接加入临时不可调度或failover,否则会把真实客户参数错误错误切号;若后续治理,应只针对已证实的精确兼容错误短语和未提交响应阶段处理。
|
||||
|
||||
## 主因二:长等待后504,切号已无时间窗
|
||||
|
||||
`自用`组11个客户错误全部是 `gpt-5.6-sol`:8个504、2个503、1个524,10个为流式。
|
||||
|
||||
- `lyon9801` 2小时109次尝试、100成功、9失败,失败率8.26%,评分75.2/C;8个客户错误,8次failover全部失败。
|
||||
- 请求 `02f0cc91-6f7f-4106-b902-49ee5260ed57` 等待约29分31秒后才收到账号15的504;1 ms后选号因 `context canceled` 失败。
|
||||
- 最近30分钟 `lyon9801` 已恢复为34/34成功、TTFT P95约7.8秒;窗口内2个客户错误均来自账号27,不是账号15。
|
||||
- 当前 OpenAI response-header timeout 为disabled。长等待是客户影响放大器,但现有数据只有端到端耗时,不能断言全部时间都在等待响应头,也不能直接把观测下限当配置值。
|
||||
|
||||
## 上游隔离与切号效果
|
||||
|
||||
- 账号28 `https://ai.whistlelads.com plus 0.03`:2小时24次尝试全部触发失败,但有26次临时不可调度事件,分组客户错误为0;大量错误被换号吸收。最近30分钟7次失败中5次切号恢复、2次失败。
|
||||
- 账号31 `https://sub2.pokexiao.com plus 0.03`:2小时215次尝试仅1次失败,评分93.1/A;最近30分钟126次中124成功,2次failover均恢复。
|
||||
- 账号33 `https://api.iceiu.com plus 0.03`:2小时约3.6%失败,评分B;最近30分钟失败率升到14.55%,并有6次HTTP 200后forward failure,需要关注。
|
||||
- 成功切号样本 `4d305f0f-3e4f-4e0d-86d1-060a03820d22`:账号32返回524、账号28返回503,约14秒后由账号27完成并最终HTTP 200,证明5xx failover链路能工作。
|
||||
|
||||
## HTTP 200后的客户体验错误
|
||||
|
||||
- `unidesk-codex-pool` 2小时有12个、最近30分钟有6个 `forwardFailureWithHttpSuccess`。
|
||||
- `自用` 2小时有3个、最近30分钟有1个同类事件。
|
||||
- 请求 `e41f8745-e223-4fbf-9fb8-041a40c8ad94` 最终HTTP 200,但约417秒后出现 `stream data interval timeout`,trace正确标记为degraded。
|
||||
- 这类请求不会进入普通HTTP客户错误分子,因此37个客户错误不是全部体验损失;分组间存在共享账号证据,不能简单把所有forward failure相加成全局唯一请求数。
|
||||
|
||||
## 调优建议
|
||||
|
||||
1. 优先治理账号27的精确Responses兼容错误族,而不是降低全池并发:先按错误短语、请求阶段和模型统计;候选方案是对已证实的 `Input must be a list`、`Invalid input[*].id`、`invalid_id_prefix` 在响应未提交前触发failover或暂时避免把对应请求形态路由到该账号。是否需要Sub2API源码变更取决于现有精确classifier能否配置表达,不能用通用400规则替代。
|
||||
2. 研究上游超时与客户deadline的预算关系:目标是让504或无响应在客户context取消前暴露并留出切号时间。缺少客户deadline和response-header阶段耗时前,不直接给出安全timeout数值。
|
||||
3. 保留账号28现有短冷却策略;它已显著避免客户错误,不建议仅因上游失败率高而长期禁用。
|
||||
4. 对账号33继续短窗口观察,重点看HTTP 200后 `response.failed`/stream timeout;这类错误无法在响应已提交后安全换号。
|
||||
5. 账号29、30的余额不足属于静态不可用,应由账号所有者补充余额或退役;当前无排队,暂不构成容量事故。
|
||||
6. 不因2小时旧错误降权 `lyon9801`;最近30分钟已恢复。若再次出现长等待504,按request ID同步抓trace和基础设施快照。
|
||||
@@ -0,0 +1,66 @@
|
||||
# R2.2.12 任务报告
|
||||
|
||||
## 结果
|
||||
|
||||
已为 runtime-manual 账号 `https://sub.yjxm1221.top plus 0.025`(ID 27)下发精确 `Input must be a list` 临时不可调度规则:
|
||||
|
||||
- statusCode:400
|
||||
- keyword:`input must be a list`
|
||||
- duration:1分钟
|
||||
- 行为:命中后当前账号短时冷却,当前请求返回 `UpstreamFailoverError` 进入切号。
|
||||
|
||||
未使用通用400匹配,避免把真实客户参数错误错误切号。
|
||||
|
||||
## 所有权与配置
|
||||
|
||||
- owning YAML:`config/platform-infra/sub2api-codex-pool.yaml`
|
||||
- 模板:`codex-upstream-failover`
|
||||
- 账号管理类型:`runtime-manual`
|
||||
- 下发方式:精准 `runtime apply --account 27 --template codex-upstream-failover --confirm`
|
||||
- 未把账号27加入 YAML managed profiles,也未运行全量 `sync`。
|
||||
|
||||
为避免模板覆盖账号27既有运行规则,YAML模板同时对齐了其已有:
|
||||
|
||||
- 429 `upstream rate limit exceeded`
|
||||
- 504 `concurrency limit exceeded` / `upstream service temporarily unavailable`
|
||||
|
||||
最终规则从6条增至7条,保留429、500、502、503、504、524,仅新增精确400。
|
||||
|
||||
## 源码确认
|
||||
|
||||
Sub2API v0.1.153 普通400进入 `handleErrorResponse` 后会调用 `handleOpenAIAccountUpstreamError`;`RateLimitService.HandleUpstreamError` 在默认400处理前执行 `tryTempUnschedulable`。规则命中返回 `shouldDisable=true`,OpenAI gateway 构造 `UpstreamFailoverError`,因此既冷却后续调度,也会为当前请求触发切号。
|
||||
|
||||
## 执行证据
|
||||
|
||||
Dry-run:
|
||||
|
||||
- account:`https://sub.yjxm1221.top plus 0.025` / 27
|
||||
- change:update
|
||||
- beforeRules:6
|
||||
- afterRules:7
|
||||
- beforeCodes:429,500,502,503,504,524
|
||||
- afterCodes:400,429,500,502,503,504,524
|
||||
|
||||
确认下发:`mutation=true`。最终 `runtime get`:
|
||||
|
||||
- management:runtime-manual
|
||||
- schedulable:true
|
||||
- matching rule count:7
|
||||
- status codes:400,429,500,502,503,504,524
|
||||
- pool mode:false
|
||||
|
||||
## 短窗口
|
||||
|
||||
下发后5分钟只读窗口:
|
||||
|
||||
- `unidesk-codex-pool`:43请求、0客户错误、客户错误率0%。
|
||||
- 账号27:7次尝试、7次成功、0失败。
|
||||
- 窗口内没有自然出现新的 `Input must be a list`,因此尚未观察到真实 `account_temp_unschedulable` 命中;不能把“零命中”解释为规则无效。
|
||||
|
||||
## 验证
|
||||
|
||||
- 精准 runtime apply dry-run通过。
|
||||
- 精准 runtime apply confirm通过。
|
||||
- 逐账号 runtime get对账通过。
|
||||
- `git diff --check`通过。
|
||||
- 未发送错误探针,未修改proxy、账号凭据、priority、capacity、membership或其他调度配置。
|
||||
@@ -0,0 +1,61 @@
|
||||
# R2.2.13 任务报告
|
||||
|
||||
## 下发结果
|
||||
|
||||
已向PK01当前全部非 `lyon9801` OpenAI账号精准批量下发 `codex-upstream-failover`:显式selector为19、27、28、29、30、31、32、33、34。
|
||||
|
||||
- selected:9
|
||||
- changed:8
|
||||
- succeeded:8
|
||||
- failed:0
|
||||
- noop:账号27已提前匹配模板
|
||||
- `lyon9801`(15):未进入selector,保持0条规则、无模板
|
||||
|
||||
最终账号19、27、28、29、30、31、32、33、34均为7条规则,状态码集合400、429、500、502、503、504、524,模板均为 `codex-upstream-failover`。账号29、30仍保持余额不足导致的error与schedulable=false;模板下发未恢复账号状态。
|
||||
|
||||
## CLI修复
|
||||
|
||||
原CLI文档声明支持 `--accounts`,但parser实际拒绝。已恢复通用精准批量入口:
|
||||
|
||||
- 支持1至100个逗号分隔ID或精确名称;
|
||||
- `--account`与`--accounts`互斥;
|
||||
- 仅apply/delete接受批量selector;
|
||||
- 先解析全部selector并校验账号类型,再进行任何写入;
|
||||
- 默认Kubernetes风格批量计划与结果表;
|
||||
- 参数解析拆到 `runtime-options.ts`,`runtime.ts`收敛到2915行。
|
||||
|
||||
Artificer后续任务R2.2.14 / issue #1998负责把确认写入后的自动批量回读对账内建到CLI,消除外部逐账号get。
|
||||
|
||||
## 真实效果
|
||||
|
||||
下发后首个5分钟窗口:
|
||||
|
||||
- 约85请求;
|
||||
- 5个上游错误;
|
||||
- 0客户错误;
|
||||
- `unidesk-codex-pool`:5次临时不可调度、3次failover、3次全部恢复;
|
||||
- `自用`:2次failover、2次全部恢复。
|
||||
|
||||
请求 `290215cf-e32b-44df-b2d6-842298c7e690` 证明新400规则真实命中:账号27收到上游400,规则1将其冷却1分钟,当前请求切到账号33,4.055秒后最终HTTP 200,错误未暴露给客户。
|
||||
|
||||
10分钟窗口出现的请求 `68cd320a-4d2b-482e-93ed-284625096d8f` 是模板下发前开始的旧长请求:等待约719秒后502,切号时context canceled,不能据此判定新模板失效。
|
||||
|
||||
## Skill固化
|
||||
|
||||
账号排障reference已固化:
|
||||
|
||||
- 非auth错误优先使用状态码加稳定短语的通用模板;
|
||||
- 先确认响应提交前能触发当前请求failover;
|
||||
- 禁止账号、ID、provider URL或分组硬编码;
|
||||
- 禁止通用400或宽泛invalid request;
|
||||
- auth/token/余额/凭据状态保留专门路径。
|
||||
|
||||
## 验证
|
||||
|
||||
- 批量dry-run:9个selector全部解析,8 update、1 noop。
|
||||
- 批量confirm:8 succeeded、0 failed。
|
||||
- 再次批量dry-run:9/9 noop。
|
||||
- 逐账号运行态对账完成;后续由R2.2.14把该过程内建到CLI。
|
||||
- `bun --check`通过。
|
||||
- `git diff --check`通过。
|
||||
- skill quick validate通过。
|
||||
@@ -0,0 +1,14 @@
|
||||
# R2.2.15 任务报告
|
||||
|
||||
Artificer 已完成只读调查并把完整证据写入 [GitHub Issue #1999 评论](https://github.com/pikasTech/unidesk/issues/1999#issuecomment-4966948330)。
|
||||
|
||||
核心结论:
|
||||
|
||||
- 精确“状态码 + 稳定短语”模板适合吸收可确认的非 auth 上游兼容错误;不得使用 generic 400、generic invalid request 或账号/provider 特例。
|
||||
- 400 `Input must be a list` 已有真实切号成功证据。本轮新请求 `e5c08dde-9e60-4a14-9458-5574cec00b65` 进一步证明:首跳账号 `https://sub.yjxm1221.top plus 0.025` 的 400 可冷却并切号,但第二跳账号 `https://sub2.pokexiao.com plus 0.03` 等待约 60.6 秒后返回 504,第二次 failover 时 context 已取消。
|
||||
- 主代理已把同一精确短语加入通用 504 规则并下发非 `lyon9801` 账号;这只降低后续重复命中,不能缩短首次长等待。
|
||||
- `Expected an ID that begins with 'ctc'` 与 `invalid_id_prefix` 暂不下发,需先有跨账号成功切换证据,避免把客户载荷或会话亲和错误误判为上游故障。
|
||||
- 根治首次长等待穿透需要 Sub2API 源码能力:客户总 deadline、动态每跳首字/响应头预算及 failover reserve;静态 response-header timeout 或 Caddy timeout 不能完整替代。
|
||||
- 流式响应一旦向客户提交字节,Sub2API 不能安全重放,只能终止流并记录错误。
|
||||
|
||||
本子任务未发送探针,未修改 runtime、YAML、账号、proxy、调度或源码,无 commit、PR 和 rollout。
|
||||
@@ -0,0 +1,23 @@
|
||||
# R2.2.16 任务报告
|
||||
|
||||
## 结论
|
||||
|
||||
用户 `mail0795336304@163.com` 的“卡住约 15 分钟”不发生在 Sub2API 的在途请求、账号排队或 proxy 3 出网阶段。服务端证据显示,上一笔请求于 2026-07-14 14:43:33(UTC+8)成功结束,下一笔直到 14:59:54 才进入 Sub2API,中间约 16 分 21 秒没有该用户的新入站请求。卡点因此位于 Sub2API 入站之前,优先怀疑客户端、本地 Agent 状态机或客户端到入口之间的请求提交链路。
|
||||
|
||||
## 原生运维证据
|
||||
|
||||
- 用户 ID:22;账号状态 active;用户并发上限 10,调查时 current concurrency 为 0。
|
||||
- API Key:名称 `pika_win`、ID 46、分组 `unidesk-codex-pool`;调查时 current concurrency 为 0。
|
||||
- 最近 30 分钟共 7 笔请求,均为成功,无用户可见错误。
|
||||
- 14:43:33 完成请求 `client:1daa3f3e-99a5-4fc4-809c-6f6ef854a2b2`,流式、`gpt-5.6-sol`,耗时 28.922 秒,账号 33。
|
||||
- 14:59:54 下一笔请求 `client:8d1f8ef9-e703-482c-ace9-0d8f13a5c8e3` 才进入服务端;内部 request ID `efdb251f-8e86-4f18-99ee-fa1094419dc0`,15:00:43 完成,HTTP 200,耗时 49.115 秒。
|
||||
- 最新请求使用上游账号 `https://api.iceiu.com plus 0.03`(ID 33)、模型 `gpt-5.6-sol`、proxy 3;没有 failover、select failure 或 forward failure。
|
||||
- proxy 3 为 `platform-infra-sub2api-pk01-local-egress-proxy`,127.0.0.1:10809;对应 Hysteria 客户端自 7 月 8 日持续 active,调查窗口无错误日志,当前存在正常连接。
|
||||
|
||||
## 判定边界
|
||||
|
||||
Sub2API v0.1.153 当前原生 request 明细记录请求完成耗时,但未记录流式首字时间和逐事件间隔。因此不能从现有数据精确证明客户端何时展示首字;不过“16 分 21 秒无入站请求”、用户/API Key 并发均为 0,以及随后请求 49 秒正常完成,足以排除 Sub2API 内部排队、上游账号占槽 15 分钟和 proxy 3 在该窗口报错。
|
||||
|
||||
## 建议
|
||||
|
||||
让用户侧保留该时段客户端日志,重点检查本地 Agent 是否在上一轮完成后未推进状态、请求是否直到 14:59:54 才真正发出,以及本地 HTTP/SSE 客户端是否错误等待已经关闭的流。后续 CLI 可泛化补充按用户邮箱映射用户 ID、请求间空窗和 TTFT/末事件可见性;本次只读调查未取消请求,也未修改 runtime、YAML、proxy 或调度。
|
||||
@@ -0,0 +1,26 @@
|
||||
# R2.2.18 任务报告
|
||||
|
||||
## 结果
|
||||
|
||||
PK01 Sub2API 已通过 YAML-first 受控流程从 v0.1.153 升级到 v0.1.155。实际运行镜像为 `docker.1panel.live/weishaw/sub2api:0.1.155`,容器状态 healthy。
|
||||
|
||||
## Source of truth
|
||||
|
||||
- owning YAML:`config/platform-infra/sub2api.yaml`
|
||||
- 仅修改 PK01 target 的 `image.tag`:`0.1.153` → `0.1.155`
|
||||
- PR:https://github.com/pikasTech/unidesk/pull/2003
|
||||
- merge commit:`d97605d8f20d3c87ff16f747cd7e500d79f9827b`
|
||||
- 未修改其他 target、公开域名、Secret、Codex pool、账号 runtime 或调度策略。
|
||||
|
||||
## 受控部署证据
|
||||
|
||||
- `platform-infra sub2api plan --target PK01`:通过。
|
||||
- `platform-infra sub2api apply --target PK01 --dry-run`:通过,渲染目标镜像 v0.1.155。
|
||||
- `platform-infra sub2api image-prepull --target PK01 --confirm --wait`:成功;镜像 ID `sha256:ec8c1452f9dc5f4ca...`。
|
||||
- apply job:`platform_infra_sub2api_apply_pk01_20260714074034649_757de2`,状态 succeeded,exit 0。
|
||||
- `platform-infra sub2api status --target PK01`:app running/healthy,实际镜像 v0.1.155;Redis保持原容器运行。
|
||||
- `platform-infra sub2api validate --target PK01`:通过;app、Redis、外部 PostgreSQL、本地 health 与 PK01 Caddy 公网 health 均成功。
|
||||
|
||||
## 边界
|
||||
|
||||
本次是纯镜像升级,没有执行 `codex-pool plan|sync|validate`,没有对齐或覆盖手工账号、临时不可调度规则、proxy 绑定和其他 runtime 配置。
|
||||
@@ -0,0 +1,42 @@
|
||||
# R2.2.20 任务报告
|
||||
|
||||
## 目标
|
||||
|
||||
快速缓解客户错误 #17440(request ID `e5c08dde-9e60-4a14-9458-5574cec00b65`),只使用 YAML-first 通用临时不可调度模板和受控 runtime CRUD,不修改 Sub2API 源码、账号容量、优先级、代理或凭据。
|
||||
|
||||
## 根因证据
|
||||
|
||||
- 原生 trace 显示请求先由账号 27(`https://sub.yjxm1221.top plus 0.025`)返回 400,精确短语 `Input must be a list` 已命中既有 400 规则并触发第一次 failover。
|
||||
- 第二跳账号 31(`https://sub2.pokexiao.com plus 0.03`)等待约 60.6 秒后返回 504;响应 detail 仍为 `Input must be a list`。
|
||||
- 第二次 failover 已进入,但选号时客户 context 已取消,最终 `reason=failover-attempted-no-candidate`。因此客户可见 504 的直接原因是第二跳耗尽请求时间窗,而不是首跳 400 未切号。
|
||||
- 账号 31 在此前 30 分钟有 75 次尝试、8 次失败、失败率 10.67%,该窗口唯一客户错误就是本请求;2 小时窗口质量仍为 B,故不据单次事件调整长期容量或优先级。
|
||||
|
||||
## 配置调优
|
||||
|
||||
owning YAML:`config/platform-infra/sub2api-codex-pool.yaml`。
|
||||
|
||||
在通用模板 `codex-upstream-failover` 的精确 504 规则中加入关键词 `input must be a list`,冷却时间保持 1 分钟。该规则只匹配实际 504 与稳定短语,不泛化到其他 4xx/5xx,也不涉及 auth 错误。
|
||||
|
||||
通过显式 selector `19,27,28,29,30,31,32,33,34` 对 9 个非 `lyon9801` OpenAI 账号执行:
|
||||
|
||||
1. 批量 dry-run:selected=9、changed=9、failed=0。
|
||||
2. 批量 confirm:selected=9、succeeded=9、failed=0。
|
||||
3. 回读账号 31 和 27:504 规则均包含 `input must be a list`,模板匹配正常。
|
||||
4. 回读 `lyon9801`:临时不可调度仍为 disabled、0 条规则,例外未被改变。
|
||||
|
||||
## 下发后观察
|
||||
|
||||
下发后的近期 10 分钟原生 Ops 窗口:
|
||||
|
||||
- 请求 88;
|
||||
- 客户错误 0,错误率 0.00%;
|
||||
- failover 8 次,8 次均恢复成功;
|
||||
- select failed 0;
|
||||
- 临时不可调度命中 4 次;
|
||||
- 当前排队 0。
|
||||
|
||||
该窗口尚未出现新的“504 + Input must be a list”样本,因此只能证明配置已实际生效且未见副作用,不能把短窗口零错误宣称为长期因果验证。
|
||||
|
||||
## 边界与剩余风险
|
||||
|
||||
本次规则会在同类 504 首次发生后短时摘除对应账号,减少连续客户请求重复命中;它无法挽救已经在第二跳耗尽 context 的当前请求。若同类首次错误仍频繁穿透,下一步应从 Sub2API 源码能力解决 failover 时间预算/每跳超时,而不是继续扩大关键词或盲目降低全池并发。
|
||||
@@ -0,0 +1,59 @@
|
||||
# R2.2.22 任务报告
|
||||
|
||||
## 结论
|
||||
|
||||
错误 #17490(request ID `4eb9a84b-7346-47a8-bb66-fb7b44317279`)已确认由 Sub2API 内部渠道监控使用的 `monitor` API Key(ID 19)发起。账号 `https://sub2.pokexiao.com plus 0.03`(ID 31)长等待后返回 502;内部监控客户端已因响应头超时取消请求,Sub2API 却继续等待被去取消的上游请求。上游错误返回后虽然进入 failover,下一轮选号使用的原始 request context 已取消,无法开始第二跳,最终记录 502。
|
||||
|
||||
## 原生 trace
|
||||
|
||||
- 请求:`/v1/responses`,`gpt-5.6-sol`,同步。
|
||||
- 调用方:用户 1、API Key `monitor`(ID 19)。
|
||||
- 单跳总耗时约 54,339 ms。
|
||||
- 上游状态:502,原始 detail:`error code: 502`。
|
||||
- Sub2API 记录 `openai.upstream_failover_switching`,switch 1/10。
|
||||
- 同一毫秒选号失败:`context canceled`,排除账号数 1。
|
||||
- 最终 HTTP 502,`reason=failover-attempted-no-candidate`。
|
||||
|
||||
近 30 分钟另一条客户错误 #17491(request ID `426551fe-cdd4-4563-a3d0-89b97aa5ffa3`)完全同型,但来自不同用户/IP,不是内部 monitor:同一账号和原始 502 detail,流式,单跳约 36,157 ms,failover 后立即 `context canceled`。因此内部监控 timeout 能解释 #17490,但不能解释所有账号 31 的客户穿透。
|
||||
|
||||
## 内部监控 timeout 与源码链
|
||||
|
||||
Sub2API v0.1.155 官方源码中,渠道监控 timeout 是 MVP 硬编码值:
|
||||
|
||||
- `monitorResponseHeaderTimeout = 30s`:等待响应头;
|
||||
- `monitorRequestTimeout = 45s`:单次模型请求总超时;
|
||||
- runner 外层使用请求超时、PING 超时和缓冲构造总 context。
|
||||
|
||||
网关上游转发则调用 `detachUpstreamContext(ctx)`,其实现为 `context.WithoutCancel(ctx)`;failover 选号继续使用 `c.Request.Context()`。因此 #17490 的完整时间线是:
|
||||
|
||||
1. 内部渠道监控发起请求;
|
||||
2. 约 30 秒仍无响应头,监控 HTTP client 取消请求并记录 timeout;
|
||||
3. 网关上游请求忽略该取消,继续等待账号 31;
|
||||
4. 约 54.3 秒账号 31 返回 502;
|
||||
5. handler 触发 failover,但原始监控 request context 已取消;
|
||||
6. 选号立即 `context canceled`,没有第二跳。
|
||||
|
||||
内部监控的 30 秒响应头超时确实等不到本次切号,但根因不仅是 timeout 数值短,还包括“首轮上游去取消、后续选号绑定已取消 context”的生命周期不一致。
|
||||
|
||||
## 账号与策略
|
||||
|
||||
账号 31 调查窗口内 163 次尝试、149 次成功、14 次失败,失败率 8.59%,质量 D/69;14 次 failover 信号中 2 次客户穿透。现有模板未命中该账号的 502,临时不可调度计数为 0。
|
||||
|
||||
原因是规则匹配原始上游响应体。模板已有 `upstream service temporarily unavailable` 等关键词,但原始 body 只有 `error code: 502`;UI 包装消息不是规则匹配输入。
|
||||
|
||||
## 基础设施
|
||||
|
||||
账号 31 绑定 proxy 3:`platform-infra-sub2api-pk01-local-egress-proxy`,HTTP `127.0.0.1:10809`;owner `host-proxy-hysteria.service` active/running、0 重启。Sub2API app 0 restart、无 OOM,主机接口无 RX/TX error/drop。同一 proxy 的其他账号仍有成功请求。查询时快照不能排除历史抖动,但没有本地 proxy 整体故障证据。
|
||||
|
||||
## 调优候选
|
||||
|
||||
1. 内部监控:把响应头 timeout、总请求 timeout 和 cadence 从硬编码提升为原生可配置事实,并在 CLI 展示来源;timeout 必须与网关每跳 deadline/failover reserve 联合设计,不能只单独放大。
|
||||
2. 网关源码:统一上游与 failover 生命周期,记录 client deadline、hop deadline、remaining budget 和 cancel source。客户端断开后若无需继续工作,应取消首轮上游;若要内部完成 failover,则选号与下一跳必须使用有界内部 context,同时停止无意义的下游写回。
|
||||
3. 配置缓解:精确短语 `error code: 502` 可进入通用非 auth 502 临时不可调度模板,减少后续请求再次命中,但不能挽救首次请求。
|
||||
4. 不建议仅降低全池并发;窗口无排队,故障发生在单账号长等待。
|
||||
|
||||
本任务只读,未修改 runtime、YAML、账号、proxy、调度或 Sub2API 源码。
|
||||
|
||||
## 原生记录闭环
|
||||
|
||||
channel 3 记录 15589:checkedAt 2026-07-14T08:52:45Z,latency 30,011 ms,错误为 http2: timeout awaiting response headers;对应 #17490 从约 08:52:45Z 开始、网关在 08:53:39Z 结束,证明内部监控在 30 秒先取消,而去取消的首轮上游继续约 24 秒后才返回并尝试 failover。
|
||||
@@ -0,0 +1,47 @@
|
||||
# R2.2.24 任务报告
|
||||
|
||||
## 结论
|
||||
|
||||
错误 #17511(请求 `f7ff0efe-64d3-4dd7-8716-20bdfc3a2f90`)不是内部 monitor 请求,也不是账号池没有候选。它由用户 1 的 `main` API Key(ID 20)发起,来源 IP `152.53.229.148`,分组“自用”(ID 3),流式 `/responses`。
|
||||
|
||||
账号 `https://sub2.pokexiao.com plus 0.03`(ID 31)等待 63,238 ms 后返回上游 502;Sub2API 在 1 ms 内记录 `openai.upstream_failover_switching`,随后选号立即以 `context canceled` 失败,最终 63,239 ms 返回 502。因此准确表述是:切号已触发,但切号开始时请求 context 已取消,备用账号没有被选取和调用。
|
||||
|
||||
同一 Key、同一来源 IP 的请求 `0503e439-ee9b-4954-bc2d-3087ec3a1d5e` 在此前 6 分钟出现相同链路:账号 31 等待 75,965 ms 后返回 504,切号同毫秒触发,选号立即 `context canceled`。这证明不是单次调度抖动。
|
||||
|
||||
## 运行面证据
|
||||
|
||||
- “自用”分组在窗口内有 9 个账号,其中 7 个可用,当前 18/218 并发,队列 0。
|
||||
- #17511 选号失败时仅排除账号 31,不存在“所有备用账号均被排除”。
|
||||
- 账号 31 最近窗口 286 次尝试、277 成功、9 失败,失败率 3.15%,质量分 89/B;9 次 failover 中 2 次最终客户错误。
|
||||
- OpenAI 专用响应头超时当前为 disabled,来源为进程环境。
|
||||
- 成功流式请求 TTFT P99 约 61.6 秒、最大约 92.9 秒;因此不能未经验证直接把全局响应头超时压到 45–60 秒。
|
||||
|
||||
## v0.1.155 源码归因
|
||||
|
||||
`openai_gateway_handler.go` 的 failover 循环:
|
||||
|
||||
1. 选号使用 `c.Request.Context()`。
|
||||
2. 上游转发进入 `Forward(c.Request.Context(), ...)`。
|
||||
3. `Forward` 构建上游请求前调用 `detachUpstreamContext(ctx)`。
|
||||
4. `detachUpstreamContext` 返回 `context.WithoutCancel(ctx)`,官方测试 `TestDetachUpstreamContextIgnoresClientCancel` 明确验证客户端取消不会取消上游请求。
|
||||
5. 首轮上游最终返回可 failover 的 502/504 后,handler 回到循环,再次用原始且已取消的 `c.Request.Context()` 选号,于是立即失败。
|
||||
|
||||
这是“首轮上游去取消、后续选号仍绑定下游取消”的生命周期不一致。它允许客户端断开后首轮上游继续消耗时间,但不保留后续 failover 的执行窗口。
|
||||
|
||||
## 版本与配置判断
|
||||
|
||||
- v0.1.153、v0.1.152、v0.1.151、v0.1.150、v0.1.149 均存在相同 `context.WithoutCancel` 实现及调用,回退到这些版本不能修复该问题。
|
||||
- 公网 Caddy owning YAML 的 `responseHeaderTimeoutSeconds` 为 600 秒,明显长于本次 63 秒,不支持“Caddy 先截断”的假设。
|
||||
- 简单降低全局 `gateway.openai_response_header_timeout` 能让首轮更早失败并为切号留预算,但当前成功 TTFT 长尾已超过 60 秒,未经按请求类型/账号评估会误杀有效请求。
|
||||
- 精确匹配账号 31 的原始错误短语 `error code: 502` 并临时不可调度可减少后续请求再次命中,但不能挽救触发规则的首个请求,也不能修复 context 生命周期。
|
||||
|
||||
## 建议
|
||||
|
||||
1. 源码根治:统一上游请求与 failover 循环的生命周期。至少在进入下一轮前检测原始 context;若业务要求客户端断开后停止工作,则不要对上游使用 `WithoutCancel`。若业务要求内部继续完成 failover,则选号、转发和最终写回必须使用一个有界的内部 deadline,并在客户端已断开时停止无意义的继续写回。
|
||||
2. 增加 failover reserve:每次首字节等待不能耗尽整体预算,应根据请求类型(普通/compact)和剩余预算设置 per-hop deadline;日志需记录 client deadline、hop deadline、remaining budget 和 cancel source。
|
||||
3. 配置缓解前先用 CLI 按普通/compact、账号统计 TTFT 与错误等待时长,再选择高于正常长尾且低于客户端 deadline 的阈值;当前证据不足以安全给出单一秒数。
|
||||
4. 对账号 31 的稳定原始 502 短语使用通用非 auth 临时不可调度模板,只作为减少连续命中的缓解,不宣称根治。
|
||||
|
||||
## 官方 main 核对
|
||||
|
||||
2026-07-14 只读抓取官方 Wei-Shaw/sub2api main 后,detachUpstreamContext 仍返回 context.WithoutCancel(ctx),Responses failover 选号仍使用 c.Request.Context();官方最新主线尚未修复该生命周期不一致。
|
||||
@@ -0,0 +1,55 @@
|
||||
# R2.2.8 任务报告
|
||||
|
||||
## 结果
|
||||
|
||||
已增强 Sub2API 只读诊断 CLI,按分组和上游账号归因近期请求质量。默认输出保持 Kubernetes 风格表格,完整展示账号名称和账号 ID;显式 `--json` 提供紧凑语义摘要,整组概要通过 `--account` 和 request ID 渐进下钻。未修改 Sub2API runtime、YAML、账号调度或源码运行面。
|
||||
|
||||
## 数据口径
|
||||
|
||||
- 成功请求、流式请求和账号 TTFT 来自 Sub2API 原生 `/api/v1/admin/usage`。
|
||||
- 客户可见错误来自原生 `/api/v1/admin/ops/request-errors`。
|
||||
- 切号、同账号重试、临时不可调度和 `forward_failed` 来自原生 system-log 索引,并自动分页读取近期窗口。
|
||||
- 失败分子按 request ID 去重;账号 attempts 是上游账号尝试次数,不等于客户请求数,切号时一个客户请求可以计入多个账号。
|
||||
- 评分权重为可靠性 60、TTFT 25、当前可用性 15,置信度单独计算。TTFT 少于 5 个样本不按零时延计分。
|
||||
- 至少 10 次尝试且至少 5 个 TTFT 样本时,才把高分映射为 A-D 可比较等级;样本不足的偶然高分标记为 `insufficient`。至少 10 次尝试且评分低于 60 时保留 E,避免高失败率被证据不足掩盖。
|
||||
- 账号名称是人类可读运维信息,报告和 CLI 完整输出;API Key、token、credentials 继续脱敏。
|
||||
|
||||
## 近期只读快照
|
||||
|
||||
采样窗口为 2026-07-14 05:32-05:35 UTC 向前 60 分钟,滚动值会随请求变化。
|
||||
|
||||
- 所有分组共 530 个请求、13 个客户错误,客户错误率 2.45%,上游错误率 6.60%。
|
||||
- `unidesk-codex-pool`:约 219 个请求、7 个客户错误,客户错误率 3.20%,上游错误率 13.24%,队列为 0。
|
||||
- `https://api.iceiu.com plus 0.03`(ID 33):89.9/B/healthy,高置信;172 次尝试、3 次失败,失败率 1.74%,TTFT P95 42.87 秒;有 3 个 `forward_failed`,未形成客户错误。
|
||||
- `https://sub.yjxm1221.top plus 0.025`(ID 27):15/E/poor,中置信;39 次尝试、26 次失败,失败率 66.67%,TTFT P95 202.12 秒;23 个切号请求,18 个恢复、5 个失败,3 次临时不可调度、3 个 `forward_failed`、7 个客户错误。它是该分组近期客户可见错误的主因。
|
||||
- `https://ai.whistlelads.com plus 0.03`(ID 28):20/E/poor,低置信;11 次尝试全部失败;11 个切号请求全部由后续账号恢复,12 次临时不可调度,未形成客户错误。说明现有切号有效遮蔽了该账号故障,但源账号质量差。
|
||||
- `https://sub2.pokexiao.com plus 0.03`(ID 31):样本内 100 分,但只有 8 次尝试,等级为 `insufficient`,不能据此认定为优质账号。
|
||||
- `https://api.lwylink.xyz pro 0.1`(ID 29)和 `https://api.lwylink.xyz plus 0.05`(ID 30)当前不可用且无近期成功证据。
|
||||
- `自用`:约 326 个请求、5 个客户错误,客户错误率 1.53%,上游错误率 2.15%,队列为 0。
|
||||
- `https://api.iceiu.com plus 0.03`(ID 33):93.5/A/preferred,高置信;230 次尝试全部成功,TTFT P95 53.94 秒。
|
||||
- `https://sub2.pokexiao.com pro 0.05`(ID 32):77.9/C/watch,中置信;33 次尝试、1 次失败,切号后恢复,TTFT P95 98.49 秒。
|
||||
- `lyon9801`(ID 15):55.9/E/poor,中置信;34 次尝试、5 次失败,失败率 14.71%;5 次切号均未恢复,5 个客户错误,是该分组近期客户可见错误主因。
|
||||
- `grok` 分组近期无请求和错误。
|
||||
- 两个活跃分组均无排队,当前证据不支持把降低全局上游并发作为主修复方向。
|
||||
- 近期未观察到同账号重试事件;不能把“未观察到”解释为功能不存在。
|
||||
|
||||
## CLI 行为
|
||||
|
||||
- `runtime errors --all-groups`:所有分组概要和稳定分页。
|
||||
- `runtime errors --group <name-or-id>`:分组内完整账号名称、评分、置信度、错误、切号和 TTFT 表格。
|
||||
- `runtime errors --group <name-or-id> --account <name-or-id>`:单账号详细归因和 request index。
|
||||
- 整组 `--json` 保留所有账号名称和质量概要;请求级证据通过 `--account` 下钻,不依赖手工 `limit`。
|
||||
- 单账号 JSON 实测 9248 字节、整组 JSON 实测 9058 字节,均低于 10240 字节输出预算;未出现 `outputTruncated`,也未暴露内部 `_requestIds`。
|
||||
|
||||
## 结论与后续建议
|
||||
|
||||
主因不是池总并发或排队,而是少数上游账号的高失败率和长 TTFT:`https://sub.yjxm1221.top plus 0.025` 与 `lyon9801` 会把错误直接传给客户,`https://ai.whistlelads.com plus 0.03` 虽被切号遮蔽但持续消耗 failover。下一步若获准调优,应优先基于高置信/中置信评分调整账号优先级或临时不可调度,并继续保持 `lyon9801` 的既有例外约束;低置信账号先积累样本,不应自动调度。任何自动评分到 runtime 的闭环都应另行审批,本任务没有实施。
|
||||
|
||||
## 验证
|
||||
|
||||
- `bun --check scripts/src/platform-infra-sub2api-codex/runtime.ts`
|
||||
- `bun --check scripts/src/platform-infra-sub2api-codex/remote-python-sync.ts`
|
||||
- `bun --check scripts/src/output.ts`
|
||||
- `bun test scripts/src/output.test.ts`:2 pass,0 fail。
|
||||
- `git diff --check`
|
||||
- PK01 原生只读命令已验证 all-groups、group 2、group 3、account 27、默认表格和显式 JSON。
|
||||
@@ -0,0 +1,48 @@
|
||||
# R2.2.9 任务报告
|
||||
|
||||
## 范围
|
||||
|
||||
只读检查 PK01 Sub2API 最近 30 分钟的所有分组、账号质量、客户可见错误、切号结果、临时不可调度和代表请求链路。未修改 runtime、YAML、账号调度或 Sub2API 运行面。
|
||||
|
||||
## 全局快照
|
||||
|
||||
抓取时间为 2026-07-14 06:14-06:15 UTC,窗口向前 30 分钟,数据会随滚动窗口变化。
|
||||
|
||||
- 所有分组共 440 个请求,7 个客户可见错误,客户错误率 1.59%;13 个上游错误,上游错误率 2.95%。
|
||||
- `unidesk-codex-pool`:约 257-258 个请求,4 个客户错误,错误率约 1.56%,上游错误率约 3.89%。
|
||||
- `自用`:约 182-183 个请求,3 个客户错误,错误率约 1.64%,上游错误率约 1.64%。
|
||||
- `grok`:无请求、无错误。
|
||||
- 两个活跃分组查询时并发约 8-9/218,排队为 0;当前证据不支持把降低池总并发作为主修复。
|
||||
- 当前有 7/9 个账号可用。`https://api.lwylink.xyz pro 0.1`(ID 29)和 `https://api.lwylink.xyz plus 0.05`(ID 30)因余额不足 403 处于错误状态。
|
||||
|
||||
## unidesk-codex-pool
|
||||
|
||||
- `https://sub2.pokexiao.com plus 0.03`(ID 31):95.6/A/preferred,高置信;84 次尝试全部成功,TTFT P95 39.95 秒。
|
||||
- `https://api.iceiu.com plus 0.03`(ID 33):87.9/B/healthy,高置信;90 次尝试、2 次失败,失败率 2.22%,TTFT P95 46.90 秒;产生 2 个客户错误。
|
||||
- `https://sub.yjxm1221.top plus 0.025`(ID 27):66.2/D/degraded,高置信;55 次尝试、6 次失败,失败率 10.91%,TTFT P95 17.56 秒;产生 2 个客户错误、4 个 `forward_failed`。其 2 次切号均恢复。
|
||||
- `https://ai.whistlelads.com plus 0.03`(ID 28):7 次尝试全部失败,证据不足等级;7 次切号全部恢复、7 次临时不可调度,没有形成客户错误,说明内部 failover 有效遮蔽了该账号近期故障。
|
||||
- 该分组 4 个客户错误均为同步请求:2 个 400、2 个 404,模型为 `gpt-5.6-luna` 和 `gpt-5.6-sol`。
|
||||
- 分组共 8 次切号,8 次全部恢复;没有候选耗尽。
|
||||
- request `46bdd23b-e8bf-461d-bb3f-ad6313412ae3`:账号 `https://sub.yjxm1221.top plus 0.025`,最终 502;上游原文是 `400 Input must be a list`,总耗时约 977 秒。该错误更像请求结构或协议兼容错误,不应按临时上游过载盲目重试。
|
||||
- request `bac355f9-22fc-4d18-a899-8f609d847615`:账号 `https://sub.yjxm1221.top plus 0.025`,HTTP 200 后出现 `stream usage incomplete: missing terminal event`,trace 判为 degraded,说明成功状态不能代表客户收到完整流式响应。
|
||||
|
||||
## 自用
|
||||
|
||||
- `https://sub2.pokexiao.com plus 0.03`(ID 31):99.1/A/preferred,中置信;23 次尝试全部成功,TTFT P95 15.98 秒。
|
||||
- `https://sub.yjxm1221.top plus 0.025`(ID 27):98.5/A/preferred,高置信;55 次尝试全部成功,TTFT P95 19.94 秒。同一账号在不同分组的模型和请求构成不同,不能跨分组合并评分。
|
||||
- `https://api.iceiu.com plus 0.03`(ID 33):86.7/B/healthy,中置信;39 次尝试、1 次失败,产生 1 个 524 客户错误;1 次切号失败。
|
||||
- `lyon9801`(ID 15):81.6/B/healthy,中置信;49 次尝试、3 次失败,失败率 6.12%,TTFT P95 9.89 秒;产生 2 个 503 流式客户错误。虽然综合评分为 B,但它仍是该分组近期客户错误主因。
|
||||
- 分组共 3 次切号,全部失败并出现 3 次选号失败。
|
||||
- request `da415526-b124-4e23-a4a7-ea48154c851e`:账号 `lyon9801`,流式 `gpt-5.6-sol`;约 1556 秒后才收到 503 并触发切号,此时 context 已取消,选号在约 1 毫秒内失败,trace 判为 `failover-attempted-no-candidate`。这是 failover 时机过晚,不是池容量不足。
|
||||
- request `4bba7f9b-15e3-4891-b063-bd65fb6e3704`:账号 `lyon9801`,HTTP 200 后发生 `upstream response failed`,trace 判为 degraded,客户可能收到不完整流。
|
||||
|
||||
## 判断
|
||||
|
||||
- 相比上一轮 60 分钟快照,当前短窗口的客户错误率和上游错误率数值更低,但窗口不同且持续滚动,不能直接解释为配置调优效果。
|
||||
- 当前主要客户可见错误已经从纯上游过载,分化为三类:
|
||||
- 请求/协议兼容错误:`Input must be a list`、404;
|
||||
- 超长等待后才 failover,客户上下文已取消;
|
||||
- HTTP 200 已提交后缺少终止事件或后续流式失败。
|
||||
- 对第一类不应无差别重试;对第二类应继续调查 response-header/首事件等待和 failover 触发时机;对第三类需要把 degraded 流式结果纳入客户错误治理,不能仅看 HTTP 状态。
|
||||
- 最近 30 分钟未观察到同账号重试事件;不能把零事件解释为重试能力不存在。
|
||||
- 当前只完成分析,没有下发任何调优。
|
||||
@@ -41,6 +41,60 @@
|
||||
#### R2.2.7
|
||||
|
||||
按仓库 P0 拆分超过 3000 行的 `remote-python-sync.ts`:把大段内嵌 Python 按 trace、sync/validate 与 sentinel 职责迁入原生模块,单文件收敛到 2000 行以下,并保持受控 CLI 行为与只读诊断契约不变,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.7_Task_Report.md)。
|
||||
#### R2.2.8 [completed]
|
||||
|
||||
增强 Sub2API CLI 的上游账号粒度质量分析与自动评分:使用原生 Ops 归因账号请求量、客户/上游错误、触发切号、切号结果、同账号重试、临时不可调度和首字延迟覆盖率/分位数,输出可解释评分、置信度和扣分原因;只做分析,不自动修改账号 runtime 或调度配置,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.8_Task_Report.md)。
|
||||
#### R2.2.9 [completed]
|
||||
|
||||
重新抓取 PK01 Sub2API 最近 30 分钟所有分组的客户可见错误、上游错误、账号质量、切号恢复、临时不可调度和首字延迟,按完整账号名称报告主因;只做原生只读分析,不修改 runtime、YAML 或调度配置,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.9_Task_Report.md)。
|
||||
#### R2.2.10 [completed]
|
||||
|
||||
只读专项调查 lyon9801 的近期 503 流式错误:增强 Sub2API CLI 同时采集账号 proxy 绑定、容器环境代理摘要、DNS/路由、连接与出网基础设施证据,结合真实 request trace 和源码链路区分 OpenAI 上游、PK01 网络、代理、超时或 Sub2API 转发问题;不发探针、不修改 runtime、YAML、代理或调度配置,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.10_Task_Report.md)。
|
||||
#### R2.2.11 [completed]
|
||||
|
||||
只读复取并综合分析最近2小时所有 Sub2API 分组的客户可见错误、上游错误、账号质量、failover、候选耗尽和流式 degraded 情况,报告完整账号名称,不发探针且不修改 runtime、YAML、代理或调度配置,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.11_Task_Report.md)。
|
||||
#### R2.2.12 [completed]
|
||||
|
||||
快速缓解账号 https://sub.yjxm1221.top plus 0.025 的 Input must be a list 客户可见400:在 owning YAML 模板加入精确400短语并保留该 runtime-manual 账号现有规则,通过精准 runtime CRUD dry-run、确认下发和短窗口只读复取验证当前请求能够冷却并切号,不泛化匹配其他400,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.12_Task_Report.md)。
|
||||
#### R2.2.13 [completed]
|
||||
|
||||
向当前PK01全部非 lyon9801 OpenAI账号精准批量下发 codex-upstream-failover 统一临时不可调度模板:从runtime账号清单固定显式selector,排除lyon9801,先dry-run再confirm并逐账号对账,不修改分组、优先级、容量、proxy或凭据,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.13_Task_Report.md)。
|
||||
#### R2.2.14 [in_progress]
|
||||
|
||||
由 Artificer 实现 Sub2API runtime 精准批量 CRUD 的命令内自动回读对账:confirm 后批量回读全部selector并输出write result、actual、reconciled、mismatch和部分写入摘要,避免外部逐账号get;关联 [GitHub issue #1998](https://github.com/pikasTech/unidesk/issues/1998),完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.14_Task_Report.md)。
|
||||
#### R2.2.15 [completed]
|
||||
|
||||
继续只读分析统一非 auth 上游模板与超时预算的下一轮调优方案:交由独立 Artificer 调查 [Issue #1999](https://github.com/pikasTech/unidesk/issues/1999),基于下发后真实400切号成功、旧长请求context canceled、其他invalid input id与HTTP 200流式degraded证据,区分可安全泛化的精确模板、需要Sub2API源码能力和不应自动处理的客户参数错误,不继续盲目扩规则,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.15_Task_Report.md)。
|
||||
|
||||
#### R2.2.16 [completed]
|
||||
|
||||
紧急只读追踪用户 mail0795336304@163.com 反馈请求卡住15分钟:定位用户ID、近30分钟请求、当前长请求/并发、request ID、账号与模型,区分排队、上游首字、流式间隔、代理网络或客户连接,不取消请求且不修改runtime、YAML、proxy或调度,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.16_Task_Report.md)。
|
||||
#### R2.2.17 [in_progress]
|
||||
|
||||
复盘用户直接反馈的只读追踪过程并交由 Artificer 实现 [Issue #2000](https://github.com/pikasTech/unidesk/issues/2000):增加通用的一键用户追踪 CLI,以邮箱或用户ID关联请求空窗、用户/API Key并发、client/internal request ID、上游账号名称、proxy基础设施和证据化归因,采用Kubernetes列表风格与稳定ID渐进披露,减少手工API与重复trace调用,不改运行面,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.17_Task_Report.md)。
|
||||
#### R2.2.18 [completed]
|
||||
|
||||
将 PK01 的 Sub2API 通过 owning YAML 从 v0.1.153 受控升级到 v0.1.155:只修改 PK01 target镜像版本,依次执行状态核对、plan、apply dry-run、镜像预拉、受控apply、状态与公开健康验证,保持账号池runtime和Codex pool配置原样,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.18_Task_Report.md)。
|
||||
#### R2.2.19 [in_progress]
|
||||
|
||||
交由 Artificer 实现 [Issue #2004](https://github.com/pikasTech/unidesk/issues/2004):增强 Sub2API CLI 拉取 v0.1.155 原生智能诊断与渠道监控状态,泛化所有分组/渠道概要和指定对象下钻,以Kubernetes列表风格呈现TTFT、上游与客户错误率、原生建议和证据边界,以及7/15/30天渠道运行状态、平台/模型、对话延迟、端点PING、可用率、最近记录和刷新事实,使用稳定ID渐进披露,不重算第二套诊断且不修改运行面,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.19_Task_Report.md)。
|
||||
|
||||
#### R2.2.20 [completed]
|
||||
|
||||
快速缓解错误 #17440:基于 request e5c08dde-9e60-4a14-9458-5574cec00b65 的真实链路,将精确短语 Input must be a list 补入通用 504 临时不可调度规则,对全部非 lyon9801 账号精准批量 dry-run、确认下发和自动回读对账,不改容量、优先级、代理或 Sub2API 源码,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.20_Task_Report.md)。
|
||||
#### R2.2.21 [in_progress]
|
||||
|
||||
交由 Artificer 实现 [Issue #2013](https://github.com/pikasTech/unidesk/issues/2013):定义并实现 Sub2API P0/P1/P2 故障快速查询 CLI,P0 为上游故障穿透客户,P1 为首字延迟偏高且普通与 compact 分开统计并突出极端离群,P2 为上游错误率偏高;支持所有分组概要与按等级、分组、账号名称、模型、请求类型、稳定 request ID 下钻,默认 Kubernetes 列表风格和固定分页,不靠手工 limit,不修改运行面,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.21_Task_Report.md)。
|
||||
|
||||
#### R2.2.22 [completed]
|
||||
|
||||
主代理只读追踪客户错误 #17490(request ID 4eb9a84b-7346-47a8-bb66-fb7b44317279):使用 Sub2API 原生 Ops、trace、账号完整名称、failover/retry/临时不可调度和近期窗口证据,定位账号 https://sub2.pokexiao.com plus 0.03 的同步502为何穿透客户,区分候选耗尽、context取消、不可重试阶段或模板未命中;先调查,不盲目新增generic 502规则,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.22_Task_Report.md)。
|
||||
#### R2.2.23 [in_progress]
|
||||
|
||||
交由 Artificer 实现 [Issue #2016](https://github.com/pikasTech/unidesk/issues/2016):增强 CLI 对 Sub2API 内部账号/渠道监控的原生只读访问,展示探测 timeout、cadence、阶段耗时、账号选择、上游首字节、切号/重试和最终状态,支持所有监控对象概要与稳定 ID 下钻,默认 Kubernetes 列表风格且不靠手工 limit;不修改运行面、不发新探针,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.23_Task_Report.md)。
|
||||
#### R2.2.24 [completed]
|
||||
|
||||
只读调查错误 #17511(request ID `f7ff0efe-64d3-4dd7-8716-20bdfc3a2f90`)为何上游 502 后未成功切号,重建流式请求的账号选择、首字节、响应提交、failover、排除列表、context deadline 与最终客户错误链,并与内部 monitor 请求 #17490 对照,不修改运行面,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.24_Task_Report.md)。
|
||||
### R2.3
|
||||
|
||||
调查并收敛 Sub2API 原生 system-log 索引未保留策略事件的能力缺口,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.3_Task_Report.md)。
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { defaultCodexPoolRuntimeTargetId } from "./runtime-target";
|
||||
|
||||
export type RuntimeAction = "list" | "get" | "errors" | "infrastructure" | "apply" | "delete";
|
||||
|
||||
export interface RuntimeOptions {
|
||||
action: RuntimeAction;
|
||||
account: string | null;
|
||||
accounts: string[];
|
||||
group: string | null;
|
||||
allGroups: boolean;
|
||||
platform: string | null;
|
||||
pageToken: string | null;
|
||||
template: string | null;
|
||||
kind: "temp-unschedulable";
|
||||
confirm: boolean;
|
||||
full: boolean;
|
||||
raw: boolean;
|
||||
json: boolean;
|
||||
since: string;
|
||||
tail: number;
|
||||
targetId: string;
|
||||
}
|
||||
|
||||
export function parseRuntimeOptions(args: string[]): RuntimeOptions {
|
||||
const [actionRaw = "list", ...rest] = args;
|
||||
if (!(["list", "get", "errors", "infrastructure", "apply", "delete"] as string[]).includes(actionRaw)) {
|
||||
throw new Error("runtime usage: list|get|errors|infrastructure|apply|delete [--account <name-or-id>|--accounts <selectors>] [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <token>] [--since 24h] [--tail 50000] [--template <id>] [--kind temp-unschedulable] [--confirm] [--target <id>] [--json|--full|--raw]");
|
||||
}
|
||||
let account: string | null = null;
|
||||
let accounts: string[] = [];
|
||||
let group: string | null = null;
|
||||
let allGroups = false;
|
||||
let platform: string | null = null;
|
||||
let pageToken: string | null = null;
|
||||
let template: string | null = null;
|
||||
let kind = "temp-unschedulable" as const;
|
||||
let confirm = false;
|
||||
let full = false;
|
||||
let raw = false;
|
||||
let json = false;
|
||||
let since = "24h";
|
||||
let tail = 50_000;
|
||||
let targetId = defaultCodexPoolRuntimeTargetId();
|
||||
for (let index = 0; index < rest.length; index += 1) {
|
||||
const arg = rest[index]!;
|
||||
const readValue = (name: string): string => {
|
||||
const value = rest[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
index += 1;
|
||||
return value.trim();
|
||||
};
|
||||
if (arg === "--confirm") confirm = true;
|
||||
else if (arg === "--full") full = true;
|
||||
else if (arg === "--raw") raw = true;
|
||||
else if (arg === "--json") json = true;
|
||||
else if (arg === "-o" && readValue("-o") === "json") json = true;
|
||||
else if (arg === "--account") account = readValue("--account");
|
||||
else if (arg.startsWith("--account=")) account = arg.slice("--account=".length).trim();
|
||||
else if (arg === "--accounts") accounts = parseAccountSelectors(readValue("--accounts"));
|
||||
else if (arg.startsWith("--accounts=")) accounts = parseAccountSelectors(arg.slice("--accounts=".length));
|
||||
else if (arg === "--group") group = readValue("--group");
|
||||
else if (arg.startsWith("--group=")) group = arg.slice("--group=".length).trim();
|
||||
else if (arg === "--all-groups") allGroups = true;
|
||||
else if (arg === "--platform") platform = readValue("--platform");
|
||||
else if (arg.startsWith("--platform=")) platform = arg.slice("--platform=".length).trim();
|
||||
else if (arg === "--page-token") pageToken = readValue("--page-token");
|
||||
else if (arg.startsWith("--page-token=")) pageToken = arg.slice("--page-token=".length).trim();
|
||||
else if (arg === "--template") template = readValue("--template");
|
||||
else if (arg.startsWith("--template=")) template = arg.slice("--template=".length).trim();
|
||||
else if (arg === "--kind") kind = parseRuntimeKind(readValue("--kind"));
|
||||
else if (arg.startsWith("--kind=")) kind = parseRuntimeKind(arg.slice("--kind=".length));
|
||||
else if (arg === "--target") targetId = readValue("--target");
|
||||
else if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length).trim();
|
||||
else if (arg === "--since") since = readValue("--since");
|
||||
else if (arg.startsWith("--since=")) since = arg.slice("--since=".length).trim();
|
||||
else if (arg === "--tail") tail = parseRuntimeTail(readValue("--tail"));
|
||||
else if (arg.startsWith("--tail=")) tail = parseRuntimeTail(arg.slice("--tail=".length));
|
||||
else throw new Error(`unsupported runtime option: ${arg}`);
|
||||
}
|
||||
const action = actionRaw as RuntimeAction;
|
||||
if (account !== null && accounts.length > 0) throw new Error("use only one of --account or --accounts");
|
||||
if ((action === "get" || action === "infrastructure") && !account) throw new Error(`runtime ${action} requires --account <name-or-id>`);
|
||||
if ((action === "apply" || action === "delete") && account === null && accounts.length === 0) throw new Error(`runtime ${action} requires --account or --accounts`);
|
||||
if (accounts.length > 0 && action !== "apply" && action !== "delete") throw new Error(`runtime ${action} does not accept --accounts`);
|
||||
if (account !== null && (account.length > 256 || /[\r\n]/u.test(account))) throw new Error("--account has an unsupported format");
|
||||
if (group !== null && (group.length > 256 || /[\r\n]/u.test(group))) throw new Error("--group has an unsupported format");
|
||||
if (platform !== null && (!/^[A-Za-z0-9._-]+$/u.test(platform) || platform.length > 64)) throw new Error("--platform has an unsupported format");
|
||||
if (pageToken !== null && (!/^\d+$/u.test(pageToken) || pageToken.length > 20)) throw new Error("--page-token must be a numeric group cursor");
|
||||
if (group !== null && allGroups) throw new Error("use only one of --group or --all-groups");
|
||||
if ((allGroups || pageToken !== null) && action !== "errors") throw new Error(`runtime ${action} does not accept all-groups pagination options`);
|
||||
if ((group !== null || platform !== null) && action !== "errors" && action !== "infrastructure") throw new Error(`runtime ${action} does not accept group scope options`);
|
||||
if (pageToken !== null && !allGroups) throw new Error("--page-token requires --all-groups");
|
||||
if (account !== null && allGroups) throw new Error("--account cannot be combined with --all-groups; use --group first");
|
||||
if (action === "apply" && !template) throw new Error("runtime apply requires --template <id>");
|
||||
if (action !== "apply" && template !== null) throw new Error(`runtime ${action} does not accept --template`);
|
||||
if (action !== "errors" && action !== "infrastructure" && (since !== "24h" || tail !== 50_000)) throw new Error(`runtime ${action} does not accept --since or --tail`);
|
||||
if (!/^\d+[mhd]$/u.test(since)) throw new Error("--since must use <number>m, <number>h, or <number>d");
|
||||
if (["list", "get", "errors", "infrastructure"].includes(action) && confirm) throw new Error(`runtime ${action} does not accept --confirm`);
|
||||
if ([full, raw, json].filter(Boolean).length > 1) throw new Error("use only one of --json, --full, or --raw");
|
||||
return { action, account, accounts, group, allGroups, platform, pageToken, template, kind, confirm, full, raw, json, since, tail, targetId };
|
||||
}
|
||||
|
||||
function parseAccountSelectors(value: string): string[] {
|
||||
const selectors = value.split(",").map((item) => item.trim()).filter(Boolean);
|
||||
if (selectors.length === 0 || selectors.length > 100) throw new Error("--accounts requires 1 to 100 comma-separated selectors");
|
||||
if (selectors.some((item) => item.length > 256 || /[\r\n]/u.test(item))) throw new Error("--accounts has an unsupported selector");
|
||||
if (new Set(selectors).size !== selectors.length) throw new Error("--accounts contains duplicate selectors");
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function parseRuntimeKind(value: string): "temp-unschedulable" {
|
||||
if (value !== "temp-unschedulable") throw new Error("--kind must be temp-unschedulable");
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseRuntimeTail(value: string): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isInteger(parsed) || parsed < 100 || parsed > 200_000) throw new Error("--tail must be an integer from 100 to 200000");
|
||||
return parsed;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user