Replies: 1 comment
|
这个"一条链而不是 15 个 bug"的框架我们用传输层实测数据背书——正好能把链条最底下一层的事实钉死,供 RFC 引用: 第 1 层(pi-ai 传输)不是缺陷所在:故障注入打真 pi-ai 0.84.1(代码未修改):429 + 这组数据支持你的层序主张:既然底层机制齐备,上层三件事(分类器把可重试错误放行、包装层不用自己的常数压掉传输层行为、配置面把 maxRetries/maxRetryDelayMs 露给用户)就是纯"接线"工作,#3128 依赖 #892 先行的顺序判断也与我们在 #3128 贴的测量一致(wrapper 层在覆盖传输层的 Retry-After 行为)。相关的用户侧配置诉求:#2225。 |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
为什么还要开一帖
这不是新的 bug 报告。 8/13 以来至少有 15 帖在描述同一件事:429 或瞬时失败本该自愈却不自愈。据我核对,每一帖都是对的;但全部 ≤3 条回复,无官方回应。
我的主张不同于它们:这不是 15 个 bug,而是一条跨 5 层的缺陷链——而且两个最显然的修复谁先落地,决定一部分用户是变好还是变坏。
行号取自
masterb150a55(dsh-v0.1.1-rc.2,8/21),当天核对。分层与归并
分层不是我发明的,是源码里写明的契约:
llm-pi-ai/src/adapter.ts的profileOptions()写死maxRetries: 0,注释 "The agent recovery layer owns visible attempts; one adapter call is one SDK attempt.";config.ts的rejectRemovedFields()直接拒绝 profile 里的maxRetries/maxRetryDelayMs,报错文案是compose agent recovery with dsh-llm-retry。即:适配器负责暴露信号,策略层负责次数与时长。LlmFailure.coderetryableCodes)llm-retry计数llm/retry事件抬高previousRetry,静默缩小预算retryPolicyRetry-After/ backoffmaxDelayMs,normal直接放弃、always完全忽略llm/retry历史抑制、渲染为 hidden(#3661)daaede29a5,v0.1.1-rc.1)关键含义:修好任何单独一层,用户可见的症状都不变。 这大概就是五个独立帖子都在报「429 不自愈」而没有一个是错的原因。
发现 A:#3128 与 #892 有先后依赖,#3128 单独落地是回归
#3128 的方案二(让 pi-ai 从响应头填
providerRetryAfterMs)方向是对的。但providerRetryAfterMs只在可重试判断与次数判断都通过之后才被读到,而此时策略层这样处理它(llm-retry/src/index.ts:195-206):对一个回
Retry-After: 30的网关(默认策略maxDelayMs: 10_000):return next()正确暴露出来的 header,把两次无用尝试变成了零次尝试。 适配器侧的修复必须晚于(或同步于)策略层对「超预算等待」的处理,否则每个处在限流网关后面的 pi-ai 用户都会退步。
这不是说 #3128 错——#892 提的
maxProviderRetryAfterMs(独立的绝对上限,让maxDelayMs只管本地退避)已经解决了它。缺的是没人指出这个依赖关系,而两帖各 0 回复,没有任何东西阻止它们以错误顺序被采纳。顺带:#3128 的方案一(打开 SDK 原生重试)与上面那份契约冲突——原生重试对
llm/retry事件、取消信号和策略层都是不可见的。所以只剩方案二,顺序问题因此是必须解决的,不是可选的。发现 B:
mode: 'always'是 L3 的自然绕法,而它不安全它是回答 #2225 / #668 / #2941 和 #3338 里那个「长时间停等」诉求的最顺手答案。两个性质似乎没有文档,组合起来大概也非本意:
retryableCodes检查(165)与previousRetry >= maxRetries检查(179)都只在normal路径上。于是AUTH、INVALID_REQUEST、CONTEXT_WINDOW_EXCEEDED会永远重试——而这些错误每次尝试的结果按构造必然相同。always丢弃该指示,改用被maxDelayMs夹住的本地退避——「无界」的模式反而在顶着 provider 的要求敲门,可能延长限流。两个模式在同一输入上朝相反方向失败:
normal等不起 30 秒;always也等不起 30 秒且永不停止。没有一个能表达「按 provider 说的等,但有个合理上限」——而这正是 L3/L4 每一份报告真正想要的。发现 C:L1 不是漏了 case,是设计方向
classifyPiAiError(llm-pi-ai/src/stream.ts:47-58)是一个十分支的文本级联,签名是(message: string)——这条路径上没有 HTTP status 可用,它产出的 failure(115-122 行)也只有message和code,没有status。于是 HTTP 状态码是靠正则从散文里还原的(\b429\b、\b5\d\d\b、\b(?:401|403)\b)。两点后果:
return 'PI_AI_ERROR'是级联的兜底值,而它不在DEFAULT_RETRYABLE_CODES(EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT)里。所以「正则没命中 ⇒ 终态不可重试」。[Bug] `WebSocket error` 被归类为 `PI_AI_ERROR`,导致绕过重试 #530 的WebSocket error、Bug: read tcp 错误被误分类为 PI_AI_ERROR,导致 session 直接终止 #3112 的read tcp绕过重试是这个设计的必然结果,不是漏了某个 case;今后每一种没被命中的新措辞都会重现。if分支加一个兜底的散文级联(master;rc.7 是八个,差别是新增的413),而它的入参只有message: string。所以逐个补 case(pi-ai adapter: third-party "server_error"/overload responses are not classified as SERVER and never retried #3407、[Bug] `WebSocket error` 被归类为 `PI_AI_ERROR`,导致绕过重试 #530、Bug: read tcp 错误被误分类为 PI_AI_ERROR,导致 session 直接终止 #3112、Fix: classify 401/403 bodies by semantics (context overflow / quota) instead of always AUTH #1127 是四次这样的请求)不会收敛——每一种没被正则命中的新措辞,都会落到第 57 行的PI_AI_ERROR。同样的顺序问题也存在于拿得到 status 的那个适配器。
llm-deepseek/src/adapter.ts的httpErrorCode:detail含error.code,所以 OpenAI 形状的 body 里那个"code": "insufficient_quota"会在 496 行命中并返回QUOTA,497 行永远走不到;QUOTA不在默认可重试集合 ⇒ 终态,零重试。这就是 #3338,且该顺序在 master 上至今未改。而insufficient_quota这个词被不同厂商用于两种相反状况——预付费余额烧穿(真终态)与 TPM/RPM 瞬时打满(数十秒自愈)。正则分不开它们;HTTP status 与厂商的code/type枚举可以。附:今天已经可配置的部分
有几帖在申请的能力其实部分已存在,只是 schema 余量远高于默认值:
maxRetries上限是Number.MAX_SAFE_INTEGER;backoff.maxDelayMs上限是MAX_TIMER_DELAY_MS=2_147_483_647ms ≈ 24.8 天。指数增长被
maxDelayMs夹住,所以这是约 20 小时的停等,同时AUTH与CONTEXT_WINDOW_EXCEEDED仍然快速失败——用于此目的严格优于mode: 'always'。把QUOTA加在部署级而非默认集合,也是我在 #3338 里的理由:code 是对响应的事实陈述,它是否值得重试是各部署不同的策略判断。三点保留:这仍是次数预算对时间现象;它对 L1/L2/L4 毫无帮助;计数是对 session 事件流的
findLast扫描且每次尝试追加一条事件,我没有在几百次量级上实测过。建议
maxProviderRetryAfterMs,或把providerRetryAfterMs夹到maxDelayMs后继续重试,而不是在 200 行return next()。maxDelayMs应当只管本地退避,不应充当对 provider 指定时长的拒绝阈值。classifyPiAiError并挂到LlmFailure上;在httpErrorCode里把error.code/error.type枚举与 status 排到散文匹配之前。llm/retry事件配对或补偿,使中断的等待不被计入。always模式要么同样应用retryableCodes,要么在文档里写明它会永久重试终态错误。(此处原有的「重试耗尽的终态错误不应被渲染为 hidden」已由daaede29a5修复、随 v0.1.1-rc.1 发布;L5 保留在分层图中是为了让链条完整。)我没有验证的部分
findLast在maxRetries数百量级下的成本。b150a55;[Bug Report] 429 限流不自动恢复:`dsh-llm-retry` 在网关 `Retry-After` 超过默认 `maxDelayMs`(10s)时直接放弃重试 #892 引用的是 rc.6 编译产物的行号,编号不同,分支相同。Retry-After属部署差异,我未做调查;发现 A 的算术假设它发送,这也是 [Bug] dsh-llm-pi-ai ignores Retry-After when retrying — TPM-limited retries always fail #3128 与 [Bug Report] 429 限流不自动恢复:`dsh-llm-retry` 在网关 `Retry-After` 超过默认 `maxDelayMs`(10s)时直接放弃重试 #892 共同描述的场景。English summary
Not a new bug report. Since Aug 13 at least 15 discussions describe the same domain: a 429 or transient failure that should self-recover and doesn't. As far as I can verify every one of them is correct — and all sit at ≤3 comments with no maintainer reply.
The claim here is different: these are not 15 bugs but one defect chain across five layers, and the landing order of the two most obvious fixes decides whether some users get better or worse. Line numbers are from
masterb150a55(dsh-v0.1.1-rc.2).L1 classify — a prose regex cascade outranks the HTTP status, and its fallback (
PI_AI_ERROR,stream.ts:57) is not inDEFAULT_RETRYABLE_CODES, so anything unmatched is terminal by construction. The cascade is nineifbranches plus a fallback on master (eight on rc.7; the addition is413), its signature is still(message: string), and the emitted failure still has nostatus. So adding one case at a time (#3407, #530, #3112, #1127 are four such requests) cannot converge. On the adapter that does have a status,httpErrorCodestill runsisQuotaExceededError(496) beforestatus === 429(497) — unchanged on master (#3338).L2 budget integrity — orphaned
llm/retryevents inflate the counter (#719).L3 budget shape — a count budget for a time-based phenomenon; defaults are 2 attempts at sub-second backoff (#1504, #2225, #668, #2941, #891).
L4 duration — pi-ai never populates
providerRetryAfterMs(#3128); where it is populated and exceedsmaxDelayMs,normalabandons the step (line 200) andalwaysignores the provider (#892).L5 consequence — the autonomous run stops after exhaustion (#1504). The terminal error was additionally suppressed by the turn's own
llm/retryhistory and rendered hidden (#3661) — fixed indaaede29a5, shipped in v0.1.1-rc.1, so this layer is already repaired on master.Finding A: #3128 alone is a regression. For a pi-ai user on a gateway sending
Retry-After: 30, today's behaviour is 2 blind-backoff attempts; after #3128 lands alone it is 0 attempts, becauseproviderRetryAfterMsis only read after the retryability and count checks pass and 30_000 > the default 10_000 triggersreturn next(). #892'smaxProviderRetryAfterMsalready fixes this — nobody has stated the dependency, and both threads have zero comments. #3128's option 1 (native SDK retry) conflicts with the stated contract (profileOptions()pinsmaxRetries: 0;config.tsrejects adapter-level retry knobs), so option 2 is the only path and the ordering must be handled.Finding B:
mode: 'always'is unsafe as the workaround for L3. It applies neitherretryableCodes(165) normaxRetries(179), soAUTH/INVALID_REQUEST/CONTEXT_WINDOW_EXCEEDEDretry forever; and it discards an over-budgetRetry-After. The two modes fail in opposite directions on the same input, and neither expresses "wait as long as the provider asked, up to a sane ceiling".Also worth knowing:
maxRetriesmaxes atNumber.MAX_SAFE_INTEGERandmaxDelayMsat2_147_483_647ms ≈ 24.8 days, so the "bigger retry budget" asked for in #2225/#668/#2941 and the long park asked for in #3338 are configurable today — just undocumented. See the YAML above.Proposals, unverified items and thread credits are in the Chinese body above; corrections to the layer map are especially welcome from the authors cited.
引用的帖子
dsh-llm-retry在网关Retry-After超过默认maxDelayMs(10s)时直接放弃重试WebSocket error被归类为PI_AI_ERROR,导致绕过重试PI_AI_ERROR,导致 session 直接终止All reactions