网关回了 200 却静默十分钟qwen-code 流空闲看门狗如何救回一条死掉的 SSE 流【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code请求 3 秒拿到 HTTP 200随后整整十分钟没有一个字符落屏。socket 没断、日志干净、重试循环纹丝不动直到 30 分钟会话 TTL 把连接掐死才醒来。这是 qwen-code 流式链路上一次真实事故的完整时间线而它的解法不是加大某个超时而是一套流空闲看门狗 流生命周期上限的组合守卫。200 之后挂掉的 595 秒事故的根因在模型网关侧Aliyun PrivateLink 链路返回 200 后SSE 响应体保持打开却完全静默约 595 秒内既无分片也无finish_reason。而 qwen-code 当时的三道防线没有一道覆盖200 之后SDK 的timeout只管连上。constants.ts 中DEFAULT_TIMEOUT 120000120 秒只覆盖建连 拿到响应对象这一层。chat.completions.create({stream: true})快速返回流对象后for await循环里分片与分片之间的等待是无界的——请求级超时鞭长莫及。唯一的空闲计时器只关 span不关请求。loggingContentGenerator.ts 的STREAM_IDLE_TIMEOUT_MS 5 * 60_0005 分钟只负责关闭 OTel span 防止泄漏不中止请求、不抛错。内容层重试永远等不到入口。NO_FINISH_REASON这类重试依赖流结束但没给 finish_reason这个信号流不结束信号就不产生重试逻辑没有介入机会。结果是会话只能干挂。传输层本该对空闲 socket 发出ETIMEDOUT但它没有——socket 活着只是不说话。合成一个 ETIMEDOUT而不是抛一个 AbortError修复的取舍逻辑来自两条既有代码事实它们共同决定了必须在哪里做、以什么形态抛错pipeline.executeStream内部创建了perRequestAc createChildAbortController(parentSignal)只有这个控制器能真正取消 fetch上一层 logging 包装器只持有只读信号所以看门狗必须落在 pipeline 层。llm-chat.ts 中classifyRetryError最先检查 abort 语义isAbortError || name CanceledError命中即判为{kind: abort, diagnosis: fail-fast}——不可重试。反过来getTransportCode(err)读取err.code/err.cause.code一个带code: ETIMEDOUT的普通 Error 会被判为{kind: transport, diagnosis: retryable}。也就是说裸抛 AbortError 会绕开整个恢复栈合成 ETIMEDOUT 则让静默停滞在分类层面与真实读超时不可区分直接复用既有的传输层重试/退避/回退。决策项选择依据空闲阈值streamIdleTimeoutMs默认240000ms4 分钟设计稿初稿为 120s落地放宽到 4 分钟为大 prompt 摄入和长思考留空间同时仍低于 logging 层 5 分钟 span 计时器保证看门狗先触发触发后的动作中止perRequestAc 抛code: ETIMEDOUT的错误让传输层重试STREAM_RETRY_CONFIG.maxRetries 2直接接管滴灌式流额外streamMaxLifetimeMs默认900000ms15 分钟每个窗口来一个分片的流会无限重置空闲计时器issue #8597需要不被 chunk 刷新的累计上限兜底计时器位置pipeline 内、SDK 原始分片流与processStreamWithLogging之间只有 pipeline 持有能中止 fetch 的控制器5 分钟遥测计时器保留不改动降级为兜底看门狗最终泛化为withStreamGuards由 OpenAI 兼容 pipeline 与 Anthropic 生成器共享同一份实现和调参旋钮issue #9005 finding 4两条链路不再各自漂移。守卫怎么写的一个只盯着上游的 Promise.race配置解析回答生效值从哪来。stream-guards.ts 的resolveStreamGuardMs对两个守卫使用同一套优先级显式程序化配置含 0 表示禁用 环境变量 内置默认值。环境变量只接受严格十进制整数/^\d$/十六进制、科学计数法、浮点、负号一律告警并回退默认——一个拼写错误不该变成一个意想不到的超时。任何超过MAX_STREAM_GUARD_TIMEOUT_MS 2_147_483_6472^31 − 1约 24.8 天的值直接拒绝因为setTimeout会把更大的延迟静默压缩成 1ms看门狗会瞬间触发。竞赛循环回答何时算静默。每一轮对it.next()挂一个计时器Promise.race决定谁赢计时器在每个原始分片含 thinking/reasoning 增量后重置所以持续输出推理过程的长思考模型不会被误杀只有真静默才触发。两个刻意的选择时间基准用performance.now()而非Date.now()NTP 向前跳会杀掉健康生成向后跳会静默禁用上限生命周期上限只累计阻塞在await it.next()上的上游等待时间upstreamMs performance.now() - awaitedAt消费者在yield之后消费多慢都不受罚——上游已完成并缓冲分片的流欠消费者的时间账。result await Promise.race([nextPromise, timeout]);竞赛失败分支里的细节都在回答别引入新的故障形态父信号已中止时抛的是普通Error且name AbortError用户取消优先于看门狗的重新标注普通 Error 能扛住错误脱敏逻辑的Object.create(getPrototypeOf(err))原型克隆DOMException的name是内部槽 getter克隆后会损坏中止后 pending 的 SDKnext()会以 AbortError 拒绝这里显式catch(() {})吞掉避免unhandledRejection告警。计时器一律unref?.()finally 中先abortRequest()释放 socket 再it.return?.()。错误对象回答上层怎么认我。StreamInactivityTimeoutError与StreamLifetimeExceededError都挂readonly code ETIMEDOUT前者的消息里直接写了调参路径provider 托管模型改modelProviders[providerId][].generationConfig.streamIdleTimeoutMsprovider 配置优先于model.generationConfig内置 Qwen OAuth 模型走环境变量QWEN_STREAM_IDLE_TIMEOUT_MS置 0 禁用。接线回答插在哪。pipeline.ts 的executeStream在 Stage 1 拿到 SDK 流之后、进入processStreamWithLogging之前const guarded idleMs 0 || maxLifetimeMs 0 ? withStreamGuards(stream, idleMs, maxLifetimeMs, () perRequestAc.abort(), parentSignal) : stream;两个守卫都 0时透传原始流——withStreamGuards内部也有同样的双关透传保护避免setTimeout(Infinity)被 Node 钳成约 1ms 导致每条流秒死。行为矩阵谁被救回、谁浮出、谁被杀掉场景触发条件最终结果零内容分片停滞事故形态静默满idleMs默认 240000ms中止 fetch抛 ETIMEDOUT → 判{transport, retryable}→ 整包重放重试≤2 次→ 自动恢复耗尽后作为传输错误浮出已有文本、随后停滞静默满idleMs不走整包重放避免重复输出纯文本生成可经 continuation 恢复独立预算maxContinuationRetries 3续写已流式产出functionCall的回合不走续写直接浮出分类后的 ETIMEDOUT滴灌式流每窗口一个分片、永不结束累计上游等待满streamMaxLifetimeMs默认 900000ms空闲计时器被无限重置、不可见由生命周期上限兜底中止上限约束的是单次尝试每次恢复拿到的新流重新计时活跃流含思考流每个分片重置计时器永不触发用户/父信号中止计时器触发时parentSignal.aborted抛 AbortErrorfail-fast 传播不被重标为 ETIMEDOUT双守卫禁用配置或环境变量置 0idleMs 0 maxLifetimeMs 0完全透传看门狗不参与验证路径假定时器与一条会闭嘴的 mock 流机制测试落在 pipeline.test.ts全部使用vi.useFakeTimers()和一个可控的 gated mock 流推入 N 个 chunk 后next()返回永不 resolve 的 Promise用advanceTimersByTimeAsync精确推进虚拟时钟。关键断言静默流推进到idleMs后以code ETIMEDOUT拒绝且底层 mock 流的return()被调用socket 已释放自定义streamIdleTimeoutMs 5000时在 4000ms 处不触发、在 5000ms 处触发——边界卡得很死idle: 0、env 置 0、负值、双 0 等五组禁用用例假时钟推进到远超默认值仍要求流保持挂起不抛错并额外锁定双 0 时流不被中止防止未来把调用方改成永远包装后无人发现setTimeout(Infinity)的钳位风险父信号中止用例断言错误name AbortError且code不是ETIMEDOUT看门狗中止后 SDK 的孤儿next()拒绝不产生unhandledRejection。最易误判的边界有两处其一部分分片后停滞不等于可重放——已交付内容的回合不会整包重发看到它浮出为 ETIMEDOUT 不是看门狗失灵其二用户取消永远优先于看门狗重标超时时刻父信号已中止则看到的一定是 AbortError。落地提醒哪些旋钮能调、哪些不能碰程序化ContentGeneratorConfig.streamIdleTimeoutMs/streamMaxLifetimeMsprovider 配置优先于model.generationConfig 0禁用单个守卫部署QWEN_STREAM_IDLE_TIMEOUT_MS、QWEN_STREAM_MAX_LIFETIME_MS只接受严格十进制毫秒整数不要把值设到2_147_483_647以上——超过即被拒绝回退因为setTimeout钳位会让看门狗近乎立即触发内置 Qwen OAuth 模型无法经 settings 覆盖只能用环境变量或显式配置。判断机制是否在工作的信号日志里出现StreamInactivityTimeoutErrorchunksReceived: 0即事故形态且随后有 RETRY 事件说明看门狗已接管若只看到 5 分钟后才关闭的 span 却无中止动作说明请求走的链路没有经过withStreamGuards。非流式execute()不需要这些守卫——它仍受 120 秒请求级超时约束。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?