# 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()；官方最新主线尚未修复该生命周期不一致。
