# R2.4.9 任务报告 ## 结论 错误 #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();官方最新主线尚未修复该生命周期不一致。