Replies: 3 comments
|
llm-pi-ai 插件的讨论是应该发在这里吗? 没找到其他地方可以讨论 |
|
Measurement that narrows this one layer down, in case it helps the fix land in the right place. We fault-injected the real pi-ai 0.84.1 transport (crafted wire responses into the unmodified client + So pi-ai's own retry machinery already does what your report asks for. The exponential-backoff-only behavior you measured on the Scope note: our runs are against the transport seam, not your Volcano Ark gateway specifically — but the TPM shape (429 + Retry-After) is exactly what the injection reproduces. |
|
We hit a related issue from the provider side (free-model tier) and ended up building What we emit on a saturated pool (
If Concrete suggestion: when the openai-completions adapter gets a 429/503 with a
Full write-up in our DSH |
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]
dsh-llm-pi-aiignores Retry-After when retrying — TPM-limited retries always failSummary
When an OpenAI-compatible gateway accessed through
dsh-llm-pi-ai(e.g. Volcano Ark) returns 429 / TPM rate limit, the retry uses the exponential backoff hard-coded indsh-llm-pi-ai(default500ms → 1000ms) and completely ignores theRetry-Afterheader returned by the server.Consequence: while the TPM limit is in effect, the retry interval is far shorter than the time needed for the quota to recover, so every retry necessarily hits 429 again; once the
maxRetriesbudget is exhausted the request stops until a human triggers it again. This is not "missing retries" — it is "retrying at the wrong time and wasting the retry budget."Impact
dsh-llm-pi-ai.dsh-llm-deepseekadapter: the latter correctly readsretry-afterand fills inproviderRetryAfterMs, whiledsh-llm-pi-aidoes not read it.Environment
@deepseek-ai/dsh-llm-pi-ai:0.1.0-rc.6(also present inrc.7, see below)@deepseek-ai/dsh-llm-retry:0.1.0-rc.6@earendil-works/pi-ai:0.82.1https://ark.cn-beijing.volces.com/api/v3, modeldeepseek-v4-flash-ga-260731, TPM limit 1000K/minwebprofileSteps to reproduce
llm-pi-ai) insettings.yaml, with protocolopenai-completionsoropenai-responses, and the model declaringreasoningEfforts(reasoning enabled).ModelAccountTpmRateLimitExceeded).Actual observation (session-log
llm/retryevents){ "retryId": "ab5c6832-c18b-468e-acee-8c8c75516b9f", "provider": "volcano-ark", "mode": "normal", "policyKey": "[\"normal\",2,[\"EMPTY_RESPONSE\",\"RATE_LIMIT\",\"SERVER\",\"TIMEOUT\",\"TRANSPORT\"],500,10000,0.1]", "retry": 1, "maxRetries": 2, "delayMs": 493.78, "failure": { "message": "429: {\"code\":\"ModelAccountTpmRateLimitExceeded\",\"message\":\"TPM (Tokens Per Minute) limit ...\"}", "code": "RATE_LIMIT" } }delayMsis~493ms,~933ms,~1072msin sequence — i.e. exponential backoff frominitialDelayMs=500(500*2^0,500*2^1).failureobject has noproviderRetryAfterMsfield.retry=2/maxRetries=2) the budget is exhausted andturn/endends with aRATE_LIMITerror; sending another message hits 429 again, in a loop.Root-cause analysis
Error propagation chain:
Key code points:
dsh-llm-pi-ai'sprofileOptionshard-codesmaxRetries: 0, disabling pi-ai's internal retry (packages/llm/llm-pi-ai). Yet pi-ai's internal retry (getRetryDelayMsinprovider-retry.js) already honorsretry-after/retry-after-ms— this is the trade-off of handing retry responsibility over to the outerdsh-llm-retry.Headers are lost during error propagation: pi-ai's
stream(e.g.openai-responses.js) doesoutput.errorMessage = formatOpenAIResponsesError(error)in the catch, turning the original error (with headers) into a string;AssistantMessageonly haserrorMessage?: string, no headers. As a resultdsh-llm-pi-aicannot obtainRetry-After, and the outerdsh-llm-retry'sfailure.providerRetryAfterMsis alwaysundefined.Comparison with the official adapter:
dsh-llm-deepseekcorrectly reads and fillsproviderRetryAfterMs(response.headers.get("retry-after"))(seepackages/llm/deepseek). So for the same gateway and the same 429, the official adapter honorsRetry-Afterwhile the pi-ai adapter does not.Expected behavior
On a 429,
dsh-llm-pi-aishould propagate the server'sRetry-After(orretry-after-ms) intofailure.providerRetryAfterMs, so the outerdsh-llm-retrywaits per the server's suggested delay instead of using a fixed exponential backoff.Proposed solutions
Option A: Enable pi-ai's internal retry (change DSH's own package only)
Leverage the fact that pi-ai already honors
Retry-Afterby havingdsh-llm-pi-ai'sprofileOptionsmapprofile.retryPolicy.maxRetriesto pi-ai'smaxRetries, thereby enabling pi-ai's internal retry (getRetryDelayMsinprovider-retry.jsnatively honorsretry-after/retry-after-ms).Pros
packages/llm/llm-pi-ai); no third-party dependency touched.Retry-Afterhandling instead of reimplementing it in DSH.profileOptions).Cons / risks
dsh-llm-retry's configurability and observability:llm/retryevents and UI presentation such as "retried model request (2/2)" would disappear or change.dsh-llm-retryalso has aprofile.retryPolicyconfigured, this can cause double retries (pi-ai inner + dsh-llm-retry outer), requiring a designed responsibility boundary (e.g. inner only waits onRetry-After, outer owns the final budget).Option B: Propagate
providerRetryAfterMs(keep the outer retry's observability)Have pi-ai's
errorevent /AssistantMessagecarryproviderRetryAfterMs(parsed innormalizeProviderErroror thestreamcatch fromerror.headers.get("retry-after")/retry-after-ms), and havedsh-llm-pi-ai'smapStopReasonfill it intofailure.providerRetryAfterMs.Pros
dsh-llm-retry: configurability, observability (llm/retryevents, UI presentation), and cross-provider consistency.providerRetryAfterMs— the path DSH already supports (the officialdsh-llm-deepseekdoes exactly this).Cons / risks
@earendil-works/pi-ai(error propagation / types) plus DSH's owndsh-llm-pi-ai(propagating into the finish failure).AssistantMessage/ error-event types, or finding another way for the DSH layer to reach the original error — more engineering effort.Comparison
Recommendation: if DSH wants to keep the "unified outer retry + observability" architecture, Option B is more correct (consistent with the official adapter), at the cost of changing the third-party pi-ai and contributing upstream; if a minimal change is preferred and moving the retry responsibility into pi-ai is acceptable, Option A is faster. Either way, the fix should not stop at "exponential backoff" without honoring
Retry-After.Additional notes
0.1.0-rc.7does not change this logic (e.g.resolveModelCompat); the issue is still present inrc.7.[Bug] dsh-llm-pi-ai 重试时不遵循服务端
Retry-After,导致 TPM 限流下重试必然失败摘要
使用
dsh-llm-pi-ai接入的 OpenAI 兼容网关(如火山方舟)在遇到 429 / TPM 限流 时,重试使用的是dsh-llm-pi-ai硬编码的指数退避(默认500ms → 1000ms),完全没有遵循服务端返回的Retry-After头。后果:在 TPM 限流持续期间,重试间隔远小于配额恢复所需时间,每次重试必然再次命中 429,
maxRetries预算迅速耗尽后请求停止,直到人工再次触发。这不是"没有重试",而是"重试时机错误、白费重试预算"。影响
dsh-llm-pi-ai使用 OpenAI 兼容网关(尤其是按 TPM 限流的网关,如火山方舟)的用户。dsh-llm-deepseek适配器对比:后者正确读取retry-after并填充providerRetryAfterMs,而dsh-llm-pi-ai不读取。环境
@deepseek-ai/dsh-llm-pi-ai:0.1.0-rc.6(rc.7 同样存在,见下)@deepseek-ai/dsh-llm-retry:0.1.0-rc.6@earendil-works/pi-ai:0.82.1https://ark.cn-beijing.volces.com/api/v3,模型deepseek-v4-flash-ga-260731,TPM 上限 1000K/min复现步骤
settings.yaml配置一个 OpenAI 兼容路由(llm-pi-ai),协议openai-completions或openai-responses,模型声明reasoningEfforts(开启推理)。ModelAccountTpmRateLimitExceeded)。实际观察(会话日志
llm/retry事件){ "retryId": "ab5c6832-c18b-468e-acee-8c8c75516b9f", "provider": "volcano-ark", "mode": "normal", "policyKey": "[\"normal\",2,[\"EMPTY_RESPONSE\",\"RATE_LIMIT\",\"SERVER\",\"TIMEOUT\",\"TRANSPORT\"],500,10000,0.1]", "retry": 1, "maxRetries": 2, "delayMs": 493.78, "failure": { "message": "429: {\"code\":\"ModelAccountTpmRateLimitExceeded\",\"message\":\"TPM (Tokens Per Minute) limit ...\"}", "code": "RATE_LIMIT" } }delayMs依次为~493ms、~933ms、~1072ms—— 即initialDelayMs=500的指数退避(500*2^0、500*2^1)。failure中没有providerRetryAfterMs字段。retry=2/maxRetries=2)耗尽,turn/end以RATE_LIMIT错误结束;人工再发消息又命中 429,循环往复。根因分析
错误传递链:
关键代码点:
dsh-llm-pi-ai的profileOptions硬编码maxRetries: 0,禁用了 pi-ai 的内部重试(packages/llm/llm-pi-ai)。而 pi-ai 的内部重试(provider-retry.js的getRetryDelayMs)本来就会遵循retry-after/retry-after-ms——这是设计上把重试统一交给外层dsh-llm-retry的取舍。错误透传丢失 headers:pi-ai 的
stream(如openai-responses.js)在 catch 里output.errorMessage = formatOpenAIResponsesError(error),把原始错误(含 headers)转成字符串;AssistantMessage只有errorMessage?: string,没有 headers。因此dsh-llm-pi-ai拿不到Retry-After,外层dsh-llm-retry的failure.providerRetryAfterMs恒为undefined。对比官方适配器:
dsh-llm-deepseek的providerRetryAfterMs(response.headers.get("retry-after"))(见packages/llm/deepseek)正确读取并填充。所以同样的网关、同样的 429,官方适配器遵循 Retry-After,pi-ai 适配器不遵循。期望行为
dsh-llm-pi-ai在遇到 429 时,应把服务端Retry-After(或retry-after-ms)透传到failure.providerRetryAfterMs,让外层dsh-llm-retry按服务端建议的延迟等待,而不是用固定指数退避。建议方案
方案 A:启用 pi-ai 内部重试(改动 DSH 自己的包)
利用 pi-ai 已遵循 Retry-After 的能力,让
dsh-llm-pi-ai的profileOptions把profile.retryPolicy.maxRetries映射到 pi-ai 的maxRetries,从而启用 pi-ai 内部重试(provider-retry.js的getRetryDelayMs天然遵循retry-after/retry-after-ms)。优点
packages/llm/llm-pi-ai),不碰第三方依赖。profileOptions一处映射)。缺点 / 风险
dsh-llm-retry的可配置与可观测性:llm/retry事件、UI 上「已重试模型请求 (2/2)」这类呈现会消失或改变。dsh-llm-retry的profile.retryPolicy也配置了重试,可能产生双重重试(pi-ai 内层 + dsh-llm-retry 外层),需要设计好职责边界(例如:内层只在遵循 Retry-After 时等待,外层负责最终预算)。方案 B:透传
providerRetryAfterMs(保留外层重试的可观测性)让 pi-ai 的 error 事件 /
AssistantMessage携带providerRetryAfterMs(在normalizeProviderError或stream的 catch 处从error.headers.get("retry-after")/retry-after-ms解析),由dsh-llm-pi-ai的mapStopReason填充到failure.providerRetryAfterMs。优点
dsh-llm-retry的全部现有能力:可配置、可观测(llm/retry事件、UI 呈现)、跨 provider 一致。providerRetryAfterMs决定等待,是 DSH 已有的、受支持的路径(官方dsh-llm-deepseek正是这么做的)。缺点 / 风险
@earendil-works/pi-ai(error 透传 / 类型)+ DSH 自己的dsh-llm-pi-ai(透传到 finish failure)。AssistantMessage/ error 事件类型,或换一种在 DSH 层能拿到原始错误的方式,工程上更繁琐。对比结论
倾向建议:若 DSH 希望维持「外层统一重试 + 可观测」的架构,方案 B 更正确(与官方适配器一致),代价是要改第三方 pi-ai 并向上游提交;若希望最小改动、接受重试职责前移到 pi-ai,则方案 A 更快。两者都不应只停在「指数退避」而不遵循
Retry-After。补充说明
0.1.0-rc.7中resolveModelCompat等未改动此逻辑,问题在 rc.7 仍存在。All reactions