feat: restore parallel ops and diagnostics changes
This commit is contained in:
@@ -128,12 +128,13 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help
|
||||
- CI/CD validation 阶段只能验证部署对象的 `/health` 端点和必要 provenance;禁止在 CI/CD gate 中运行 web-probe、Playwright、远程浏览器截图、用户路径 E2E 或等价重型业务探针。业务/用户入口验证只能作为发布后的独立 post-deploy validation 证据,不得阻塞 CI/CD 一键交付。
|
||||
- 产品功能配置 schema 的 CI/CD 合同:
|
||||
- repo 内唯一约定路径固定为 `config/feature-config.schema.json`;
|
||||
- schema 固定使用 JSON Schema Draft 2020-12,并由 lockfile 锁定的完整结构化 validator 校验;禁止用正则、浅层字段遍历或关闭整体 strict 代替;
|
||||
- schema 固定使用 JSON Schema Draft 2020-12,并由 lockfile 锁定的完整结构化 validator 校验;禁止用正则、只检查顶层字段的浅层解析器或关闭整体 strict 代替;
|
||||
- schema 只描述产品功能配置,不得承载数据 authority、事件路径、投影实现或交付路径选择;
|
||||
- 每个顶层 `properties` 成员必须声明唯一非空 `x-unidesk-feature`;一个产品功能只允许一个规范配置 property,禁止别名、反向变量或重复开关;
|
||||
- schema 的每个顶层 property 是一个规范产品配置键,值可以是对象,不强制使用大写 env 命名;
|
||||
- 每个顶层 property 必须声明唯一非空 `x-unidesk-feature`;同一功能只允许一个规范配置 property,禁止在 schema、renderer、Deployment env 或应用解析器中保留别名、反向变量或重复开关;
|
||||
- candidate snapshot 必须由正常 source-artifact 阶段从 owning YAML 或已渲染 workload 显式生成;禁止读取 runner `process.env` 作为产品配置 authority;
|
||||
- HWLAB、AgentRun 与 Sentinel consumer 必须复用同一 shared helper,并在正常 source-artifact/GitOps publish 路径执行,不得只挂到某个 consumer 的专属后处理;
|
||||
- CI/CD 必须检查 schema 语法、嵌套配置值匹配和功能到配置 property 的一对一关系,并输出有界 typed warning;输入快照缺失使用独立 typed warning,不得与 schema 非法混淆;
|
||||
- CI/CD 必须检查 schema 语法、嵌套配置值匹配和功能到配置 property 的一对一关系,并输出有界 typed warning;输入快照缺失使用独立的 `feature-config-input-missing` warning,不得与 schema 非法混淆;
|
||||
- schema 缺失、非法、candidate 缺失、配置不匹配、重复 property、validator/依赖异常或 OTel 导出失败时,只允许记录 warning;embedded 命令必须以 `0` 退出,PipelineRun、artifact、GitOps promote、Argo reconcile、runtime rollout 和 `/health` closeout 必须继续;
|
||||
- schema 校验器只读,禁止写回配置、注入默认值、删除变量、关闭功能、切换架构路径或执行任何补救操作;运行时必须原样接收本次待发布配置;
|
||||
- schema warning 必须实际导出 CI/CD OTel span/event;endpoint、serviceName、sampling 和 exporter timeout 只读 `config/platform-infra/pipelines-as-code.yaml#observability` 与精确 repository params,零/多 repository 匹配在 renderer 生成期 fail-closed;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
---
|
||||
name: unidesk-sub2api
|
||||
description: UniDesk Sub2API 平台运维技能。用户提到 Sub2API、sub2api、platform-infra sub2api、Codex pool、统一 API key、Sub2API FRP 暴露、Sub2API 管理 UI、配置 master ~/.codex 走 Sub2API、添加/删除 Codex 上游账号、校验 Sub2API /v1/models 时使用。
|
||||
description: >-
|
||||
UniDesk Sub2API 平台运维技能。用户提到 Sub2API、platform-infra sub2api、Codex pool、统一 API key、
|
||||
runtime CRUD、精准批量账号操作、临时不可调度、上游错误率、客户可见错误、换号/failover、
|
||||
模型映射、可用模型探测、FRP 暴露、管理 UI、配置 master ~/.codex、上游账号增删或校验 /v1/models 时使用。
|
||||
---
|
||||
|
||||
# UniDesk Sub2API
|
||||
@@ -35,12 +38,14 @@ bun scripts/cli.ts platform-infra sub2api image-prepull --target PK01 --confirm
|
||||
## 何时读取 reference
|
||||
|
||||
- 部署、状态、target 边界、PK01 host-Docker、k3s target、egress proxy、镜像升级:读 [references/operations.md](references/operations.md)。
|
||||
- Codex pool、统一 key、trace、account temp-unschedulable、`codex-pool sync|validate`:读 [references/codex-pool.md](references/codex-pool.md)。
|
||||
- Codex pool、统一 key、runtime CRUD、精准批量、模型探测、trace、account temp-unschedulable、`codex-pool sync|validate`:读 [references/codex-pool.md](references/codex-pool.md)。
|
||||
- Sentinel、marker-only 判定、账号冻结/恢复、`sentinel-report|sentinel-probe|sentinel-image`:读 [references/sentinel.md](references/sentinel.md)。
|
||||
- 受保护手动账号代理、分组绑定、WebUI account test:读 [references/manual-accounts.md](references/manual-accounts.md)。
|
||||
- 添加或删除上游 profile/account:读 [references/upstreams.md](references/upstreams.md)。
|
||||
- FRP/Caddy、PK01 shared Caddy managed block、public URL 暴露:读 [references/public-exposure.md](references/public-exposure.md)。
|
||||
- master `~/.codex` 统一消费端配置:读 [references/local-codex-consumer.md](references/local-codex-consumer.md)。
|
||||
- closeout 验收和最小 smoke:读 [references/validation.md](references/validation.md)。
|
||||
- 排障总入口:先读 [references/troubleshooting.md](references/troubleshooting.md),再按失败层读取运行面、账号池或公网暴露专题。
|
||||
- 排障总入口:
|
||||
- 先读 [references/troubleshooting.md](references/troubleshooting.md),再按失败层读取运行面、账号池或公网暴露专题。
|
||||
- 客户可见错误溯源、错误率分析和配置调优必须继续读 [references/troubleshooting-accounts.md](references/troubleshooting-accounts.md)。
|
||||
- 禁止事项和越界判断:读 [references/guardrails.md](references/guardrails.md)。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
interface:
|
||||
display_name: "UniDesk Sub2API"
|
||||
short_description: "Operate UniDesk Sub2API and Codex pool"
|
||||
default_prompt: "Use $unidesk-sub2api to operate the UniDesk Sub2API Codex pool safely."
|
||||
short_description: "运维 Sub2API、runtime 配置和 Codex 账号池"
|
||||
default_prompt: "使用 $unidesk-sub2api 通过受控 CLI 运维 Sub2API,分析客户错误,并安全调整 Codex pool runtime 配置。"
|
||||
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
|
||||
@@ -4,11 +4,40 @@
|
||||
- PK01 host-Docker 的 `sync --confirm` 通过 Sub2API admin API 对齐 group、YAML-managed accounts、统一消费 API key、capacity/loadFactor、WebSocket v2 标记和内置 `temp_unschedulable` 规则;
|
||||
- 统一消费 key 写入 YAML 声明的 `targets[PK01].hostDocker.envPath`;
|
||||
- PK01 路径不创建 k8s Secret、不部署 sentinel 资源,也不触发 `sub2api apply`、Docker compose、Caddy reload 或容器重启;
|
||||
- `runtime list|get` 查询 pool group 内全部账号或指定账号的实际状态;
|
||||
- `runtime errors` 按账号汇总 Sub2API 日志中的真实 `account_upstream_error` 状态码,并用 `openai.forward_failed` 提供脱敏错误摘要;
|
||||
- `runtime apply|delete` 只修改显式 `--account` 指定账号的 `temp_unschedulable` 配置;
|
||||
- `runtime list|get` 查询 pool group 内全部账号或指定账号的实际状态;`get` 同时披露脱敏 `modelMapping` 和完整临时不可调度规则;
|
||||
- `runtime models --account` 通过 Sub2API 原生账号模型接口同时读取配置模型和上游实时模型,不把配置映射误当作真实上游能力;
|
||||
- `runtime errors` 优先使用 Sub2API 原生 admin/ops 能力:
|
||||
- `--all-groups` 按稳定 group ID 游标分页返回所有分组概要;
|
||||
- `--group <name-or-id>` 返回指定分组的账号根因、策略效果和 request 索引;
|
||||
- `--platform <name>` 可筛选平台;省略分组参数时继续分析 YAML 当前 Codex pool;
|
||||
- 默认输出使用 Kubernetes 风格紧凑表格;机器读取必须显式使用 `--json`、`--full` 或 `--raw`;
|
||||
- dashboard 请求量和客户错误率按分组精确统计;
|
||||
- 账号被多个分组共享时,账号级 upstream-errors 和缺少 `group_id` 的 runtime 事件属于共享证据,禁止跨分组相加;
|
||||
- dashboard overview 提供请求分母和错误率;
|
||||
- account availability 提供可用、限流、过载和错误状态;
|
||||
- concurrency 提供容量与排队;
|
||||
- upstream errors 提供账号根因;
|
||||
- 原生 system-log 索引提供事件时直接使用;索引未保留临时不可调度、failover 或 `forward_failed` 时,才从 target runtime 日志补这些事件,并显式披露 fallback 原因和数量;
|
||||
- `policyEffectiveness` 从同一日志窗口汇总临时不可调度、切号、无候选和网关最终响应,并且只通过 `request_id` 关联切号结果;
|
||||
- `requestDrilldown` 给出切号后成功、失败、结果缺失和选号失败的有限 request id,可继续交给 `trace` 下钻;
|
||||
- 只有 `account_temp_unschedulable` 事件才算规则命中,不能仅凭错误状态码推断模板生效;
|
||||
- `requestIdCoverage` 披露各类事件缺少 request id 的数量;缺失时不能把零关联解释为没有发生摘号后切号;
|
||||
- `forwardFailureWithHttpSuccessRequestCount` 单列 HTTP 200 与 `forward_failed` 并存的请求,避免把流式响应已提交后的错误误判为完整成功;
|
||||
- 网关最终错误占比只使用窗口内目标 API 路径的 `http request completed` 日志事件,必须同时披露窗口与 tail 边界,不能解释为完整业务错误率;
|
||||
- `--full` 额外输出模型、小时、流式分布和有限详情样本;
|
||||
- 当前并发是查询时快照,不代表错误发生瞬间并发;没有历史并发证据时,命令必须明确输出不可用,不能据此建议降低并发;
|
||||
- 错误列表没有账号全部路由尝试数时,命令只报告错误数量,不伪造错误率;
|
||||
- Ops 记录与 runtime 日志是独立证据,命令明确披露采样数量,不按日志位置强行关联;
|
||||
- `runtime apply|delete` 支持 `temp-unschedulable` 和 `model-mapping` 两类 runtime 配置;
|
||||
- 单账号必须显式使用 `--account`,精准批量必须显式使用 `--accounts <id-or-exact-name,...>`,不提供隐式 `all`;
|
||||
- 批量操作先校验全部 selector 存在且账号类型可修改,再顺序调用 Sub2API admin API:
|
||||
- 选择错误不会产生部分写入;
|
||||
- 上游 admin API 在批次中途失败时仍需逐账号 `get` 对账;
|
||||
- 写操作不带 `--confirm` 时只返回 dry-run,带 `--confirm` 才通过 Sub2API admin API 写入;
|
||||
- runtime 命令不输出 API key 或完整 credentials,默认返回紧凑 JSON,使用 `get`、`--full` 或 `--raw` 下钻;
|
||||
- 批量默认输出账号 ID、名称、change、mode、mutation 和汇总计数;runtime 命令不输出 API key 或完整 credentials,使用 `get`、`--full` 或 `--raw` 下钻;
|
||||
- 文档中的 runtime action 或参数被当前分支 CLI 拒绝时,按 CLI 集成漂移处理:
|
||||
- 不退回手写 Sub2API 管理 API。
|
||||
- 先确认当前工作分支包含对应受控实现,或完成语义合入后再操作运行面。
|
||||
- `sentinel-report`、`sentinel-probe`、`sentinel-image` 和部分 `trace` 能力仍以 k3s target 为主,需要这些能力时显式选择对应 k3s target。
|
||||
|
||||
k3s 账号池操作示例:
|
||||
@@ -19,10 +48,17 @@ bun scripts/cli.ts platform-infra sub2api codex-pool sync --target D601 --confir
|
||||
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 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>
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --account <name-or-id> --template <template-id>
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --account <name-or-id> --template <template-id> --confirm
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --template <template-id>
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --template <template-id> --confirm
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --target PK01 --account <name-or-id> --kind temp-unschedulable
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --account <name-or-id> --kind model-mapping --model <client-model> --upstream-model <upstream-model>
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --target PK01 --account <name-or-id> --kind model-mapping --model <client-model>
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status --target D601
|
||||
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --target D601 --confirm
|
||||
@@ -41,7 +77,14 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60
|
||||
- `pool.defaultTempUnschedulableTemplate`: YAML-managed 账号默认引用的临时不可调度模板。`sync --confirm` 只把模板渲染到 YAML-managed 账号。
|
||||
- `profiles.entries[].tempUnschedulableTemplate`: 可选账号级模板引用,只用于明确偏离默认模板。
|
||||
- YAML 未声明的账号标记为 `runtime-manual`;它可以显示 `matchingTemplate`,但模板差异不是 drift,也不能由 `sync` 自动改写。
|
||||
- 手工账号需要采用模板时,必须通过 `runtime apply --account ... --template ... --confirm` 显式写入;需要删除该类配置时使用 `runtime delete --account ... --kind temp-unschedulable --confirm`。
|
||||
- YAML 只保存可复用规则模板和 managed account 的模板引用。未在 YAML 预配置的手工账号 runtime 配置必须使用 CRUD,不得为了批量下发而把账号纳入 YAML managed profile。
|
||||
- 手工账号需要采用模板时:
|
||||
- 写入使用显式 `runtime apply --account ...` 或 `--accounts ... --template ... --confirm`。
|
||||
- 删除使用相同显式 selector 的 `runtime delete --kind temp-unschedulable --confirm`。
|
||||
- 模型映射先用 `runtime models --account ...` 获取原生配置与实时上游能力:
|
||||
- 只有实时模型列表或真实模型请求证明支持时才确认写入。
|
||||
- 禁止把名称相近的模型猜测为别名。
|
||||
- 禁止用错误映射掩盖 `model_not_found` 或无候选。
|
||||
- runtime 模板与外部 `sentinel.*` 分开配置、互不驱动。内置规则负责请求路径即时冷却和 failover;哨兵负责 marker health、账号级隔离、恢复和 probe 退避。
|
||||
- 外部 sentinel 的写入面只允许通过 Sub2API admin `schedulable` 接口冻结/恢复账号;不能写入、清理或间接清理 `temp_unschedulable_until` / `temp_unschedulable_reason`、rate-limit、overload、model-rate-limit 等 Sub2API 请求路径 runtime 状态,也不能调用 `recover-state` 作为恢复动作。看到 UI 里的“触发时间/解除时间/规则序号/匹配关键词”临时不可调度状态时,默认先归因到 Sub2API 内置 request-path temp-unschedulable,而不是 sentinel。
|
||||
- YAML 只选择和配置 Codex 上游,不声明 `schedulable` 长期字段;`codex-pool sync --confirm` 不负责把既有账号恢复为 `schedulable=true`。既有账号的 `schedulable=false` 必须由哨兵先同步 Sub2API runtime 状态,再在 marker probe 命中后恢复。
|
||||
@@ -60,7 +103,7 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60
|
||||
|
||||
`sync --confirm` 会登录 Sub2API admin、创建/更新 group、创建/更新 YAML 中的 `unidesk-codex-*` accounts,并创建/复用统一 API key。k3s target 还会写入统一 API key Secret 并部署/更新哨兵资源;PK01 host-Docker target 只写 Sub2API runtime 和 host-Docker env 文件。host-Docker `validate` 也先使用 env 中的 Sub2API admin 凭据登录管理 API;如果 env 的统一 `API_KEY` 缺失或不匹配,它只通过管理 API 恢复或创建名为 YAML `pool.apiKeyName` 的统一消费 key 并写回 host-Docker env,不读取本地 profile 文件、不创建/更新账号、不 prune、不改变调度策略。`sync` 不把既有 managed account 直接恢复为 `schedulable=true`。恢复只由哨兵在读取 Sub2API runtime `schedulable=false` 后触发 recovery probe,并在 marker 命中时执行。`sync` 默认不删除 YAML 中缺席的 managed account。只有明确退役上游时才使用 `sync --confirm --prune-removed` 删除缺席且 `extra.unidesk_managed=true` 的 `unidesk-codex-*` account。对 `manualAccounts.protected`,`sync` 只执行 YAML 显式允许的窄同步;当前允许项是从目标 `egressProxy` 创建/更新 Sub2API internal proxy 记录并绑定 `proxy_id`,以及把受保护手动账号加入当前 `pool.groupName`。它仍不接管该账号凭据、status、schedulable、priority/capacity/loadFactor 或哨兵状态。若受保护手动账号在运行面缺失,`sync`/`validate` 会报告 manual account drift;不要自动创建、删除、接管或从 YAML 移除该账号。
|
||||
|
||||
`trace --request-id <requestId>` 是只读 request 追溯报表,不触发 probe、不修改账号。默认输出请求开始/最终状态、failover、`account_select_failed`、窗口内 `account_temp_unschedulable`、admin schedulable 写入计数和当前账号快照;`reason=failover-attempted-no-candidate` 表示 Sub2API 已进入自动切号,但排除当前失败账号后没有可用候选。需要机器处理时使用 `--raw`,需要原始匹配行时加 `--show-lines`。
|
||||
`trace --request-id <requestId>` 是只读 request 追溯报表,不触发 probe、不修改账号。命令优先使用原生 `/admin/ops/requests` 获取请求结果,并使用 `/admin/ops/system-logs` 获取 request-id 索引事件;target runtime 日志只补原生索引未保留的邻近事件。报表显式输出 target、runtime mode、主数据源、上下文数据源和读取状态。默认输出请求开始/最终状态、failover、`account_select_failed`、`forward_failed`、窗口内 `account_temp_unschedulable`、admin schedulable 写入计数和当前账号快照。`forward_failed` 与 HTTP 200 并存且没有 failover 时,结果标记为 `degraded`,不能解释为完整流式成功;`reason=failover-attempted-no-candidate` 表示 Sub2API 已进入自动切号,但排除当前失败账号后没有可用候选。需要机器处理时使用 `--raw`,需要原始匹配行时加 `--show-lines`。
|
||||
|
||||
对已支持的 k3s target,`sync --confirm`、`validate` 和 sentinel 操作可能超过单次 SSH/runtime 短连接窗口;远端 job/poll 边界见 [sentinel.md](sentinel.md)。
|
||||
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
## 账号池排障
|
||||
|
||||
### 客户可见错误与配置调优
|
||||
|
||||
1. 先用 `runtime errors --since <window>` 获取全账号同窗口证据:
|
||||
- 以 Sub2API 原生 dashboard overview 的请求分母、客户错误率和上游错误率为主。
|
||||
- 以 account availability、concurrency 和 upstream errors 解释账号可用性、排队、状态码与根因。
|
||||
- 原生 system-log 索引缺少策略事件时,才使用 target runtime 日志补临时不可调度、failover、`account_select_failed` 和 `forward_failed`,并保留 fallback 披露。
|
||||
2. 从客户可见错误中选有限 request id 运行 `trace`,按请求链区分:
|
||||
- 上游失败后成功切号。
|
||||
- 切号后候选耗尽。
|
||||
- 首次选号即无候选。
|
||||
- HTTP 200 已提交流式响应后出现 `forward_failed` 的 degraded 请求。
|
||||
3. 规则证据必须同时包含真实 HTTP 状态码与响应体关键词:
|
||||
- 只有状态码、通用包装文案或推断出的原因不能扩展关键词。
|
||||
- 只有 `account_temp_unschedulable` 才证明规则命中;不能用相同状态码再次出现来推断规则失效。
|
||||
- 优先使用精确短语,避免把 `context window`、认证失败、模型不支持等不可互换错误混入通用不可用规则。
|
||||
4. 无候选或模型路由错误先运行 `runtime models`:
|
||||
- 配置模型来自 `model_mapping`,实时模型来自上游原生同步,两者必须分开报告。
|
||||
- 没有账号真实支持目标模型时,保持池不声明该模型并快速失败,不映射到名称相近的其他模型。
|
||||
- 上游模型同步失败只能标记证据不足,不能据此补映射。
|
||||
5. 调优按所有权下发:
|
||||
- 通用模板先修改 owning YAML,并保持冷却保守;默认从一分钟开始,没有新证据和明确授权不得超过三分钟。
|
||||
- YAML-managed 账号走 `sync`;runtime-manual 账号走显式 `runtime apply|delete`。
|
||||
- 多账号使用 `--accounts` 精准集合;例外账号从 selector 中显式排除,不使用隐式全量操作。
|
||||
- 先 dry-run 核对 change、before/after 和账号集合,再加 `--confirm`,最后逐账号 `get` 或批量 noop dry-run 对账。
|
||||
6. 不把当前并发快照当作错误时并发:
|
||||
- 只有错误原文明确指向 concurrency,或存在错误时历史并发证据,才把降低上游并发作为有界对照实验。
|
||||
- 缺少直接证据时先处理主导上游根因。
|
||||
7. 下发后立即拉短窗口只做回归检查:
|
||||
- 效果判定必须等待新的完整窗口。
|
||||
- 同时比较请求分母、客户错误率、上游错误率、规则命中、切号成功、候选耗尽和 degraded 流式请求。
|
||||
8. 版本回退必须有源码差异或版本对照证据证明当前版本引入回归:
|
||||
- 旧版具有同一选择或错误传播逻辑时,不用回退替代配置治理。
|
||||
- 新版包含相关 failover 修复时,优先保留新版并治理真实根因。
|
||||
|
||||
- Codex pool 哨兵、账号冻结/恢复、marker-only 判断或 probe 周期看不清:第一步跑 `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report`。这个报表是主观察面;只有报表缺字段或需要底层证据时,才继续看 `--raw`、CronJob log、state ConfigMap 或 Sub2API 管理 UI。若看到“临时不可调度状态”且包含规则序号/匹配关键词,检查 Sub2API `account_temp_unschedulable` 日志和账号 `temp_unschedulable_*` 字段;sentinel 只解释 `schedulable=false` 的 active quarantine,不解释这类内置临时冷却。
|
||||
- 只加强监控、不让哨兵自动冻结账号时,把 YAML `sentinel.actions.enabled=false` 后 `codex-pool sync --confirm`。此时 marker probe 和 gateway failure monitor 仍记录 `would-freeze` / observe-only 证据,但不会通过 Sub2API admin 写 `schedulable=false`;`/responses/compact` 的 `codex.remote_compact.failed` 和 compact 上游 5xx failover 只作为 `gateway-compact-*` 观察事件记录,不作为哨兵自动切换触发器。
|
||||
- 单个 request id 报 502/503/中断/没有自动切号:第一步跑 `bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId>`。先看 `outcome`、`reason`、`FAILOVER`、`SELECT-FAILED`、`ACCOUNT SIGNALS` 和 `WINDOW STATS`;只有 trace 报表缺字段或需要审计原始日志时,才加 `--show-lines` 或 `--raw`。若 `reason=failover-attempted-no-candidate`,说明切号动作已发生,但 scheduler 在排除失败账号后没有可用候选;继续用 `sentinel-report` 和 `validate --full` 区分 sentinel quarantine、request-path temp-unschedulable、账号 status 或容量耗尽。
|
||||
|
||||
@@ -12,22 +12,30 @@ runtime:
|
||||
templates:
|
||||
- id: codex-upstream-failover
|
||||
kind: temp-unschedulable
|
||||
description: Reusable Sub2API request-path cooling and failover policy for Codex-compatible API-key accounts.
|
||||
description: Codex 兼容 API-key 账号的请求路径短时冷却与换号策略。
|
||||
spec:
|
||||
enabled: true
|
||||
rules:
|
||||
- statusCode: 429
|
||||
keywords: [concurrency limit exceeded]
|
||||
durationMinutes: 1
|
||||
description: 明确命中上游并发限制时短时冷却当前账号。
|
||||
- statusCode: 500
|
||||
keywords: [failed to validate api key, do request failed]
|
||||
durationMinutes: 1
|
||||
description: 上游 API-key 查询依赖或请求传输内部失败时短时冷却当前账号。
|
||||
- statusCode: 502
|
||||
keywords: [upstream service temporarily unavailable, upstream request failed, context window, stream usage incomplete, missing terminal event]
|
||||
keywords: [upstream service temporarily unavailable, upstream request failed, service temporarily unavailable, overloaded, concurrency limit exceeded]
|
||||
durationMinutes: 1
|
||||
description: Observed 502 upstream availability, context-window, and incomplete-stream failures briefly cool down the selected account.
|
||||
description: 明确命中 502 上游不可用、过载或并发限制时短时冷却当前账号。
|
||||
- statusCode: 503
|
||||
keywords: [upstream service temporarily unavailable, upstream request failed, temporarily unavailable]
|
||||
keywords: [upstream service temporarily unavailable, upstream request failed, service temporarily unavailable, temporarily unavailable, overloaded, concurrency limit exceeded]
|
||||
durationMinutes: 1
|
||||
description: Observed 503 temporary upstream availability failures briefly cool down the selected account.
|
||||
description: 明确命中 503 上游暂时不可用、过载或并发限制时短时冷却当前账号。
|
||||
- statusCode: 524
|
||||
keywords: [upstream request failed, missing_required_parameter, encrypted_content]
|
||||
keywords: [upstream request failed, service temporarily unavailable, overloaded, concurrency limit exceeded]
|
||||
durationMinutes: 1
|
||||
description: Observed 524 upstream request and encrypted-content failures briefly cool down the selected account.
|
||||
description: 明确命中 524 上游不可用、过载或并发限制时短时冷却当前账号。
|
||||
|
||||
pool:
|
||||
groupName: unidesk-codex-pool
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# R4.4.2 任务报告
|
||||
|
||||
## 当前状态
|
||||
|
||||
任务仍在进行中。UniDesk #1959 / PR #1961 已解决 runtime GitOps 大段脚本内联导致的 ARG_MAX 问题,后续 PR 已修复 ConfigMap ownership、YAML 解析和 terminal marker 组合;这些修复保留。
|
||||
|
||||
## 最新架构纠偏
|
||||
|
||||
此前把 direct/live/refresh/projector/outbox/realtime 六个架构布尔值同时投影到 YAML、renderer、Deployment env、应用 parser 和 CI verify,已经造成合法自动链被错误阻塞,并使缺少单个 env 的新 cloud-api Pod CrashLoop。该模型已废弃:
|
||||
|
||||
- 数据 authority 与事件路径固定为 agentrun.event.v1 → transactional projector → PostgreSQL facts/outbox → hwlab.event.v1 → realtime/replay SSE。
|
||||
- 架构不再提供 env/YAML 开关,旧 HWLAB #2536 已关闭。
|
||||
- 产品功能使用仓库内 config/feature-config.schema.json,一个功能只对应一个规范变量。
|
||||
- schema 缺失、非法、不匹配或重复只产生 typed warning;不得写回配置、关闭功能、切换路径或阻塞滚动上线。
|
||||
|
||||
## 当前执行
|
||||
|
||||
- UniDesk #1981 / MDTODO R6:删除 CI/CD 六 env 硬门禁并实现只读 warning schema 校验;Artificer task qt_7d73811bd7194ea186c904645da8be6c。
|
||||
- HWLAB #2538 / MDTODO R4.4.2.10:删除运行时架构 env 和旧分支,固定纯 Kafka transactional 投影;Artificer task qt_37898861dcea44219115c91075af340f。
|
||||
|
||||
任务将在 PR 审核、正常 merge 自动交付、NC01/v03 新 Pod Ready、Argo Synced/Healthy、Kafka 健康指标归零及新 hi 实时/回放/canonical duration 验收后收口。
|
||||
@@ -0,0 +1,22 @@
|
||||
# R6 任务报告
|
||||
|
||||
## 结论
|
||||
|
||||
- 产品功能 schema 固定为 `config/feature-config.schema.json`,使用 JSON Schema Draft 2020-12 与锁定的 `ajv-dist@8.17.1` 结构化 validator。
|
||||
- 每个顶层 property 通过唯一 `x-unidesk-feature` 建立一对一配置关系;CI runner env 不作为产品配置 authority。
|
||||
- schema、输入、重复、值不匹配、validator 与 OTel 异常均只产生 typed warning,不写回配置、不关闭功能、不切换架构,也不阻断 artifact、GitOps、Argo、rollout 与 health。
|
||||
- PR #1987 完成 schema/validator/OTel/`status|history|debug-step` 投影和旧 capability env 门禁删除;PR #1989 修复 source artifact 缺少 Ajv bundle 的首断点。
|
||||
|
||||
## 验证
|
||||
|
||||
- 定向测试首轮 53/53;主代理 shell 状态纠偏后回归 49/49。
|
||||
- merge commit `92b19ce6c8b55b1059506d6b4527388d1f671828` 自动创建 `unidesk-host-v2xn8`,44 秒 Succeeded。
|
||||
- GitOps `fa4d36a288ac2943091ffb92e0a4d5a0e93e5ff5`;Argo `Synced/Healthy` 且 revision relation=exact。
|
||||
- registry/runtime digest 同为 `sha256:a10a22b94976eb65a664b0b3b0c838a5720d39c037229b325c6fe0083578f355`,1/1 ready,health=200。
|
||||
- 全程未人工创建 PipelineRun、mirror sync、Argo refresh 或 patch 运行面。
|
||||
|
||||
## 工件
|
||||
|
||||
- Issue: https://github.com/pikasTech/unidesk/issues/1981
|
||||
- PR: https://github.com/pikasTech/unidesk/pull/1987
|
||||
- Follow-up PR: https://github.com/pikasTech/unidesk/pull/1989
|
||||
@@ -0,0 +1,37 @@
|
||||
# R1 任务报告
|
||||
|
||||
## 目标
|
||||
|
||||
把 Sub2API 上游故障调查从零散日志读取收敛为可复用的受控 CLI 能力,并用实际错误证据形成保守、可逆的账号级 runtime 调度缓解。
|
||||
|
||||
## 工具改进
|
||||
|
||||
- `runtime list|get` 补充 `proxyId`、并发上限、当前并发、`loadFactor` 和优先级,能够直接核对代理与调度事实。
|
||||
- `runtime errors` 综合 `account_upstream_error`、`openai.forward_failed` 和 Sub2API Ops 上游错误记录,输出全池主因排名、账号主因、状态码、消息、模型、小时和流式分布;详细样本通过 `--full` 渐进披露。
|
||||
- 错误分析显式区分 Ops 样本与 runtime 日志,不按时间或位置强行关联;缺少全部请求尝试分母时不计算错误率,缺少错误发生时并发历史时不把当前并发快照作为降并发依据。
|
||||
- `runtime apply|delete` 仅修改显式指定账号的临时不可调度配置;无 `--confirm` 时为 dry-run,有 `--confirm` 时才写入,并回读模板匹配、代理、并发、状态和调度状态。
|
||||
|
||||
## 全池结论
|
||||
|
||||
- 最近观测窗口内主要错误是仅状态码上游失败、通用上游失败、过载和上游 API-key 校验失败;明确并发错误只占较小部分,降低全池并发量不应作为首要修复。
|
||||
- 账号 27 的集中 HTTP 500 证据为 `Failed to validate API key`,指向上游网关 key 查询或依赖失败;账号 32 才有明确并发限制证据。
|
||||
- context window、缺少 terminal event 和 `encrypted_content` 属于请求语义或流式续链问题,不应通过临时摘号模板泛化掩盖。
|
||||
- Sub2API 仅能在响应尚未提交时通过临时不可调度触发换号;SSE 已向客户端写出后再缺少终止事件,不能透明切换账号。
|
||||
|
||||
## 配置与运行面应用
|
||||
|
||||
- owning YAML 的 `codex-upstream-failover` 模板收敛为 5 条实际观测规则:`429`、`500`、`502`、`503`、`524`,每条冷却 1 分钟。
|
||||
- `500` 仅匹配实际证据中的 `failed to validate api key` 和 `do request failed`;可用性、过载和并发关键词按实际状态码分别配置。
|
||||
- 通过 runtime CRUD 将模板显式应用到 9 个 API-key 手工账号;OAuth 账号 `lyon9801` 保持 `enabled=false`、零规则。
|
||||
- 未改变账号凭据、代理、并发上限、优先级、长期 `status` 或 `schedulable`;原先已禁用或错误的账号保持原状。
|
||||
|
||||
## 验证
|
||||
|
||||
- 全池 runtime 回读确认 9 个 API-key 账号均为 5 条规则并匹配模板,OAuth 例外未变化。
|
||||
- `bun --check scripts/src/platform-infra-sub2api-codex/runtime.ts` 通过。
|
||||
- Bun 内置 YAML 解析确认规则状态码为 `429,500,502,503,524`,持续时间均为 1 分钟。
|
||||
- `git diff --check` 通过。
|
||||
|
||||
## 后续边界
|
||||
|
||||
本次只能证明配置正确应用,不能立即证明错误率下降。后续应在独立的新时间窗口复核错误记录、切号证据和最终客户响应,并继续保持错误数量、请求分母和流式提交阶段的语义区分。
|
||||
@@ -0,0 +1,55 @@
|
||||
# R2.1 任务报告
|
||||
|
||||
## 目标
|
||||
|
||||
增强 UniDesk Sub2API CLI,优先使用 Sub2API 原生 admin/ops 运维能力诊断全池错误、账号状态、并发和单请求链路,运行日志只补原生索引未提供的策略事件。
|
||||
|
||||
## 官方能力依据
|
||||
|
||||
对照 `Wei-Shaw/sub2api` 官方仓库提交 `5d1c577cb2c735ca1f1d57533dff1302f6998d91`,确认可复用接口:
|
||||
|
||||
- `/api/v1/admin/ops/dashboard/overview`:请求总数、客户可见错误、错误率、上游错误率、健康分。
|
||||
- `/api/v1/admin/ops/upstream-errors`:账号级上游错误、状态码、request id 和详情。
|
||||
- `/api/v1/admin/ops/account-availability`:账号可用、限流、过载和错误状态。
|
||||
- `/api/v1/admin/ops/concurrency`:平台、组和账号并发占用、容量与排队。
|
||||
- `/api/v1/admin/ops/requests`:成功与错误请求的统一 request-id 视图。
|
||||
- `/api/v1/admin/ops/system-logs` 及 `/health`:索引日志和 sink 健康状态。
|
||||
|
||||
## CLI 改进
|
||||
|
||||
- `runtime errors` 增加 `nativeOps`,使用 overview 的完整请求分母计算客户可见错误率,避免把错误记录数当错误率。
|
||||
- 增加原生账号可用性、组并发、排队和 system-log sink 丢弃/写入失败状态。
|
||||
- Ops 上游错误改为所有选中账号主动查询,不再要求容器日志先出现对应事件。
|
||||
- `policyEffectiveness` 关联临时不可调度、failover、选号失败、`forward_failed` 和最终结果;仅按 request id 关联并披露缺失覆盖率。
|
||||
- 原生 system-log 索引没有策略事件而 overview 存在上游错误时,自动回退 target runtime 日志补事件,并披露 fallback 原因、数量和原生索引计数。
|
||||
- 全池默认只输出主因排名,账号详情通过 `--account` 下钻;全池和单账号默认输出均保持在 CLI 预算内,`--full` 保留完整分布。
|
||||
- `trace` 改为 `/ops/requests` 与 `/ops/system-logs` 优先,target runtime 日志只补邻近上下文。
|
||||
- 修复 PK01 trace 固定读取 k3s Deployment、Docker stderr 业务日志遗漏和原生事件倒序问题。
|
||||
- `forward_failed` 与 HTTP 200 并存且没有 failover 时,trace 标记为 `degraded`,不再误报普通成功。
|
||||
- 修正 `No available channel for model` 根因分类和所有 Ops 错误被描述为 HTTP 500 的误导文案。
|
||||
|
||||
## 现场验证
|
||||
|
||||
- 10 分钟原生 overview 样本曾观测 165 个请求、2 个客户可见错误,错误率 1.21%,上游错误率 3.03%;后续窗口为 189 个请求、1 个客户可见错误,错误率 0.53%,上游错误率 2.65%。这些是滚动窗口快照,不代表长期趋势。
|
||||
- 原生 concurrency 显示组容量 218,现场占用约 9 至 10,无排队;全池错误不能归因于 Sub2API 组并发耗尽。
|
||||
- 原生 availability 显示账号 29、30 因 `403 Insufficient account balance` 处于 error;账号 27 在短时冷却窗口可能暂不可用。
|
||||
- 策略事件样本显示临时不可调度和 failover 均实际发生;部分切号成功,仍存在切号后失败、结果缺失和 `forward_failed + HTTP 200`。
|
||||
- request `03a94444-ff26-4038-b1b9-67e249ce7f59`:账号 33 返回 524 后切到账号 31,账号 31 返回 502,随后 `account-select-failed/context canceled`,最终客户状态 502。
|
||||
- request `55d32c6a-09ae-46ac-9a34-3f63934e7f62`:账号 31 出现并发错误但 HTTP 200,没有 failover,trace 正确标记 `degraded`。
|
||||
- 账号 31 的 10 分钟 Ops 记录同时包含 429 rate-limit 和明确 concurrency 文案;当前并发快照不能替代错误发生时并发,后续只能通过受控对比判断是否需要调整账号并发。
|
||||
- 原生 system-log sink 健康,无 dropped/write failed;但策略事件查询为零,当前需要 runtime 事件 fallback。
|
||||
|
||||
## 验证命令
|
||||
|
||||
- `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/platform-infra-sub2api-codex/render.ts`
|
||||
- `bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors --target PK01 --since 10m`
|
||||
- `bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors --target PK01 --since 10m --account 31`
|
||||
- `bun scripts/cli.ts platform-infra sub2api codex-pool trace --target PK01 --request-id 03a94444-ff26-4038-b1b9-67e249ce7f59`
|
||||
- `git diff --check`
|
||||
|
||||
## 后续
|
||||
|
||||
- R2.2 持续观察新窗口,区分客户可见错误率、上游错误率、成功 failover 和流式 degraded。
|
||||
- R2.3 调查原生 system-log 索引为何未保留策略事件,并优先推动 Sub2API 原生能力覆盖,减少 runtime 日志 fallback。
|
||||
@@ -0,0 +1,107 @@
|
||||
# R2.2.1 客户可见错误溯源与调优建议
|
||||
|
||||
## 范围与数据源
|
||||
|
||||
- 运行目标:PK01 host-Docker,Sub2API `0.1.153`。
|
||||
- 原生 API:`dashboard/overview`、`upstream-errors`、`account-availability`、`concurrency`、`requests`、`system-logs/health`。
|
||||
- 原生 `system-logs` 未索引策略事件,按既定 CLI 规则回退读取 runtime 日志。
|
||||
- 观察窗口:连续 20 分钟快照与 60 分钟汇总。
|
||||
- 本轮只分析,没有修改 YAML、runtime 配置或 Sub2API 代码。
|
||||
|
||||
## 客户错误率
|
||||
|
||||
- 20 分钟窗口:335 次请求,4 次客户错误,客户错误率 1.19%,上游错误率 2.99%。
|
||||
- 60 分钟窗口:907 次请求,8 次客户错误,客户错误率 0.88%,上游错误率 3.64%。
|
||||
- 60 分钟内发生 7 次 `account_select_failed`,几乎覆盖全部客户可见错误。
|
||||
- 组并发快照为 9/218,负载 4.13%,排队 0;全池并发不是主因。
|
||||
- system-log sink 的 queue、drop 和 write failure 均为 0;日志落库不是业务错误来源。
|
||||
|
||||
## 请求级根因
|
||||
|
||||
### 模型候选为零
|
||||
|
||||
- 三个 `gpt-5.4-nano` 流式请求分别在 5ms、10ms、28ms 内未选中账号并返回 404。
|
||||
- 请求 ID:`21e5d449-3b0d-4a70-a798-1dc0af0ba110`、`55f7eb2a-b469-4d90-9ec9-3d20d278aef7`、`e90dfd63-22c1-419b-a479-53f860d4151a`。
|
||||
- 官方源码会在组内存在账号但没有任何 `model_mapping` 支持请求模型时返回 `404 model_not_found`。
|
||||
- 这不是瞬时容量问题,重试、临时不可调度和提高切号次数均无效。
|
||||
|
||||
### 多账号连续失败后候选耗尽
|
||||
|
||||
- `8150234d-eee2-4a08-a43e-e47a22274609`:`gpt-5.4`,account 28 返回 503、account 27 返回 429、account 31 返回 429,约 6.1 秒后无候选,最终 429。
|
||||
- `bf9aea0a-869a-4ee0-a99c-3b0ceb553822`:`gpt-5.6-luna`,account 28 返回 503、account 27 返回 429,约 67 秒后上下文取消且无候选,最终 429。
|
||||
- `03a94444-ff26-4038-b1b9-67e249ce7f59`:`gpt-5.6-sol`,account 33 返回 524、account 31 返回 502,约 180 秒后上下文取消且无候选,最终 502。
|
||||
- `5d8f05ff-9cbc-4040-b6eb-4b262ab85f39`:`gpt-5.6-sol`,account 33 返回 524,约 126 秒后上下文取消且无候选,最终 502。
|
||||
- `max_account_switches=10` 只是上限。官方实现会排除已失败账号并重新选号;剩余账号不满足模型或运行状态时,会在只切 1 至 3 次后结束。
|
||||
|
||||
### 流式提交后错误
|
||||
|
||||
- 成功样本证明自动 failover 正常工作:account 27 的 502 可切到 account 33,account 28 的 503 可切到 account 31。
|
||||
- 存在 `forward_failed + HTTP 200`,例如 account 31 返回 `Concurrency limit exceeded`,但下游已经提交流式响应。
|
||||
- 官方 `v0.1.153` 在检测到已写入语义响应后明确禁止换号,以免拼接两个上游流。
|
||||
- 此类错误不能靠临时不可调度透明恢复,只能减少上游发生率,或由客户端在流中断后重试或恢复。
|
||||
|
||||
## 上游账号主因
|
||||
|
||||
- 60 分钟上游记录共 72 条:rate-limit 24、overload 15、concurrency 12、generic upstream failure 11、status-only 6、model-route-unavailable 4。
|
||||
- account 28:10 次 overload,当前高频触发 503 临时不可调度。
|
||||
- account 27:rate-limit 为主,同时有 502 generic failure;现有规则只覆盖 502,不覆盖真实 429 文本。
|
||||
- account 31:13 次 rate-limit 为主,另有 concurrency 和一次 `GROUP_DISABLED`;模板未匹配 `Upstream rate limit exceeded`。
|
||||
- account 19:7 次 504,其中 2 次明确 concurrency、1 次 upstream unavailable、4 次只有状态;另有 3 次 `No available channel for model`。
|
||||
- account 32:2 次 429 rate-limit、1 次 502 concurrency。
|
||||
- account 29、30:余额不足导致 403,已处于 `status=error`、`schedulable=false`,不会继续被调度,但减少了候选容量。
|
||||
|
||||
## 官方版本判断
|
||||
|
||||
- PK01 运行 `v0.1.153`。
|
||||
- `v0.1.152..v0.1.153` 的相关变更是 WebSocket ingress 生命周期限制,不改变当前 HTTP 模型选号、failover 或临时不可调度匹配。
|
||||
- `v0.1.149` 已包含无账号支持模型时返回 404 的实现,因此回退不会消除 `gpt-5.4-nano` 404。
|
||||
- `v0.1.153` 已包含 compact 心跳不阻断 failover 的官方 #3887 修复。没有证据表明回退能降低当前错误率,不建议回退。
|
||||
|
||||
## 调优建议
|
||||
|
||||
### P0:修正模型目录与映射
|
||||
|
||||
- 查明 `gpt-5.4-nano` 是客户端误配、错误广告模型还是预期别名。
|
||||
- 若没有真实上游支持,从客户可见模型目录和调用配置中移除。
|
||||
- 若业务确实需要,只映射到已经实测支持的真实模型,不能为消除 404 添加虚假映射。
|
||||
|
||||
### P1:扩展保守临时不可调度模板
|
||||
|
||||
- 在 429 规则增加真实关键词 `upstream rate limit exceeded`,保持 1 分钟冷却。
|
||||
- 新增 504 规则时只加入实测关键词 `concurrency limit exceeded` 和 `upstream service temporarily unavailable`,保持 1 分钟冷却。
|
||||
- 不增加任意 504 规则;4 次 504 只有状态码,不能推断真实原因。
|
||||
- 不把 `No available channel for model` 放入通用账号级模板;这是模型级缺口,账号级摘除会误伤其他模型。
|
||||
- `lyon9801` 继续保持例外,不绑定临时不可调度规则。
|
||||
|
||||
### P1:账号级并发对照调优
|
||||
|
||||
- 不降低组级并发。
|
||||
- account 31 有最强直接并发证据,可先做单账号对照,例如从 30 降到 20,观察 30 至 60 分钟的错误和吞吐。
|
||||
- account 19、27、32 也有信号,但没有错误时刻并发历史;不要一次性全降。
|
||||
- 手工账号属于 runtime 管理面,后续通过 runtime CRUD 调整,不纳入 YAML managed profile 自动同步。
|
||||
|
||||
### P1:降低慢失败对客户时间预算的吞噬
|
||||
|
||||
- 524 请求分别在约 126 秒和 180 秒后穿透,第一个慢上游已消耗大部分客户超时预算。
|
||||
- 官方默认 `gateway.response_header_timeout=600s`,明显长于当前客户或边缘上下文。
|
||||
- 建议先确认运行实际值,再受控收紧 OpenAI 响应头超时,为一次 failover 预留预算;不能直接照抄固定值。
|
||||
- 对照指标应包含首 token 延迟、超时型上游错误、failover 成功率和客户错误率。
|
||||
|
||||
### P2:恢复候选容量与调度偏好
|
||||
|
||||
- account 29、30 只有补足余额并通过真实请求验证后才能恢复,不能直接改为 schedulable。
|
||||
- account 28、27、31 当前优先级高且错误集中,可在确认模型覆盖后降低调度偏好,让更稳定账号先承担请求。
|
||||
- Ops system-log 未索引策略事件,60 分钟有 144 条事件依赖 runtime fallback;这不造成业务错误,但限制长期归因,由 R2.3 跟踪。
|
||||
|
||||
## 不建议的措施
|
||||
|
||||
- 不建议回退到 `v0.1.149`。
|
||||
- 不建议提高 `max_account_switches`。
|
||||
- 不建议降低全池并发。
|
||||
- 不建议启用 `pool_mode` 做同账号重试。
|
||||
- 不建议对所有 4xx/5xx 做无关键词临时不可调度。
|
||||
|
||||
## 后续验收
|
||||
|
||||
- 按 20 分钟和 60 分钟双窗口继续观察客户错误率、upstream error rate、`account_select_failed`、模型 404、failover 成功率、`forward_failed + 200` 和每账号根因。
|
||||
- 每次只改变一个变量并保留前后窗口;R2.2 继续保持进行中。
|
||||
@@ -0,0 +1,64 @@
|
||||
# R2.2.2 任务报告
|
||||
|
||||
## 目标
|
||||
|
||||
执行三项客户错误调优:
|
||||
|
||||
1. 处理 `gpt-5.4-nano` 无候选。
|
||||
2. 为 429 临时不可调度规则加入真实上游关键词 `upstream rate limit exceeded`。
|
||||
3. 增加仅匹配 `concurrency limit exceeded` 与 `upstream service temporarily unavailable` 的 504 规则。
|
||||
|
||||
同时按用户追加要求增强精准批量 runtime API。
|
||||
|
||||
## 模型能力结论
|
||||
|
||||
通过 Sub2API 原生账号接口 `GET /api/v1/admin/accounts/:id/models` 和 `POST /api/v1/admin/accounts/:id/models/sync-upstream` 检查账号 15、19、27-34:
|
||||
|
||||
- 所有账号当前 `model_mapping` 均不包含 `gpt-5.4-nano`。
|
||||
- 可实时同步的账号 19、27、28、32、33、34 的上游模型列表均不包含该模型。
|
||||
- 账号 29、30、31 的上游模型同步返回 HTTP 403,不能据此声明支持。
|
||||
- `lyon9801` 为 OAuth 账号,Sub2API 原生同步接口明确不支持该类型,当前映射也不包含该模型。
|
||||
|
||||
因此没有创建虚假的 `nano -> mini` 或同名映射。当前池的 `/v1/models` 不宣称支持 `gpt-5.4-nano`,直接请求继续快速返回无候选 404。后续只有真实上游模型列表或实测请求证明支持时,才通过显式 model-mapping CRUD 加入。
|
||||
|
||||
## 配置调优
|
||||
|
||||
YAML 模板 `codex-upstream-failover` 保持一分钟冷却,并累计为六条状态规则:
|
||||
|
||||
- 429:`concurrency limit exceeded`、`upstream rate limit exceeded`。
|
||||
- 500:`failed to validate api key`、`do request failed`。
|
||||
- 502、503、524:保留前序已验证的上游不可用、过载和并发关键词。
|
||||
- 504:仅 `concurrency limit exceeded`、`upstream service temporarily unavailable`。
|
||||
|
||||
通过受控 runtime CLI 对账号 19、27-34 dry-run 后确认下发。逐账号回读均为 `ruleCount=6`、状态码 `429/500/502/503/504/524`,且匹配模板。账号 15 `lyon9801` 保持 `ruleCount=0`,没有应用临时不可调度。
|
||||
|
||||
## CLI 增强
|
||||
|
||||
新增:
|
||||
|
||||
- `runtime models --account <id>`:同时显示脱敏 `model_mapping`、配置模型和原生上游实时模型。
|
||||
- `runtime apply|delete --kind model-mapping`:显式单账号模型映射 CRUD,默认 dry-run。
|
||||
- `runtime apply|delete --accounts <id-or-name,...>`:一次受控作业内按显式账号集合顺序执行,拒绝隐式 all、重复账号和非 API-key 账号。
|
||||
- 批量默认输出压缩为账号 ID、名称、change、mode、mutation 和汇总计数;`--full/--raw` 保留完整计划。
|
||||
|
||||
批量验收命令精确选择账号 `19,27,28,29,30,31,32,33,34`,结果为 `requestedCount=9`、`succeededCount=9`、`mutatedCount=0`,全部 noop,且未包含账号 15。
|
||||
|
||||
## 即时观察
|
||||
|
||||
调优后拉取 20 分钟 runtime 错误窗口:
|
||||
|
||||
- 匹配 12 个日志事件。
|
||||
- 结构化上游状态为 502 一次、524 六次。
|
||||
- 账号 27 仍出现四次并发限制文本,但该窗口跨越配置下发时刻,不能用于评价新规则最终效果。
|
||||
- 账号 33 有一次 `gpt-5.6-sol` 与 ChatGPT Codex 账号不兼容的 400,属于独立模型路由问题。
|
||||
- 未观察到配置写入造成的新类型异常。
|
||||
|
||||
父任务 R2.2 继续用后续完整窗口观察客户可见错误率、切号和冷却命中效果。
|
||||
|
||||
## 验证
|
||||
|
||||
- `bun --check scripts/src/platform-infra-sub2api-codex/runtime.ts`:通过。
|
||||
- `git diff --check`:通过。
|
||||
- 九账号 runtime dry-run、confirm、逐账号 get:通过。
|
||||
- 精准批量 dry-run:通过。
|
||||
- 20 分钟 runtime errors:通过。
|
||||
@@ -0,0 +1,53 @@
|
||||
# R2.2.3 任务报告
|
||||
|
||||
## 目标
|
||||
|
||||
把本轮形成的 Sub2API runtime 控制、原生数据诊断、客户可见错误溯源和配置调优方法沉淀到 `unidesk-sub2api` skill,避免后续代理重新试探入口、证据口径和下发边界。
|
||||
|
||||
## 沉淀内容
|
||||
|
||||
### Skill 触发与路由
|
||||
|
||||
- 扩展 skill description,使 runtime CRUD、精准批量、临时不可调度、上游错误率、客户可见错误、failover、模型映射和可用模型探测均能触发本 skill。
|
||||
- `SKILL.md` 将 runtime 控制路由到 `references/codex-pool.md`。
|
||||
- 客户错误溯源、错误率分析和配置调优路由到 `references/troubleshooting-accounts.md`。
|
||||
- 同步更新 `agents/openai.yaml` 的简述和默认提示。
|
||||
|
||||
### Runtime 控制合同
|
||||
|
||||
`references/codex-pool.md` 固化:
|
||||
|
||||
- `runtime list|get|models|errors|apply|delete` 的职责。
|
||||
- `get` 的脱敏 `modelMapping` 与临时不可调度规则可见性。
|
||||
- `models` 区分配置模型和上游实时模型。
|
||||
- `temp-unschedulable` 与 `model-mapping` 两类 CRUD。
|
||||
- 单账号 `--account` 和显式精准批量 `--accounts`,禁止隐式 `all`。
|
||||
- 批量选择预检、顺序写入、中途 admin API 失败后的逐账号对账要求。
|
||||
- 默认 dry-run、`--confirm` 写入、紧凑批量输出和 Secret/credentials 脱敏。
|
||||
- YAML 只保存通用模板和 managed 引用,runtime-manual 账号继续使用 CRUD,不为批量操作强行纳入 YAML。
|
||||
- 文档命令与当前分支 CLI 不一致时按集成漂移处理,禁止退回手写管理 API。
|
||||
|
||||
### 调优分析流程
|
||||
|
||||
`references/troubleshooting-accounts.md` 固化八步流程:
|
||||
|
||||
1. 用原生 dashboard、availability、concurrency、upstream errors 获取全账号同窗口事实。
|
||||
2. 用有限 request id 和 `trace` 区分切号成功、候选耗尽、首次无候选和流式 degraded。
|
||||
3. 临时不可调度规则必须同时有真实状态码与响应关键词证据。
|
||||
4. 无候选先用原生模型接口核实能力,不猜测近似模型映射。
|
||||
5. 按 YAML-managed 与 runtime-manual 所有权分别下发,默认一分钟冷却且无新证据不超过三分钟。
|
||||
6. 不用当前并发快照证明错误时并发,降低并发只做有直接证据的有界对照。
|
||||
7. 短窗口只做回归,效果等待新的完整窗口并综合请求分母、错误率、规则命中和切号结果。
|
||||
8. 版本回退必须有源码差异或版本对照证据。
|
||||
|
||||
## 实现边界
|
||||
|
||||
精准批量和 `runtime models` 已在 `/root/unidesk/.worktree/sub2api-error-tuning` 完成真实 PK01 验证。主工作区当前 runtime CLI 尚未语义吸收这部分实现,且同文件存在并行诊断增强修改,因此本任务没有覆盖主工作区 runtime 源码。skill 已加入 CLI 集成漂移防护,后续必须先完成语义合入,不能手写 Sub2API admin API 绕过。
|
||||
|
||||
## 验证
|
||||
|
||||
- `quick_validate.py .agents/skills/unidesk-sub2api`:通过。
|
||||
- `git diff --check`:通过。
|
||||
- skill 主文件 46 行,三个核心文件合计 207 行,保持渐进披露。
|
||||
- `agents/openai.yaml` 的 `default_prompt` 显式引用 `$unidesk-sub2api`。
|
||||
- 未写入账号 ID、供应商名称、当前错误率或其他一次性运行数据。
|
||||
@@ -0,0 +1,104 @@
|
||||
# R2.2.4 所有分组客户错误与 CLI 渐进披露报告
|
||||
|
||||
## 交付范围
|
||||
|
||||
- `runtime errors --all-groups`:按稳定 group ID 游标分页,默认返回所有分组概要。
|
||||
- `runtime errors --group <name-or-id>`:下钻单个分组的原生 Ops 指标、账号根因、策略效果和 request ID 索引。
|
||||
- `--platform <name>`:可选平台过滤。
|
||||
- `--page-token <group-id>`:固定分页游标,不使用手工 `limit` 规避输出问题。
|
||||
- `runtime list|get|errors|apply|delete`:默认统一为 Kubernetes 风格紧凑表格。
|
||||
- `--json`、`--full`、`--raw`:仅在显式请求时返回机器结构或完整披露。
|
||||
|
||||
## 运行面证据
|
||||
|
||||
- 目标:PK01 host-Docker。
|
||||
- 实际镜像:`docker.1panel.live/weishaw/sub2api:0.1.153`,容器 healthy。
|
||||
- 数据源:Sub2API 原生 dashboard overview、account availability、concurrency、upstream-errors、requests 和 system-logs。
|
||||
- 原生 system-log 没有保留全部策略事件时,CLI 显式回退到 runtime 日志。
|
||||
- system-log sink 的 queue、drop 和 write failure 均为 0。
|
||||
|
||||
## 所有分组概要
|
||||
|
||||
同一 60 分钟查询页共 3 个分组:
|
||||
|
||||
- 全局:757 次请求,8 次客户错误,客户错误率 1.06%,34 次上游错误,上游错误率 4.49%。
|
||||
- `unidesk-codex-pool`,group 2:277 次请求,6 次客户错误,错误率 2.17%;21 次上游错误,上游错误率 7.58%。
|
||||
- `自用`,group 3:480 次请求,2 次客户错误,错误率 0.42%;13 次上游错误,上游错误率 2.71%。
|
||||
- `grok`,group 5:窗口内无请求和错误。
|
||||
- group 2 承担 36.6% 请求,却产生 75% 客户错误,是当前主要客户错误来源。
|
||||
|
||||
查询时两个活跃分组均为 3/218 并发、队列 0。降低全局或分组并发不能解释当前错误,不应作为主修复。
|
||||
|
||||
## 账号和共享证据边界
|
||||
|
||||
- group 2、3 共享大部分 OpenAI 账号。
|
||||
- 原生 dashboard 请求量和客户错误率按 group 精确统计,可以跨组汇总。
|
||||
- upstream-errors 是账号级数据;共享账号的根因记录会出现在多个 group 下钻中,禁止跨组相加。
|
||||
- 缺少 `group_id` 的临时摘号事件只能按账号 membership 和 request ID 关联,CLI 已标注为共享证据。
|
||||
- 原生聚合显示每个活跃组 9 个账号、6 个可用、3 个错误账号。
|
||||
- 错误账号为 29、30 的余额不足 403,以及 31 的 API Key 所属分组已删除 403。
|
||||
|
||||
账号共享窗口内的 34 条上游根因记录为:
|
||||
|
||||
- overload:18。
|
||||
- status-only-upstream-failure:9。
|
||||
- generic-upstream-failure:7。
|
||||
|
||||
## 客户可见错误链
|
||||
|
||||
### 长耗时失败后 context 已取消
|
||||
|
||||
- `cf6b6257-9640-414f-918d-c55180fb6a8d`:`gpt-5.6-luna`,账号 27 返回 502,约 41.6 秒后进入 failover,下一次选号立即报 `context canceled`,最终 502。
|
||||
- `4ee54daa-f41c-4f50-bd72-11080c38e299`:`gpt-5.6-sol` 流式请求,账号 27 返回 502,约 730.8 秒后进入 failover,下一次选号立即报 `context canceled`,最终 502。
|
||||
- 两条请求均已触发切号;失败点是第一个上游耗尽了客户请求时间预算,不是 `max_account_switches` 太小,也不是池容量不足。
|
||||
|
||||
### HTTP 200 后流式失败
|
||||
|
||||
- `46c1374a-c7f1-49f8-877d-d7ffb216976c`:账号 28 返回 503 后切到账号 33,网关最终 access status 为 200。
|
||||
- 随后出现 `openai.forward_failed`,原文指向 `input[2].encrypted_content` 的 ObjectParam/required-parameter 错误。
|
||||
- CLI 现判定 `outcome=degraded`、`reason=forward-failed-http-success`,不再把 HTTP 200 误报为完整成功。
|
||||
- 流式语义已经提交后不能安全拼接另一个上游响应,透明换号只适用于提交语义响应之前。
|
||||
|
||||
## 临时不可调度和 failover 效果
|
||||
|
||||
- group 3 下钻观察到 20 次临时不可调度事件、13 个 failover 请求。
|
||||
- 其中 2 个明确成功、1 个明确失败、10 个结果在有限日志索引中缺失。
|
||||
- 另有 1 个 `forward_failed + HTTP 200` degraded 请求。
|
||||
- 规则能减少立即重复命中坏账号,但无法恢复已经耗尽 context 的请求,也无法恢复已经提交语义流后的协议错误。
|
||||
|
||||
## 官方仓库与版本判断
|
||||
|
||||
- 官方 v0.1.153 发布说明只涉及调度缓存异常时间、WebSocket 生命周期等修复,没有声明改变当前 HTTP Responses 的 failover 时间预算或流式提交后切号规则。
|
||||
- 官方 issue #2067 与合并 PR #2068 证明旧版曾存在 Responses 多轮 reasoning item 回放导致 404/502 的协议转换缺陷;修复已在 2026-04-29 合入主线。
|
||||
- 当前 `encrypted_content` 原文与该 issue 相近但不完全相同,不能直接判定为同一个缺陷。
|
||||
- 现有证据不支持从 v0.1.153 回退;回退可能重新引入已修复的 Responses 协议问题。
|
||||
- 官方来源:
|
||||
- https://github.com/Wei-Shaw/sub2api/releases/tag/v0.1.153
|
||||
- https://github.com/Wei-Shaw/sub2api/issues/2067
|
||||
- https://github.com/Wei-Shaw/sub2api/pull/2068
|
||||
|
||||
## 调优建议
|
||||
|
||||
- P0:保持 v0.1.153,不回退,不提高 `max_account_switches`,不降低全池并发。
|
||||
- P0:确认实际 OpenAI 响应头/首 token 超时,把单个慢上游的允许时间收紧到能为至少一次 failover 留出预算;先做有界对照,不直接照抄固定值。
|
||||
- P1:继续使用最多 3 分钟的保守临时不可调度;当前一分钟冷却已经频繁命中,不扩大到无关键词的所有 5xx。
|
||||
- P1:针对账号 28 的 overload 和账号 27 的 502 保持即时冷却,观察完整 60 分钟窗口的客户错误率和 failover 成功率。
|
||||
- P1:对账号 33 的 `gpt-5.6-sol` continuation/`encrypted_content` 错误积累更多 request 证据;重复出现时再做账号与模型级调度隔离,不能凭单个样本做全账号摘除。
|
||||
- P1:修复账号 29、30 的余额和账号 31 的失效 API Key 后,通过真实请求验证再恢复;禁止仅改 `schedulable`。
|
||||
- P2:继续增强原生 system-log 对策略事件和最终 outcome 的索引,减少 runtime fallback 与 outcome missing。
|
||||
|
||||
## 验证
|
||||
|
||||
- `bun --check`:`runtime.ts`、`remote-python-sync.ts`、`output.ts` 通过。
|
||||
- `git diff --check` 通过。
|
||||
- `bun test scripts/src/output.test.ts`:2 pass,0 fail。
|
||||
- 默认 `runtime errors --all-groups`:紧凑表格,无 JSON envelope,无 `outputTruncated`。
|
||||
- 默认 `runtime errors --group 3`:紧凑详情、共享证据提示和 request 索引可见。
|
||||
- 默认 `runtime list`:Kubernetes 风格账号列表。
|
||||
- 显式 `--json`:返回机器可读结构。
|
||||
- degraded trace 与两条 failover-no-candidate trace 均通过真实 PK01 原生 API 回归。
|
||||
|
||||
## 后续
|
||||
|
||||
- 父任务 R2.2 保持进行中,继续按完整窗口观察调优效果。
|
||||
- 未创建外部 GitHub issue;相关未决项继续由本 MDTODO 问题域跟踪,避免未经授权写入外部系统。
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
合并修复 PR 后只观察新的 GitHub webhook→Gitea mirror→PaC→Tekton→GitOps/Argo 自动事件,验证 watcher Ready、TaskRun 创建及 AgentRun/Monitor runtime 收敛,不人工补齐既有 queued run,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R5.3_Task_Report.md)。
|
||||
|
||||
## R6 [in_progress]
|
||||
## R6 [completed]
|
||||
|
||||
执行 [UniDesk #1981](https://github.com/pikasTech/unidesk/issues/1981):固定仓库产品功能 schema 路径为 config/feature-config.schema.json,CI/CD 只读校验 schema 语法、值匹配和功能到唯一配置变量的一对一关系;缺失、非法、不匹配或重复仅输出 typed warning 并投影到 OTel/status/history/debug-step,必须继续 artifact、GitOps/Argo 和滚动上线,禁止写回、默认值、变量删除、功能关闭、架构切换及其他补救操作,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R6_Task_Report.md)。
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Sub2API 上游可靠性
|
||||
|
||||
本问题域持续跟踪 Sub2API 上游错误的真实归因、账号运行时可见性、受控调度缓解和用户侧错误隔离。配置事实以 owning YAML 为准,手工账号运行时配置通过受控 CRUD 显式管理。
|
||||
|
||||
|
||||
## R1 [completed]
|
||||
|
||||
完善 Sub2API 上游错误归因与 runtime CRUD 可见性,并基于实际错误证据应用保守的临时不可调度策略,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R1_Task_Report.md)。
|
||||
|
||||
## R2 [in_progress]
|
||||
|
||||
在新观测窗口复核临时不可调度命中、内部换号和最终客户响应,按请求分母评估调优效果并识别仍会穿透的流式错误,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2_Task_Report.md)。
|
||||
|
||||
### R2.1 [completed]
|
||||
|
||||
增强 CLI 使用 Sub2API 原生 admin/ops 数据诊断请求错误、账号可用性、并发和 request trace,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.1_Task_Report.md)。
|
||||
|
||||
### R2.2 [in_progress]
|
||||
|
||||
在连续新窗口复核临时不可调度、failover、客户可见错误率和流式 degraded 请求,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2_Task_Report.md)。
|
||||
|
||||
#### R2.2.1 [completed]
|
||||
|
||||
溯源最近窗口客户可见错误请求,区分上游失败、切号耗尽、无候选和流式提交后错误并给出配置调优建议,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.1_Task_Report.md)。
|
||||
#### R2.2.2 [completed]
|
||||
|
||||
执行客户错误调优:处理 gpt-5.4-nano 无候选,扩展 429 upstream rate limit 与精确 504 临时不可调度规则,并通过受控 runtime apply 下发和验证,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.2_Task_Report.md)。
|
||||
#### R2.2.3 [completed]
|
||||
|
||||
将 runtime 精准 CRUD、批量操作、原生模型探测、客户可见错误溯源和基于真实证据的配置调优流程沉淀到 unidesk-sub2api skill,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.3_Task_Report.md)。
|
||||
#### R2.2.4 [completed]
|
||||
|
||||
重新分析所有 Sub2API 分组最近的用户可见错误,并把 runtime errors 泛化为 all-groups 与精确 group 下钻;默认输出必须提供语义摘要、索引和渐进披露,禁止用手工 limit 规避输出问题,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.2.4_Task_Report.md)。
|
||||
|
||||
### R2.3
|
||||
|
||||
调查并收敛 Sub2API 原生 system-log 索引未保留策略事件的能力缺口,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.3_Task_Report.md)。
|
||||
@@ -157,8 +157,26 @@
|
||||
- `runtime.templates` 保存可复用的 Sub2API runtime 规则模板。模板本身不是账号绑定,不会自动接管未在 YAML 中声明的手工账号。
|
||||
- `pool.defaultTempUnschedulableTemplate` 是 YAML-managed 账号的默认临时不可调度模板引用;`profiles.entries[].tempUnschedulableTemplate` 可以为明确声明的账号选择其他模板。`codex-pool sync --confirm` 只向这些 managed 账号渲染 `temp_unschedulable_enabled` 和 `temp_unschedulable_rules`。
|
||||
- YAML 未声明的账号属于 `runtime-manual`。`codex-pool runtime list|get` 可以显示其实际状态和 `matchingTemplate`,但与模板不同不构成 drift,也不得由 `sync` 自动纳管。
|
||||
- `codex-pool runtime errors` 从实际 Sub2API 日志按账号聚合错误。规则状态码只能取 `account_upstream_error` 的观测值;`openai.forward_failed` 只作为脱敏错误摘要证据,不能凭摘要推测未观测状态码。
|
||||
- `codex-pool runtime errors` 优先从 Sub2API 原生 admin/ops 能力读取服务数据:
|
||||
- dashboard overview 提供请求分母、客户可见错误率和上游错误率;
|
||||
- account availability 提供账号可用、限流、过载和错误状态;
|
||||
- concurrency 提供组与账号容量、当前占用和排队;
|
||||
- upstream errors 提供账号、状态码和根因详情;
|
||||
- system-log 索引未保留策略事件时,才从 target runtime 日志补临时不可调度、failover、选号失败和 `forward_failed`,并披露 fallback;
|
||||
- 规则状态码只能取 `account_upstream_error` 的观测值;
|
||||
- `openai.forward_failed` 只作为脱敏错误摘要证据,不能凭摘要推测未观测状态码;
|
||||
- Sub2API Ops 上游错误记录覆盖全部错误账号和状态码,默认输出全池与账号主因;
|
||||
- `policyEffectiveness` 通过 `request_id` 关联临时不可调度、切号、无候选和最终网关响应,并提供可交给 `trace` 的 request id;
|
||||
- 规则命中必须有 `account_temp_unschedulable` 日志证据,状态码相同不代表响应体关键词也命中;
|
||||
- `requestIdCoverage` 必须披露事件关联缺口,缺少 request id 时不能用零关联证明策略链未发生;
|
||||
- HTTP 200 与 `forward_failed` 可以在流式响应中并存,必须单列这类请求,不能仅凭最终 HTTP 状态判断客户收到完整终止事件;
|
||||
- 窗口内最终错误占比只描述目标 API 路径的已完成日志事件,必须保留 `since`、`tail` 和分母覆盖边界;
|
||||
- `--full` 才展开模型、小时、流式分布和有限详情样本;
|
||||
- 账号当前并发只作为查询时快照,不能替代错误发生瞬间的历史并发证据;
|
||||
- 没有账号全部路由尝试数时,只能报告错误数量,不能把错误记录数当作错误率;
|
||||
- Ops 样本与 runtime 日志保持独立计数,不把时间邻近当作请求级关联。
|
||||
- 手工账号的 runtime 配置必须通过 `codex-pool runtime apply|delete` 受控修改。写操作必须显式指定 `--account`,不带 `--confirm` 时只生成 dry-run,带 `--confirm` 才通过 Sub2API admin API 写入。
|
||||
- `codex-pool trace` 优先使用原生 `/admin/ops/requests` 和 `/admin/ops/system-logs`,target runtime 日志只补索引缺失的邻近事件;必须显式披露 target、runtime mode、主数据源、上下文数据源和读取状态。PK01 host-Docker 回退时不得固定读取 k3s Deployment,并且必须合并 Docker stdout/stderr 日志流。HTTP 200 与 `forward_failed` 并存且没有 failover 时应标记为 `degraded`。
|
||||
- 内置临时不可调度配置与外部 `sentinel.*` 是独立控制面。runtime 模板负责请求路径即时冷却与 failover;`sentinel.*` 负责账号 marker health、隔离、恢复和 probe 周期,任一控制面都不得静默重写另一控制面的状态或规则。
|
||||
- The external sentinel write surface is intentionally limited to the Sub2API admin `schedulable` action. Sentinel freeze/restore may set `schedulable=false|true`, but must not write, clear, or indirectly clear Sub2API request-path runtime state such as `temp_unschedulable_until`, `temp_unschedulable_reason`, rate-limit, overload, or model-rate-limit state. In particular, sentinel restore must not call Sub2API `recover-state`, because that endpoint is a broader runtime-state recovery operation rather than a pure schedulability restore.
|
||||
- Codex accounts selected by YAML do not declare `schedulable` as durable configuration. `codex-pool sync --confirm` must not restore existing account schedulability merely because YAML selects the account or sentinel state lacks an active quarantine. Existing `schedulable=false` is runtime state: the sentinel first reads Sub2API's actual account state, schedules a recovery probe for unschedulable managed accounts, and restores `schedulable=true` only after the marker probe matches.
|
||||
|
||||
@@ -307,7 +307,7 @@ function renderTextOutput(command: string, text: string): string {
|
||||
trigger,
|
||||
...(dump === null ? {} : { dump }),
|
||||
next: [
|
||||
"Use rg --max-count, head, tail, --limit, --tail-bytes, or an id-specific query.",
|
||||
"Use the command's semantic summary, page cursor, stable id, or id-specific detail query before requesting complete output.",
|
||||
"Use --full or --raw only when the complete one-off payload is intentionally required.",
|
||||
],
|
||||
},
|
||||
@@ -341,7 +341,7 @@ function oversizedOutputNext(envelope: JsonEnvelope<unknown>): string[] {
|
||||
];
|
||||
}
|
||||
return [
|
||||
"Use --limit, --tail-bytes, an id-specific view, or another semantic narrow query.",
|
||||
"Use the command's semantic summary, page cursor, stable id, or id-specific detail query; improve the command when none is available.",
|
||||
"Use --full or --raw only when the complete one-off payload is intentionally required.",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2867,6 +2867,11 @@ def classify_trace_event(item, account_names_by_id):
|
||||
"error": item.get("error"),
|
||||
"excludedAccountCount": item.get("excluded_account_count"),
|
||||
})
|
||||
elif "openai.forward_failed" in message:
|
||||
event.update({
|
||||
"type": "forward-failed",
|
||||
"error": item.get("error"),
|
||||
})
|
||||
elif "account_upstream_error" in message:
|
||||
event.update({
|
||||
"type": "upstream-error",
|
||||
@@ -2939,10 +2944,15 @@ def trace_reason(events, final_event):
|
||||
failovers = [item for item in events if item.get("type") == "failover"]
|
||||
select_failures = [item for item in events if item.get("type") == "select-failed"]
|
||||
upstream_errors = [item for item in events if item.get("type") == "upstream-error"]
|
||||
forward_failures = [item for item in events if item.get("type") == "forward-failed"]
|
||||
if failover_budget_exhausted(failovers, final_event):
|
||||
return "failover-budget-exhausted"
|
||||
if failovers and select_failures:
|
||||
return "failover-attempted-no-candidate"
|
||||
if forward_failures and isinstance(final_event, dict) and isinstance(final_event.get("statusCode"), int) and final_event.get("statusCode") < 400:
|
||||
return "forward-failed-http-success"
|
||||
if forward_failures:
|
||||
return "forward-failed"
|
||||
if failovers:
|
||||
return "failover-attempted"
|
||||
if select_failures:
|
||||
@@ -3006,6 +3016,60 @@ def run_trace():
|
||||
if not isinstance(request_id, str) or not request_id:
|
||||
raise RuntimeError("trace payload missing requestId")
|
||||
admin_email, token, admin_compliance = login()
|
||||
request_query = quote(request_id, safe="")
|
||||
native_request_status = {"status": "unavailable"}
|
||||
native_request = None
|
||||
try:
|
||||
request_data = ensure_success(
|
||||
curl_api("GET", f"/api/v1/admin/ops/requests?request_id={request_query}&kind=all&page=1&page_size=100", bearer=token),
|
||||
"get ops request details",
|
||||
)
|
||||
request_items = extract_items(request_data)
|
||||
native_request = next((item for item in request_items if isinstance(item, dict) and item.get("request_id") == request_id), None)
|
||||
native_request_status = {
|
||||
"status": "available",
|
||||
"matched": native_request is not None,
|
||||
"returned": len(request_items),
|
||||
"total": request_data.get("total", len(request_items)) if isinstance(request_data, dict) else len(request_items),
|
||||
}
|
||||
except Exception as exc:
|
||||
native_request_status = {"status": "unavailable", "reason": text(str(exc), 500)}
|
||||
|
||||
native_system_log_status = {"status": "unavailable"}
|
||||
native_matched = []
|
||||
try:
|
||||
system_log_data = ensure_success(
|
||||
curl_api("GET", f"/api/v1/admin/ops/system-logs?request_id={request_query}&page=1&page_size=200", bearer=token),
|
||||
"get ops request system logs",
|
||||
)
|
||||
system_log_items = extract_items(system_log_data)
|
||||
for row in system_log_items:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
parsed = dict(row.get("extra") or {}) if isinstance(row.get("extra"), dict) else {}
|
||||
for source_key in ("request_id", "client_request_id", "account_id", "platform", "model"):
|
||||
if parsed.get(source_key) is None and row.get(source_key) is not None:
|
||||
parsed[source_key] = row.get(source_key)
|
||||
parsed["_at"] = row.get("created_at")
|
||||
parsed["_message"] = row.get("message")
|
||||
parsed["_line"] = json.dumps({
|
||||
"created_at": row.get("created_at"),
|
||||
"message": row.get("message"),
|
||||
"request_id": row.get("request_id"),
|
||||
"account_id": row.get("account_id"),
|
||||
"platform": row.get("platform"),
|
||||
"model": row.get("model"),
|
||||
"extra": parsed,
|
||||
}, ensure_ascii=False, default=str)
|
||||
native_matched.append(parsed)
|
||||
native_system_log_status = {
|
||||
"status": "available",
|
||||
"returned": len(system_log_items),
|
||||
"total": system_log_data.get("total", len(system_log_items)) if isinstance(system_log_data, dict) else len(system_log_items),
|
||||
}
|
||||
except Exception as exc:
|
||||
native_system_log_status = {"status": "unavailable", "reason": text(str(exc), 500)}
|
||||
|
||||
account_snapshot, account_snapshot_error = account_snapshot_from_runtime(token)
|
||||
account_names_by_id = {}
|
||||
for row in account_snapshot:
|
||||
@@ -3014,17 +3078,21 @@ def run_trace():
|
||||
account_id = int(account_id)
|
||||
if isinstance(account_id, int) and isinstance(row.get("accountName"), str):
|
||||
account_names_by_id[account_id] = row.get("accountName")
|
||||
proc = kubectl(["-n", NAMESPACE, "logs", "deployment/sub2api", f"--since={since}", f"--tail={tail}"])
|
||||
stdout = proc.stdout.decode("utf-8", errors="replace")
|
||||
proc = runtime_logs(since, tail)
|
||||
log_bytes = proc.stdout + b"\\n" + proc.stderr
|
||||
stdout = log_bytes.decode("utf-8", errors="replace")
|
||||
parsed_lines = []
|
||||
matched = []
|
||||
runtime_matched = []
|
||||
for line in stdout.splitlines():
|
||||
parsed = parse_log_line(line)
|
||||
if parsed is None:
|
||||
continue
|
||||
parsed_lines.append(parsed)
|
||||
if request_id in line:
|
||||
matched.append(parsed)
|
||||
runtime_matched.append(parsed)
|
||||
matched = native_matched if native_matched else runtime_matched
|
||||
matched.sort(key=lambda item: (log_time_epoch(item) is None, log_time_epoch(item) or 0))
|
||||
trace_event_source = "sub2api-native-admin-ops-system-logs" if native_matched else "runtime-logs-fallback"
|
||||
first_epoch = None
|
||||
last_epoch = None
|
||||
for item in matched:
|
||||
@@ -3045,22 +3113,55 @@ def run_trace():
|
||||
window_lines = matched
|
||||
events = [classify_trace_event(item, account_names_by_id) for item in matched]
|
||||
request_start = next((item for item in events if item.get("type") == "request-start"), None)
|
||||
if isinstance(native_request, dict):
|
||||
if not isinstance(request_start, dict):
|
||||
request_start = {
|
||||
"type": "request-start",
|
||||
"at": native_request.get("created_at"),
|
||||
"requestId": native_request.get("request_id"),
|
||||
"path": None,
|
||||
"model": native_request.get("model"),
|
||||
"accountId": native_request.get("account_id"),
|
||||
"accountName": account_names_by_id.get(native_request.get("account_id")),
|
||||
"stream": native_request.get("stream"),
|
||||
"source": "sub2api-native-admin-ops-requests",
|
||||
}
|
||||
else:
|
||||
if request_start.get("stream") is None:
|
||||
request_start["stream"] = native_request.get("stream")
|
||||
if not request_start.get("model"):
|
||||
request_start["model"] = native_request.get("model")
|
||||
final_event = next((item for item in reversed(events) if item.get("type") == "final"), None)
|
||||
if not isinstance(final_event, dict) and isinstance(native_request, dict) and isinstance(native_request.get("status_code"), int):
|
||||
final_event = {
|
||||
"type": "final",
|
||||
"at": native_request.get("created_at"),
|
||||
"requestId": native_request.get("request_id"),
|
||||
"path": None,
|
||||
"model": native_request.get("model"),
|
||||
"accountId": native_request.get("account_id"),
|
||||
"accountName": account_names_by_id.get(native_request.get("account_id")),
|
||||
"statusCode": native_request.get("status_code"),
|
||||
"latencyMs": native_request.get("duration_ms"),
|
||||
"source": "sub2api-native-admin-ops-requests",
|
||||
}
|
||||
failovers = [item for item in events if item.get("type") == "failover"]
|
||||
select_failures = [item for item in events if item.get("type") == "select-failed"]
|
||||
upstream_errors = [item for item in events if item.get("type") == "upstream-error"]
|
||||
forward_failures = [item for item in events if item.get("type") == "forward-failed"]
|
||||
temp_unsched = [with_trace_phase(classify_trace_event(item, account_names_by_id), first_epoch, last_epoch) for item in window_lines if "account_temp_unschedulable" in str(item.get("_message") or "")]
|
||||
admin_sched = [with_trace_phase(classify_trace_event(item, account_names_by_id), first_epoch, last_epoch) for item in window_lines if ("schedulable" in str(item.get("_message") or "") or "/schedulable" in str(item.get("path") or ""))]
|
||||
window_events = [classify_trace_event(item, account_names_by_id) for item in window_lines]
|
||||
final_errors = [item for item in window_events if item.get("type") == "final" and isinstance(item.get("statusCode"), int) and item.get("statusCode") >= 400]
|
||||
window_failovers = [item for item in window_events if item.get("type") == "failover"]
|
||||
window_select_failures = [item for item in window_events if item.get("type") == "select-failed"]
|
||||
window_forward_failures = [item for item in window_events if item.get("type") == "forward-failed"]
|
||||
untried_schedulable_accounts = trace_untried_schedulable_accounts(failovers, final_event or {}, account_snapshot)
|
||||
reason = trace_reason(events, final_event)
|
||||
if not matched:
|
||||
outcome = "not-found"
|
||||
elif isinstance(final_event, dict) and isinstance(final_event.get("statusCode"), int) and final_event.get("statusCode") < 400:
|
||||
outcome = "succeeded"
|
||||
outcome = "degraded" if forward_failures else ("succeeded-with-failover" if failovers else "succeeded")
|
||||
elif isinstance(final_event, dict):
|
||||
outcome = "failed"
|
||||
else:
|
||||
@@ -3068,11 +3169,14 @@ def run_trace():
|
||||
return {
|
||||
"ok": proc.returncode == 0 and len(matched) > 0,
|
||||
"mode": "trace",
|
||||
"targetId": TARGET_ID,
|
||||
"runtimeMode": RUNTIME_MODE,
|
||||
"namespace": NAMESPACE,
|
||||
"serviceDns": SERVICE_DNS,
|
||||
"appPod": APP_POD,
|
||||
"admin": {"email": admin_email, "tokenPrinted": False, "compliance": admin_compliance},
|
||||
"requestId": request_id,
|
||||
"eventSource": trace_event_source,
|
||||
"summary": {
|
||||
"outcome": outcome,
|
||||
"reason": reason,
|
||||
@@ -3090,10 +3194,16 @@ def run_trace():
|
||||
},
|
||||
"request": request_start or {},
|
||||
"final": final_event or {},
|
||||
"nativeOps": {
|
||||
"requestDetails": native_request_status,
|
||||
"request": native_request,
|
||||
"systemLogs": native_system_log_status,
|
||||
},
|
||||
"events": events,
|
||||
"failovers": failovers,
|
||||
"selectFailures": select_failures,
|
||||
"upstreamErrors": upstream_errors,
|
||||
"forwardFailures": forward_failures,
|
||||
"tempUnschedulable": temp_unsched,
|
||||
"adminSchedulable": admin_sched[-20:],
|
||||
"windowStats": {
|
||||
@@ -3102,6 +3212,8 @@ def run_trace():
|
||||
"finalErrorCount": len(final_errors),
|
||||
"failoverCount": len(window_failovers),
|
||||
"selectFailedCount": len(window_select_failures),
|
||||
"forwardFailedCount": len(window_forward_failures),
|
||||
"matchedForwardFailedCount": len(forward_failures),
|
||||
"tempUnschedulableCount": len(temp_unsched),
|
||||
"adminSchedulableCount": len(admin_sched),
|
||||
},
|
||||
@@ -3114,8 +3226,10 @@ def run_trace():
|
||||
"rawLines": [{"line": item.get("_line")} for item in matched[-30:]] if show_lines else [],
|
||||
"showLines": show_lines,
|
||||
"logs": {
|
||||
"source": trace_event_source,
|
||||
"contextSource": f"host-docker:{HOST_DOCKER_APP_CONTAINER}" if RUNTIME_MODE == "host-docker" else f"k3s:{NAMESPACE}/deployment/sub2api",
|
||||
"exitCode": proc.returncode,
|
||||
"stderrTail": text(proc.stderr, 1000),
|
||||
"stderrTail": text(proc.stderr, 1000) if proc.returncode != 0 else "",
|
||||
"stdoutLineCount": len(stdout.splitlines()),
|
||||
},
|
||||
"valuesPrinted": False,
|
||||
|
||||
@@ -398,8 +398,10 @@ export function renderTraceReport(
|
||||
const failovers = recordArray(parsed.failovers);
|
||||
const selectFailures = recordArray(parsed.selectFailures);
|
||||
const upstreamErrors = recordArray(parsed.upstreamErrors);
|
||||
const forwardFailures = recordArray(parsed.forwardFailures);
|
||||
const tempUnschedulable = recordArray(parsed.tempUnschedulable);
|
||||
const windowStats = isRecord(parsed.windowStats) ? parsed.windowStats : {};
|
||||
const logs = isRecord(parsed.logs) ? parsed.logs : {};
|
||||
const accountSnapshot = recordArray(parsed.accountSnapshot);
|
||||
const lines: string[] = [];
|
||||
lines.push([
|
||||
@@ -409,6 +411,17 @@ export function renderTraceReport(
|
||||
`outcome=${stringValue(summary.outcome) ?? "-"}`,
|
||||
`reason=${stringValue(summary.reason) ?? "-"}`,
|
||||
].join(" "));
|
||||
lines.push([
|
||||
"SOURCE",
|
||||
`target=${textValue(parsed.targetId)}`,
|
||||
`runtime=${textValue(parsed.runtimeMode)}`,
|
||||
`logs=${textValue(logs.source)}`,
|
||||
`context=${textValue(logs.contextSource)}`,
|
||||
`exit=${textValue(logs.exitCode)}`,
|
||||
].join(" "));
|
||||
if (parsed.ok !== true && stringValue(logs.stderrTail) !== null) {
|
||||
lines.push(`LOG ERROR ${shorten(stringValue(logs.stderrTail) ?? "", 240)}`);
|
||||
}
|
||||
lines.push([
|
||||
"REQUEST",
|
||||
`path=${request.path ?? final.path ?? "-"}`,
|
||||
@@ -452,6 +465,19 @@ export function renderTraceReport(
|
||||
]),
|
||||
]));
|
||||
}
|
||||
if (forwardFailures.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("FORWARD-FAILED");
|
||||
lines.push(renderTable([
|
||||
["AT", "ACCOUNT", "STATUS", "ERROR"],
|
||||
...forwardFailures.map((item) => [
|
||||
shortIso(item.at),
|
||||
formatAccountRef(item),
|
||||
textValue(item.statusCode),
|
||||
shorten(stringValue(item.error) ?? "-", 72),
|
||||
]),
|
||||
]));
|
||||
}
|
||||
if (upstreamErrors.length > 0 || tempUnschedulable.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("ACCOUNT SIGNALS");
|
||||
@@ -482,13 +508,14 @@ export function renderTraceReport(
|
||||
lines.push("");
|
||||
lines.push("WINDOW STATS");
|
||||
lines.push(renderTable([
|
||||
["MATCH", "EVENTS", "FINAL_4XX_5XX", "FAILOVER", "SELECT_FAIL", "TEMP_UNSCHED", "ADMIN_SCHED"],
|
||||
["MATCH", "EVENTS", "FINAL_4XX_5XX", "FAILOVER", "SELECT_FAIL", "FORWARD_FAIL", "TEMP_UNSCHED", "ADMIN_SCHED"],
|
||||
[
|
||||
textValue(windowStats.matchedLines),
|
||||
textValue(windowStats.eventCount),
|
||||
textValue(windowStats.finalErrorCount),
|
||||
textValue(windowStats.failoverCount),
|
||||
textValue(windowStats.selectFailedCount),
|
||||
textValue(windowStats.forwardFailedCount),
|
||||
textValue(windowStats.tempUnschedulableCount),
|
||||
textValue(windowStats.adminSchedulableCount),
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -348,9 +348,9 @@ export function codexPoolHelp(): unknown {
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool plan --target D601",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sync [--target D601] --confirm [--prune-removed]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool validate [--target D601] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime list [--target PK01] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime get --account <name-or-id> [--target PK01] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors [--account <name-or-id>] [--since 24h] [--tail 50000] [--target PK01] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime list [--target PK01] [--json|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime get --account <name-or-id> [--target PK01] [--json|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <group-id>] [--account <name-or-id>] [--since 24h] [--tail 50000] [--target PK01] [--json|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account <name-or-id> --template <id> [--target PK01] [--confirm]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --account <name-or-id> --kind temp-unschedulable [--target PK01] [--confirm]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool trace [--target D601] --request-id <requestId> [--since 24h|--tail 20000|--context-seconds 300|--show-lines|--raw]",
|
||||
|
||||
Reference in New Issue
Block a user