mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge commit '70707a46f6868858a0e80ad8bf48ba160403e1eb' into codex/product-subagent-failure-facts-claude
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-24-provider-retry-policies.md
|
||||
2026-07-24-provider-retry-policies.md: 1831ce6b96178d11e7c9927ceccbe07ea578cd2c
|
||||
2026-07-24-provider-retry-policies.zh.md: 22f18badcb47e4ce086ff2c68c7011612b2eabba
|
||||
2026-07-24-provider-retry-policies.md: 96979b219aebece96a1bcc09aa3dd572d2b9222d
|
||||
2026-07-24-provider-retry-policies.zh.md: 02fe13e0ddead035ec750c027e889da08e2557ae
|
||||
|
||||
@@ -12,7 +12,7 @@ Provider policy must follow the request that actually failed, including a route
|
||||
|
||||
## Decision
|
||||
|
||||
Each concrete adapter accepts an optional `retryPolicy` inside its provider configuration. The adapter validates and resolves the policy, and `ctx.llm` captures it when that exact provider route registers. When a call enters its final adapter boundary, `ctx.llm` binds the serving registration's immutable policy to that call; the agent loop passes it to closed-step recovery even if the route is disposed or replaced while the request is in flight. `@deepseek-ai/dsh-llm-retry` combines that call-local policy with the failed step's durable provider identity. A call that never reaches a final adapter has no serving policy and delegates. A provider without `retryPolicy` uses the normal defaults.
|
||||
Each concrete adapter accepts an optional `retryPolicy` inside its provider configuration, validates and resolves it, and exposes that resolved route policy through `providerRetryPolicy()`. Omission selects the shared core normal default of five retries for every composition, including Web, headless, and custom profiles. The effective policy remains route-owned registration state rather than a retry-executor setting. Layered settings may retain normal-only `maxRetries` or `retryableCodes` after changing `mode` to `always`; the resolver ignores those inactive fields while still rejecting unknown keys, and the registered always policy omits them. When a call enters its final adapter boundary, `ctx.llm` binds the serving registration's immutable policy to that call; the agent loop passes it to closed-step recovery even if the route is disposed or replaced while the request is in flight. `@deepseek-ai/dsh-llm-retry` combines that call-local policy with the failed step's durable provider identity. A call that never reaches a final adapter has no serving policy and delegates.
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
@@ -44,22 +44,28 @@ Each scheduled retry appends a non-surface `llm/retry` event with the failed pro
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**One global `always` switch** — rejected because it cannot isolate the unbounded cost and latency risk to the provider that needs it and can silently apply after runtime rerouting.
|
||||
**One retry-executor-level `always` switch** — rejected because it cannot isolate the unbounded cost and latency risk to the provider that needs it and can silently apply after runtime rerouting. Provider route policies remain authoritative, and the effective policy is captured only after routing selects a registration.
|
||||
|
||||
**A separate exact-provider list on `dsh-llm-retry`** — rejected because it duplicates provider route names outside their owning adapter configuration and lets provider registration drift from recovery policy.
|
||||
|
||||
**A very large finite retry count** — rejected because it eventually violates the requested keep-retrying contract and serializes an arbitrary operational limit as if it were meaningful.
|
||||
|
||||
**Adapter-specific omission defaults** — rejected because a shared budget would have to be repeated by every adapter family and every future adapter, making equivalent model routes behave differently depending on their implementation.
|
||||
|
||||
**An LLM deployment-level default** — rejected because it introduces another configuration layer only to make Web differ from other compositions. The product default is uniform, while provider settings retain the existing per-route override.
|
||||
|
||||
**Stamp five retries into profiles when the Web UI writes them** — rejected because existing profiles, settings written outside that UI, and non-Web compositions would retain the old value.
|
||||
|
||||
**Provider-SDK retries** — rejected because hidden attempts multiply agent-level budgets, cannot use the closed-step durability boundary, and may splice or discard streamed output without a reconstructable retry record.
|
||||
|
||||
**Put the error into model context** — rejected because a transport or provider diagnostic is operational state, not conversation content. It can expose sensitive provider details and changes the retried request instead of repeating the failed request.
|
||||
|
||||
## Verification
|
||||
|
||||
Adapter tests validate nested policies at provider load, prove registration captures configured and default policies, and retain the serving policy across in-flight route replacement. Unit tests select policies from the failed request's serving registration, separate provider and changed-policy histories, exercise always mode beyond the normal budget, pin jitter and delay caps, prove downstream recovery ordering, prove cancellation and disposal drain delegated recovery before reaching quiescence, and prove both abort active backoff waits. Request-level coverage compares the complete messages of failed and retried attempts and rejects both provider error text and discarded partial output. A keyless headless `stream-json` snapshot runs failure, retry, and success through the assembled app, pins the complete `llm/retry` record, and rejects any model-message change between attempts. JSONL and SQLite tests round-trip an always event without `Infinity`; invariant tests bind provider identity to the request header, validate failure and mode-specific timer bounds, and bind retry numbers to provider-policy keys; TUI tests render finite and infinite limits.
|
||||
Adapter tests validate nested policies at provider load, prove explicit profile policies reach registration, prove omission resolves to five retries, and retain the serving policy across in-flight route replacement. LLM service tests prove adapter policies are captured and omission uses the shared five-retry behavior. Resolver tests prove always mode ignores retained normal-only fields but returns a pure always policy. Unit tests select policies from the failed request's serving registration, separate provider and changed-policy histories, exercise always mode beyond the normal budget, pin jitter and delay caps, prove downstream recovery ordering, prove cancellation and disposal drain delegated recovery before reaching quiescence, and prove both abort active backoff waits. Request-level coverage compares the complete messages of failed and retried attempts and rejects both provider error text and discarded partial output. A keyless headless `stream-json` snapshot runs failure, retry, and success through the assembled app, pins the complete `llm/retry` record, and rejects any model-message change between attempts. The shipped Web composition snapshot pins omitted DeepSeek and pi-ai policies at five retries, then proves settings can write `{ mode: 'always', maxRetries: 5 }` and obtain a pure always policy. JSONL and SQLite tests round-trip an always event without `Infinity`; invariant tests bind provider identity to the request header, validate failure and mode-specific timer bounds, and bind retry numbers to provider-policy keys; TUI tests render finite and infinite limits.
|
||||
|
||||
## Consequences
|
||||
|
||||
Normal mode remains a finite default, while an explicit always policy can spend unbounded requests and time on permanent authentication, quota, invalid-request, protocol, or context failures. Operators must pair always mode with a cancellable caller and provider-specific cost controls. Retry state stays observable and durable without becoming model-visible, and serving-registration capture prevents adapter lifecycle changes from retroactively changing an in-flight request's recovery contract.
|
||||
Normal mode remains a finite default, while an explicit always policy can spend unbounded requests and time on permanent authentication, quota, invalid-request, protocol, or context failures. Operators must pair always mode with a cancellable caller and provider-specific cost controls. Any model route using omission defaults may spend up to three more requests and their backoff time than under the former two-retry default, in exchange for recovering from longer transient outages. Retry state stays observable and durable without becoming model-visible, and serving-registration capture prevents adapter lifecycle changes from retroactively changing an in-flight request's recovery contract.
|
||||
|
||||
This decision extends the closed-step recovery, single visible adapter attempt, structured failure, and durable status design in [bounded recovery for transient LLM request failures](../architecture/2026-06-21-bounded-llm-request-recovery.md).
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
每个具体适配器都在其提供方配置中接受可选的 `retryPolicy`。适配器负责校验并解析策略,`ctx.llm` 则在该特定提供方路由注册时捕获策略。当调用进入最终适配器边界时,`ctx.llm` 会把实际提供服务的注册项所持不可变策略绑定到该调用;即使路由在请求进行期间被 dispose(资源释放)或替换,agent loop(智能体循环)仍会把该策略传给已关闭步骤恢复。`@deepseek-ai/dsh-llm-retry` 会把绑定到该调用的策略与失败步骤的持久化提供方标识结合起来。未到达最终适配器的调用没有实际提供服务的策略,因而会委托后续处理。未配置 `retryPolicy` 的提供方使用 normal 默认值。
|
||||
每个具体适配器都在其提供方配置中接受可选的 `retryPolicy`,对它进行校验与解析,并通过 `providerRetryPolicy()` 公开解析后的路由策略。省略配置时,Web、headless 与自定义 profile 等所有组合都使用核心共享的 normal 模式五次重试默认值。有效策略仍然是路由拥有的注册状态,而不是重试执行器设置。分层 settings 在把 `mode` 改为 `always` 后可能保留仅属于 normal 的 `maxRetries` 或 `retryableCodes`;解析器会忽略这些未启用字段,同时仍拒绝未知键,注册后的 always 策略也不包含它们。当调用进入最终适配器边界时,`ctx.llm` 会把实际提供服务的注册项所持不可变策略绑定到该调用;即使路由在请求进行期间被 dispose(资源释放)或替换,agent loop(智能体循环)仍会把该策略传给已关闭步骤恢复。`@deepseek-ai/dsh-llm-retry` 会把绑定到该调用的策略与失败步骤的持久化提供方标识结合起来。未到达最终适配器的调用没有实际提供服务的策略,因而会委托后续处理。
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
@@ -44,22 +44,28 @@ always 模式先请求下游恢复,使上下文溢出压缩(compaction)之
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**单一全局 `always` 开关**:不予采纳,因为它无法把无界成本与延迟风险限制在确有需要的提供方,还可能在运行时重新路由后悄然生效。
|
||||
**重试执行器级的单一 `always` 开关**:不予采纳,因为它无法把无界成本与延迟风险限制在确有需要的提供方,还可能在运行时重新路由后悄然生效。提供方路由策略仍然权威,而且只有在路由选定注册后才捕获有效策略。
|
||||
|
||||
**在 `dsh-llm-retry` 上维护单独的指定提供方列表**:不予采纳,因为它会在所属适配器配置之外重复提供方路由名称,并让提供方注册与恢复策略发生偏差。
|
||||
|
||||
**设置很大的有限重试次数**:不予采纳,因为它最终仍会违反持续重试的约定,并把任意选取的运维上限序列化成看似有意义的数值。
|
||||
|
||||
**按适配器设置不同的省略默认值**:不予采纳,因为共享预算必须在每种适配器族以及未来的每个适配器中重复配置,同等模型路由也会因实现不同而表现不同。
|
||||
|
||||
**LLM 部署级默认值**:不予采纳,因为这只为区分 Web 与其他组合增加了一层配置。产品默认值保持统一,提供方 settings 则保留既有的逐路由覆盖能力。
|
||||
|
||||
**在 Web UI 写入 profile 时把五次重试写死进去**:不予采纳,因为现有 profile、从该 UI 之外写入的 settings 以及非 Web 组合仍会保留旧值。
|
||||
|
||||
**使用提供方 SDK 重试**:不予采纳,因为隐藏尝试会叠加 agent 层预算,无法利用已关闭步骤的持久性边界,还可能在没有可重建重试记录的情况下拼接或丢弃流式输出。
|
||||
|
||||
**把错误放入模型上下文**:不予采纳,因为传输或提供方诊断信息属于运维状态,而非对话内容。它可能暴露敏感的提供方细节,并会改变重试请求,无法重复原本失败的请求。
|
||||
|
||||
## 验证
|
||||
|
||||
适配器测试会在提供方加载时校验嵌套策略,证明注册流程会捕获已配置策略和默认策略,并证明请求进行期间替换路由后仍会保留实际提供服务的策略。单元测试根据失败请求实际使用的注册项选择策略、分离不同提供方和策略变更后的重试历史、验证 always 模式可越过 normal 预算、固定抖动和延迟上限、证明下游恢复顺序、证明取消与 dispose 会先排空已委托的恢复再达到完全停稳,并证明二者都会停止正在进行的退避等待。请求级覆盖会比较失败尝试与重试尝试的完整消息,并排除提供方错误文本和丢弃的部分输出。一个无密钥 headless `stream-json` 快照会通过组装后的应用执行失败、重试与成功流程,固定完整的 `llm/retry` 记录,并拒绝各次尝试之间出现任何模型消息变化。JSONL 与 SQLite 测试会往返读写不含 `Infinity` 的 always 事件;不变式测试会将提供方标识绑定到请求头、校验失败事实和各模式的计时器边界,并将重试编号绑定到提供方策略键;TUI 测试会渲染有限和无限上限。
|
||||
适配器测试会在提供方加载时校验嵌套策略,证明显式 profile 策略抵达注册流程,证明省略配置会解析为五次重试,并证明请求进行期间替换路由后仍会保留实际提供服务的策略。LLM 服务测试会证明适配器策略被捕获,且省略配置使用共享的五次重试行为。解析器测试会证明 always 模式忽略残留的 normal 专属字段,但返回纯 always 策略。单元测试根据失败请求实际使用的注册项选择策略、分离不同提供方和策略变更后的重试历史、验证 always 模式可越过 normal 预算、固定抖动和延迟上限、证明下游恢复顺序、证明取消与 dispose 会先排空已委托的恢复再达到完全停稳,并证明二者都会停止正在进行的退避等待。请求级覆盖会比较失败尝试与重试尝试的完整消息,并排除提供方错误文本和丢弃的部分输出。一个无密钥 headless `stream-json` 快照会通过组装后的应用执行失败、重试与成功流程,固定完整的 `llm/retry` 记录,并拒绝各次尝试之间出现任何模型消息变化。随附的 Web 组合快照会把省略配置的 DeepSeek 与 pi-ai 策略固定为五次重试,再证明 settings 可以写入 `{ mode: 'always', maxRetries: 5 }` 并得到纯 always 策略。JSONL 与 SQLite 测试会往返读写不含 `Infinity` 的 always 事件;不变式测试会将提供方标识绑定到请求头、校验失败事实和各模式的计时器边界,并将重试编号绑定到提供方策略键;TUI 测试会渲染有限和无限上限。
|
||||
|
||||
## 后果
|
||||
|
||||
normal 模式仍是有限的默认策略;显式的 always 策略可能在永久性的身份验证、配额、无效请求、协议或上下文错误上耗费无限次请求和无限时间。运维方必须为 always 模式配备可取消的调用方和针对提供方的成本控制。重试状态保持可观察且会持久化,但不会对模型可见;捕获实际提供服务的注册项,也能防止适配器生命周期变化反过来改变进行中请求的恢复约定。
|
||||
normal 模式仍是有限的默认策略;显式的 always 策略可能在永久性的身份验证、配额、无效请求、协议或上下文错误上耗费无限次请求和无限时间。运维方必须为 always 模式配备可取消的调用方和针对提供方的成本控制。任何使用省略默认值的模型路由相比原先的两次重试默认值,最多会多花费三次请求及其退避时间,以此换取从更长短暂故障中恢复的能力。重试状态保持可观察且会持久化,但不会对模型可见;捕获实际提供服务的注册项,也能防止适配器生命周期变化反过来改变进行中请求的恢复约定。
|
||||
|
||||
本决策扩展了[瞬态 LLM(大语言模型)请求失败的有界恢复](../architecture/2026-06-21-bounded-llm-request-recovery.md)中确定的已关闭步骤恢复、单次可见适配器尝试、结构化失败与持久化状态设计。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Boots the shipped Web composition over the built dist this lane already uses
|
||||
// and asserts what that composition produces: the model-visible tool catalog
|
||||
// and file-reference guidance plus the sandbox/approval knobs it ships with.
|
||||
// and file-reference guidance plus its retry, sandbox, and approval defaults.
|
||||
// No browser and no model call — these are composition facts, and the browser
|
||||
// scenarios in this lane cover the surface itself.
|
||||
import { readFileSync } from 'node:fs'
|
||||
@@ -10,6 +10,7 @@ import { afterEach, expect, it } from 'vitest'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
// Empty type imports carry the tools/sandboxPolicy/approval Context merges.
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-sandbox-policy'
|
||||
@@ -73,9 +74,66 @@ afterEach(async () => {
|
||||
scaffold = undefined
|
||||
})
|
||||
|
||||
it('assembles the shipped Web catalog, file-reference guidance, and confined access default', async () => {
|
||||
scaffold = await launchWebScaffold()
|
||||
it('assembles the shipped Web catalog, file-reference guidance, retry policy, and confined access default', async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
|
||||
const ctx = scaffold.ctx
|
||||
expect(ctx.llm.providerRetryPolicy('deepseek-official')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"initialDelayMs": 500,
|
||||
"jitterRatio": 0.1,
|
||||
"maxDelayMs": 10000,
|
||||
"maxRetries": 5,
|
||||
"mode": "normal",
|
||||
"retryableCodes": [
|
||||
"EMPTY_RESPONSE",
|
||||
"RATE_LIMIT",
|
||||
"SERVER",
|
||||
"TIMEOUT",
|
||||
"TRANSPORT",
|
||||
],
|
||||
}
|
||||
`)
|
||||
await ctx.settings.update(settingsNamespace('llm-deepseek'), {
|
||||
retryPolicy: { mode: 'always', maxRetries: 5 },
|
||||
})
|
||||
expect(ctx.llm.providerRetryPolicy('deepseek-official')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"initialDelayMs": 500,
|
||||
"jitterRatio": 0.1,
|
||||
"maxDelayMs": 10000,
|
||||
"mode": "always",
|
||||
}
|
||||
`)
|
||||
await ctx.settings.update(settingsNamespace('llm-pi-ai'), {
|
||||
providers: {
|
||||
openai: {},
|
||||
anthropic: { retryPolicy: { mode: 'always' } },
|
||||
},
|
||||
})
|
||||
expect(ctx.llm.providerRetryPolicy('openai')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"initialDelayMs": 500,
|
||||
"jitterRatio": 0.1,
|
||||
"maxDelayMs": 10000,
|
||||
"maxRetries": 5,
|
||||
"mode": "normal",
|
||||
"retryableCodes": [
|
||||
"EMPTY_RESPONSE",
|
||||
"RATE_LIMIT",
|
||||
"SERVER",
|
||||
"TIMEOUT",
|
||||
"TRANSPORT",
|
||||
],
|
||||
}
|
||||
`)
|
||||
expect(ctx.llm.providerRetryPolicy('anthropic')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"initialDelayMs": 500,
|
||||
"jitterRatio": 0.1,
|
||||
"maxDelayMs": 10000,
|
||||
"mode": "always",
|
||||
}
|
||||
`)
|
||||
// The catalog belongs to an AGENT, not to the process: every model-facing row
|
||||
// now lives in a preset mounted under one session's scope, so the global
|
||||
// layer holds nothing and a caller must name the agent to see anything. This
|
||||
|
||||
@@ -372,7 +372,7 @@ describe('dsh web keyless CLI smoke', () => {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
retry: 1,
|
||||
maxRetries: 2,
|
||||
maxRetries: 5,
|
||||
failure: { code: 'TRANSPORT' },
|
||||
})
|
||||
expect(JSON.stringify(page.events)).toContain('WEB_RETRY_DISCARDED')
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- img
|
||||
- text: Context injection @deepseek-ai/dsh-system-prompt
|
||||
- group:
|
||||
- status: Retried model request (1/2) · {{duration}}
|
||||
- status: Retried model request (1/5) · {{duration}}
|
||||
- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.":
|
||||
- img
|
||||
- img
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||
config-catalog.md: 686e84f7c65624d2b8282785fb88917b7e8a3429
|
||||
config-catalog.zh.md: e0f4809721853ac4ad1f487e703d01e5784e02c7
|
||||
config-catalog.md: a78e0af87f88c617d739ad02ee88c63cdca107c4
|
||||
config-catalog.zh.md: a70e819b58ae015286b8fcf1224f4f59a2c78758
|
||||
|
||||
@@ -870,7 +870,7 @@ export interface Config {
|
||||
models?: DeepSeekCatalogModel[]
|
||||
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
|
||||
streamIdleTimeoutMs?: number
|
||||
/** Provider-owned model-request retry policy; omission uses normal defaults. */
|
||||
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
|
||||
retryPolicy?: RetryPolicyConfig
|
||||
}
|
||||
|
||||
@@ -992,7 +992,7 @@ export interface PiAiProviderProfile {
|
||||
* requests instead of being rejected by a request-size cap.
|
||||
*/
|
||||
maxRequestImageBytes?: number
|
||||
/** Provider-owned model-request retry policy; omission uses normal defaults. */
|
||||
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
|
||||
retryPolicy?: RetryPolicyConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -872,7 +872,7 @@ export interface Config {
|
||||
models?: DeepSeekCatalogModel[]
|
||||
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
|
||||
streamIdleTimeoutMs?: number
|
||||
/** Provider-owned model-request retry policy; omission uses normal defaults. */
|
||||
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
|
||||
retryPolicy?: RetryPolicyConfig
|
||||
}
|
||||
|
||||
@@ -994,7 +994,7 @@ export interface PiAiProviderProfile {
|
||||
* requests instead of being rejected by a request-size cap.
|
||||
*/
|
||||
maxRequestImageBytes?: number
|
||||
/** Provider-owned model-request retry policy; omission uses normal defaults. */
|
||||
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
|
||||
retryPolicy?: RetryPolicyConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md
|
||||
llm-streaming.md: 7c0e0865f8dcc0e7722bb2205d0129d9e0ca3086
|
||||
llm-streaming.zh.md: 5c31909ee79137c6c5eef101235b43a2419b1339
|
||||
llm-streaming.md: c1b2ab5f1e0926864f25409c021691d078df9e0f
|
||||
llm-streaming.zh.md: 7bf04bf7a4a7de63d20eb67461842ff49f3b189d
|
||||
|
||||
@@ -240,7 +240,7 @@ Every adapter MUST obey these, and every consumer may rely on them:
|
||||
|
||||
## `ResolvedRetryPolicy`
|
||||
|
||||
Provider configuration resolves before route registration into an immutable discriminated union. Normal mode carries `mode: 'normal'`, finite `maxRetries`, `retryableCodes`, and required `initialDelayMs`, `maxDelayMs`, and `jitterRatio`; always mode carries `mode: 'always'` and the same required backoff fields without a finite maximum. `LlmRuntime.providerRetryPolicy(provider)` returns the currently registered value and supplies normal defaults when the adapter omits one; `llmRetryPolicyOf(stream)` returns the value captured from the serving registration after the call selects that registration, so later route disposal or replacement cannot change an in-flight failure's recovery policy. The [generated config catalog](../config-catalog.md) lists the optional input fields.
|
||||
Retry configuration resolves before route registration into an immutable discriminated union. Normal mode carries `mode: 'normal'`, finite `maxRetries`, `retryableCodes`, and required `initialDelayMs`, `maxDelayMs`, and `jitterRatio`; always mode carries `mode: 'always'` and the same required backoff fields without a finite maximum. Omitting a provider policy uses the normal default of five retries. Layered settings may retain normal-only `maxRetries` or `retryableCodes` after switching to always mode; the resolver ignores those inactive fields and captures the pure always policy. `LlmRuntime.providerRetryPolicy(provider)` returns the registered value, and `llmRetryPolicyOf(stream)` returns the value captured from the serving registration after the call selects it, so later route disposal or replacement cannot change an in-flight failure's recovery policy. The [generated config catalog](../config-catalog.md) lists the optional input fields.
|
||||
|
||||
## `AppIdentity` — app attribution
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ interface LlmFailure {
|
||||
|
||||
## `ResolvedRetryPolicy`
|
||||
|
||||
提供方配置会在路由注册前解析为不可变的可辨识联合。normal mode 携带 `mode: 'normal'`、有限的 `maxRetries`、`retryableCodes`,以及必填的 `initialDelayMs`、`maxDelayMs` 与 `jitterRatio`;always mode 携带 `mode: 'always'` 和相同的必填退避字段,但没有有限上限。`LlmRuntime.providerRetryPolicy(provider)` 返回当前注册的值,并在适配器省略策略时提供 normal 默认值;调用选定该注册后,`llmRetryPolicyOf(stream)` 返回为该调用服务的注册所捕获的值,因此之后释放或替换路由都无法改变进行中失败的恢复策略。可选配置输入字段由[生成的配置目录](../config-catalog.md)列出。
|
||||
重试配置会在路由注册前解析为不可变的可辨识联合。normal mode 携带 `mode: 'normal'`、有限的 `maxRetries`、`retryableCodes`,以及必填的 `initialDelayMs`、`maxDelayMs` 与 `jitterRatio`;always mode 携带 `mode: 'always'` 和相同的必填退避字段,但没有有限上限。省略提供方策略时使用重试五次的 normal 默认值。分层 settings 在切换到 always 模式后可能保留仅属于 normal 的 `maxRetries` 或 `retryableCodes`;解析器会忽略这些未启用字段,并捕获纯 always 策略。`LlmRuntime.providerRetryPolicy(provider)` 返回注册值;调用选定实际提供服务的注册后,`llmRetryPolicyOf(stream)` 返回从中捕获的值,因此之后释放或替换路由都无法改变进行中失败的恢复策略。可选配置输入字段由[生成的配置目录](../config-catalog.md)列出。
|
||||
|
||||
## `AppIdentity`:应用归属
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/bundle/web-app/README.md
|
||||
README.md: 90d1566b5a7a25f6a827079c4a8ab776e05cc7a3
|
||||
README.zh.md: b7156bcab66b53964bb2f0e1c87d9e9a806e3f75
|
||||
README.md: 28fb5b3dcfc7fbb912493a6b97495e2ed5a3eece
|
||||
README.zh.md: 92157f05497e53c48506666a3a53d638d98a7c9e
|
||||
|
||||
@@ -4,6 +4,10 @@ English | [中文](README.zh.md)
|
||||
|
||||
The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides over [`dsh-base`](../base/README.md): it sets the coding persona, inserts the Web host rows (webserver, API gateway, workspace, projection cache, storage) and the browser plugin roster, the always-on client-plugin reload chain ([`dsh-client-hmr`](../../client/hmr/README.md), idle until a rebuild watcher rewrites client bundles), and mounts this package's `web-runtime` glue plugin (config `{printUrl, surfaceContext, trustedHosts}`). That plugin resolves the built frontend dist through `@deepseek-ai/dsh-web-frontend`'s exports, samples bind-dependent LAN trust once, provides it as `webRuntime` to the browser-trust fence and client roster, mounts the [`frontend-static`](../../host/frontend-static/README.md) fallback owner, registers the harness-source and web-surface prompt sections plus the bash-visible `DSH_WEB_URL` runtime variable when `surfaceContext` is true, and prints the `dsh web:` URL line when `printUrl` is true, after its Loader tree settles so a sibling failure cannot announce a dead app. This bundle also owns the app command line: the ordinary `web-startup` provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), parses `--host`, `--port`, repeatable `--trusted-host`, and the app's `--help`, then provides `webStartup`. It rejects `--host 0.0.0.0` before publishing that service because the CLI intentionally does not support all-interfaces binding yet. Flag-configured rows inject the service and read it directly from lazy config, so nothing binds a port before argument resolution and `dsh --profile web --help` starts no server. [`dsh-headless`](../headless/README.md) is a sibling surface over the same base and does not mount this bundle.
|
||||
|
||||
## Model retry defaults
|
||||
|
||||
Web uses the shared bounded normal default of five eligible retries after the initial request. The `deepseek-official` route and settings-added pi-ai routes use that default when they omit `retryPolicy`; explicit provider policies still win. Web adds no retry-specific composition override, so the same omission behavior applies to non-Web profiles.
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Harness-source and Web-surface context
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 [`dsh-base`](../base/README.md) 之上:设置 coding persona,插入 Web 宿主行(webserver、API 网关、workspace、投影缓存、存储)、浏览器插件名录与始终挂载的客户端插件重载链([`dsh-client-hmr`](../../client/hmr/README.md),在重建 watcher 改写客户端 bundle 之前保持空闲),并挂载本包的 `web-runtime` 粘合插件(配置为 `{printUrl, surfaceContext, trustedHosts}`)。该插件通过 `@deepseek-ai/dsh-web-frontend` 的 exports 解析已构建的前端 dist,只采样一次依赖 bind 的 LAN 信任信息并将其作为 `webRuntime` 提供给浏览器信任栅栏和客户端名录,挂载 [`frontend-static`](../../host/frontend-static/README.md) 回退席位所有者,在 `surfaceContext` 为 true 时注册 Harness 源码与 Web 表层提示词段落,以及 bash 可见的 `DSH_WEB_URL` 运行时变量,并在 `printUrl` 为 true 时等自身的 Loader 配置树结算后再打印 `dsh web:` URL 行,避免兄弟行失败时公告一个已失效的应用。本组合包还持有应用命令行:普通 `web-startup` 提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.md)),解析 `--host`、`--port`、可重复的 `--trusted-host` 以及应用自己的 `--help`,再提供 `webStartup`。它会在发布该服务前拒绝 `--host 0.0.0.0`,因为 CLI 目前有意不支持绑定所有网络接口。由 flag 配置的行会注入该服务,并在惰性配置中直接读取它,因此参数解析完成前不会有任何东西绑定端口,`dsh --profile web --help` 也不会启动服务器。[`dsh-headless`](../headless/README.md) 是同一 base 之上的同级表层,不挂载本组合包。
|
||||
|
||||
## 模型重试默认值
|
||||
|
||||
Web 使用共享的有界 normal 默认值,在首次请求后最多再重试五次符合条件的失败。`deepseek-official` 与由 settings 新增的 pi-ai 路由在省略 `retryPolicy` 时使用该默认值;显式提供方策略仍然优先。Web 不再增加重试专用的组合覆盖,因此非 Web profile 的省略行为与之相同。
|
||||
|
||||
## 模型体验
|
||||
|
||||
### Harness 源码与 Web 表层上下文
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
|
||||
README.md: af89e7ee3bab6ec209349d047f81308eb6e87cef
|
||||
README.zh.md: e9ce3206027ffeee9bc49eb7a2ed76ddcbe7bfc8
|
||||
README.md: 9bb28e6876b82c521341769123a8b2d0e5d98e09
|
||||
README.zh.md: 21c55cfa32bf68e0cac4c0bd72c94c86d955fc00
|
||||
|
||||
@@ -20,7 +20,7 @@ The package root exposes the Cordis plugin contract and `DeepSeekAdapter`; wire
|
||||
reasoningEffort: high # optional; off | low | high | max — omitted ⇒ high
|
||||
maxTokens: 256000 # optional positive per-request output cap; this is the default
|
||||
streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default
|
||||
retryPolicy: # optional; omission uses bounded normal defaults
|
||||
retryPolicy: # optional; omission uses normal mode with five retries
|
||||
mode: always # normal | always
|
||||
backoff:
|
||||
initialDelayMs: 500
|
||||
@@ -35,7 +35,7 @@ The package root exposes the Cordis plugin contract and `DeepSeekAdapter`; wire
|
||||
contextWindow: 512000
|
||||
```
|
||||
|
||||
The plugin registers the single provider route `deepseek-official` together with its resolved `retryPolicy`. A request selects it with `provider: deepseek-official`; its `model` is passed through as the wire `model` string, so changing DeepSeek models does not require lifecycle-time registration. Omitting `models` advertises `deepseek-v4-flash` as `DeepSeek-V4-Flash` and `deepseek-v4-pro` as `DeepSeek-V4-Pro`, each with a 1,000,000-token context window; an explicit list replaces those defaults, while `models: []` advertises none. Catalog entries are exposed through `ctx.llm.listModels('deepseek-official')` for clients such as ACP editors and the Web selector, but remain advisory: unlisted model ids still pass through unchanged. An omitted entry name defaults to its id.
|
||||
The plugin registers the single provider route `deepseek-official` together with its resolved `retryPolicy`; omission resolves to normal mode with five retries. A request selects it with `provider: deepseek-official`; its `model` is passed through as the wire `model` string, so changing DeepSeek models does not require lifecycle-time registration. Omitting `models` advertises `deepseek-v4-flash` as `DeepSeek-V4-Flash` and `deepseek-v4-pro` as `DeepSeek-V4-Pro`, each with a 1,000,000-token context window; an explicit list replaces those defaults, while `models: []` advertises none. Catalog entries are exposed through `ctx.llm.listModels('deepseek-official')` for clients such as ACP editors and the Web selector, but remain advisory: unlisted model ids still pass through unchanged. An omitted entry name defaults to its id.
|
||||
|
||||
`contextWindow` is optional per configured model and is not exposed through the advisory catalog. `ctx.llm.resolveModelInfo('deepseek-official', model).context` returns an exact model value first, then `defaultContextWindow` for an entry without capacity or an unlisted pass-through id. The adapter default is 1,000,000; pressure-sensitive plugins therefore get deployment-owned capacity without treating the model selector as authoritative. Registering another adapter for `deepseek-official` throws `LlmError('DUPLICATE_ADAPTER')`.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
|
||||
reasoningEffort: high # optional; off | low | high | max — omitted ⇒ high
|
||||
maxTokens: 256000 # optional positive per-request output cap; this is the default
|
||||
streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default
|
||||
retryPolicy: # optional; omission uses bounded normal defaults
|
||||
retryPolicy: # optional; omission uses normal mode with five retries
|
||||
mode: always # normal | always
|
||||
backoff:
|
||||
initialDelayMs: 500
|
||||
@@ -35,7 +35,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
|
||||
contextWindow: 512000
|
||||
```
|
||||
|
||||
该插件注册唯一提供方路由 `deepseek-official`,同时注册解析后的 `retryPolicy`。请求使用 `provider: deepseek-official` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布 `deepseek-v4-flash`(名称为 `DeepSeek-V4-Flash`)和 `deepseek-v4-pro`(名称为 `DeepSeek-V4-Pro`),两者的上下文窗口均为 1,000,000 token;显式列表会替换这些默认值,`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek-official')` 公开给 ACP(Agent Client Protocol)编辑器和 Web 选择器等客户端,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id。
|
||||
该插件注册唯一提供方路由 `deepseek-official`,并一同注册解析后的 `retryPolicy`;省略时会解析为 normal 模式并重试五次。请求使用 `provider: deepseek-official` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布 `deepseek-v4-flash`(名称为 `DeepSeek-V4-Flash`)和 `deepseek-v4-pro`(名称为 `DeepSeek-V4-Pro`),两者的上下文窗口均为 1,000,000 token;显式列表会替换这些默认值,`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek-official')` 公开给 ACP(Agent Client Protocol)编辑器和 Web 选择器等客户端,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id。
|
||||
|
||||
`contextWindow` 对每个已配置模型都可选,不会通过建议 catalog 公开。`ctx.llm.resolveModelInfo('deepseek-official', model).context` 先返回精确模型值,再对不含容量的配置项或未列出原样传递 id 返回 `defaultContextWindow`。适配器默认值为 1,000,000;因此,压力敏感插件可以获得由部署决定的容量,不会将模型 selector 视为权威。为 `deepseek-official` 注册另一个适配器会抛出 `LlmError('DUPLICATE_ADAPTER')`。
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export interface Config {
|
||||
models?: DeepSeekCatalogModel[]
|
||||
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
|
||||
streamIdleTimeoutMs?: number
|
||||
/** Provider-owned model-request retry policy; omission uses normal defaults. */
|
||||
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
|
||||
retryPolicy?: RetryPolicyConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md
|
||||
README.md: 5dbcb905451f72a700dd09b4052dcb2f88e858c9
|
||||
README.zh.md: 217244c7b4d7ecd5aa88427990feb58c96e5acaf
|
||||
README.md: f696b6bee50b844bfbc6bab7f9def0e785d450c9
|
||||
README.zh.md: cace9d1fdfd85a559674b121b79b3dbb86b4337b
|
||||
|
||||
@@ -8,7 +8,7 @@ The package root exposes the Cordis plugin contract, `PiAiAdapter`, and `support
|
||||
|
||||
## Config
|
||||
|
||||
Configure credentials, the model catalog, and deployment-specific transport settings per provider, keyed by the provider route itself. `apiKeyEnv` is a credential *reference* resolved per request, so no secret enters this file. Omitting it leaves the route unauthenticated, which for an installed catalog route means pi-ai's provider-native ambient discovery; a configured reference that resolves to nothing fails the request with `MISSING_CREDENTIAL` instead, because falling through would authenticate with whatever unrelated key the environment happens to hold. One credential serves every model on its route.
|
||||
Configure credentials, the model catalog, and deployment-specific transport settings per provider, keyed by the provider route itself. Each profile may set a `retryPolicy`; omission uses normal mode with five retries. `apiKeyEnv` is a credential *reference* resolved per request, so no secret enters this file. Omitting it leaves the route unauthenticated, which for an installed catalog route means pi-ai's provider-native ambient discovery; a configured reference that resolves to nothing fails the request with `MISSING_CREDENTIAL` instead, because falling through would authenticate with whatever unrelated key the environment happens to hold. One credential serves every model on its route.
|
||||
|
||||
```yaml
|
||||
- id: llm
|
||||
@@ -113,7 +113,7 @@ A model that carries reasoning metadata — from the installed catalog or from i
|
||||
|
||||
A model **without** that metadata — a hand-declared one whose entry declares no `reasoningEfforts`, and a catalog model pi-ai marks as non-reasoning — exposes no `reasoning` at all. pi-ai reports such a model as supporting the single level `off`, but `off` is translated to *omitting* the reasoning option, which is byte-for-byte the request that naming no effort already produces: selecting it could not disable anything, so a provider whose own default is to think would keep thinking with `off` shown as selected. Reporting the capability as unavailable leaves a surface offering the provider's default and nothing that misrepresents it. The profile `reasoning` value, including `off`, is the deployment default when configured; omitting it preserves the provider default. Per-request `GenerateOptions.reasoningEffort` takes precedence, and a level absent from the exact model capability fails the REQUEST with `UNSUPPORTED_REASONING_EFFORT` before network I/O instead of being clamped. Describing a model never fails that way: the models under one provider disagree about which levels they accept, so `resolveModel` reports a profile level the exact model cannot take as no default at all rather than throwing. A throw there would take the whole provider out of every model catalog built over it — one mis-set profile field hiding even the models that do support the level — so a bad configuration surfaces where it is acted on, not where it is described. pi-ai's common stream options represent `off` by omitting `reasoning`.
|
||||
|
||||
Supported profile fields are `apiKeyEnv`, `displayName`, `api`, `baseURL`, `models`, `modelOverrides`, `compat`, `defaultContextWindow`, `defaultMaxTokens`, `defaultInput`, `headers`, `reasoning`, `thinkingBudgets`, `cacheRetention`, `transport`, `timeoutMs`, `websocketConnectTimeoutMs`, `streamIdleTimeoutMs`, `maxRequestImageBytes`, and `retryPolicy`. Each profile's optional retry policy is captured with that provider route; omission uses bounded normal defaults. The stream-idle interval is a positive finite Node timer delay, defaults to five minutes, and covers only an outstanding provider read, not consumer think time. `maxRequestImageBytes` bounds one request's base64-encoded image payload (default 20MiB, a positive integer): every image in history is re-encoded into every request, so when the accumulated payload exceeds the bound, the oldest images are replaced by a fixed text placeholder until the request fits, keeping an image-heavy session serviceable instead of permanently rejected by a gateway request-size cap. The default leaves capacity for system prompts, history, tools, and JSON; deployments behind stricter gateways lower it per route. Harness app attribution wins a conflicting configured header name.
|
||||
Supported profile fields are `apiKeyEnv`, `displayName`, `api`, `baseURL`, `models`, `modelOverrides`, `compat`, `defaultContextWindow`, `defaultMaxTokens`, `defaultInput`, `headers`, `reasoning`, `thinkingBudgets`, `cacheRetention`, `transport`, `timeoutMs`, `websocketConnectTimeoutMs`, `streamIdleTimeoutMs`, `maxRequestImageBytes`, and `retryPolicy`. Each resolved profile retry policy is captured with that provider route; omission uses the shared bounded normal default of five retries. The stream-idle interval is a positive finite Node timer delay, defaults to five minutes, and covers only an outstanding provider read, not consumer think time. `maxRequestImageBytes` bounds one request's base64-encoded image payload (default 20MiB, a positive integer): every image in history is re-encoded into every request, so when the accumulated payload exceeds the bound, the oldest images are replaced by a fixed text placeholder until the request fits, keeping an image-heavy session serviceable instead of permanently rejected by a gateway request-size cap. The default leaves capacity for system prompts, history, tools, and JSON; deployments behind stricter gateways lower it per route. Harness app attribution wins a conflicting configured header name.
|
||||
|
||||
The adapter forces pi-ai's SDK `maxRetries` to zero so one `stream()` call makes one provider request. The removed profile fields `maxRetries` and `maxRetryDelayMs` fail load instead of silently multiplying or hiding the separately composed agent-level retry budget. Idle expiry aborts the SDK's stable request signal and surfaces `TIMEOUT`; an earlier caller abort remains `ABORTED`.
|
||||
|
||||
@@ -202,4 +202,4 @@ Recorded response content appends to the next request and does not invalidate it
|
||||
- **`GenerateOptions.stop` is unsupported** — pi-ai's common stream options cannot guarantee stop-sequence behavior across providers, so the adapter rejects the field.
|
||||
- **In-history `system` messages use pi-ai's common context conversion** — provider-specific placement follows pi-ai rather than a harness-owned wire override.
|
||||
- **Provider HTTP status is unavailable** — pi-ai error events do not expose a stable HTTP status across providers; failures expose only stable harness error codes.
|
||||
- **Retry policy is provider-owned, not an SDK retry** — each provider profile may configure nested `retryPolicy`, which `dsh-llm-retry` executes at the agent failed-step extension point; pi-ai SDK retries stay disabled so durable agent steps and `llm/retry` events own every visible attempt, and direct `ctx.llm.stream()` calls remain single-attempt.
|
||||
- **Retry policy is provider-owned, not an SDK retry** — each provider profile may supply nested `retryPolicy`; omission resolves to normal mode with five retries, and the effective route policy is what `dsh-llm-retry` executes at the agent failed-step extension point. pi-ai SDK retries stay disabled so durable agent steps and `llm/retry` events own every visible attempt, and direct `ctx.llm.stream()` calls remain single-attempt.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 配置
|
||||
|
||||
按提供方配置凭据、模型 catalog 与部署特定传输设置,并以提供方路由本身为键。`apiKeyEnv` 是按请求解析的凭据*引用*,因此机密不进入该文件。省略它会让该路由处于未认证状态;对已安装 catalog 路由而言,这意味着交给 pi-ai 的提供方原生环境发现。已配置却解析不出任何值的引用则相反,会让请求以 `MISSING_CREDENTIAL` 失败,因为放行下去就会用环境里恰好持有的某个无关密钥完成认证。一条凭据服务该路由下的全部模型。
|
||||
按提供方配置凭据、模型 catalog 与部署特定传输设置,并以提供方路由本身为键。每个 profile 都可以设置 `retryPolicy`;省略时使用 normal 模式并重试五次。`apiKeyEnv` 是按请求解析的凭据*引用*,因此机密不进入该文件。省略它会让该路由处于未认证状态;对已安装 catalog 路由而言,这意味着交给 pi-ai 的提供方原生环境发现。已配置却解析不出任何值的引用则相反,会让请求以 `MISSING_CREDENTIAL` 失败,因为放行下去就会用环境里恰好持有的某个无关密钥完成认证。一条凭据服务该路由下的全部模型。
|
||||
|
||||
```yaml
|
||||
- id: llm
|
||||
@@ -114,7 +114,7 @@ profile 的 `models` 列表是*替换*该路由已安装 catalog,而不是扩
|
||||
|
||||
**没有**这份元数据的模型——条目未声明 `reasoningEfforts` 的手工声明模型,以及 pi-ai 标记为不具备推理能力的 catalog 模型——完全不公开 `reasoning`。pi-ai 会把这类模型报告为只支持 `off` 一档,但 `off` 会被翻译成*省略* reasoning 选项,而那与「不点名任何档位」产出的请求逐字节相同:选它关不掉任何东西,于是自身默认就在思考的提供方,会在界面显示 `off` 被选中的同时继续思考。把该能力报告为不可用,界面就只剩提供方默认这一项,不会再出现自相矛盾的控件。配置 profile 的 `reasoning` 值(包括 `off`)在存在时是部署默认值;省略它会保留提供方默认值。每次请求的 `GenerateOptions.reasoningEffort` 优先;未出现在确切模型能力中的档位会让**请求**在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败,而不会被自动调整。**描述**一个模型则从不这样失败:同一提供方下各模型接受的档位并不一致,因此 `resolveModel` 对该模型拿不下的 profile 档位报告为「没有默认值」,而不是抛错。在那里抛错会让整个提供方从任何基于它构建的模型目录中消失——一个配错的 profile 字段连支持该档位的模型也一并藏起来——所以坏配置暴露在被执行处,而不是被描述处。pi-ai 的通用流选项通过省略 `reasoning` 表示 `off`。
|
||||
|
||||
受支持的 profile 字段是 `apiKeyEnv`、`displayName`、`api`、`baseURL`、`models`、`modelOverrides`、`compat`、`defaultContextWindow`、`defaultMaxTokens`、`defaultInput`、`headers`、`reasoning`、`thinkingBudgets`、`cacheRetention`、`transport`、`timeoutMs`、`websocketConnectTimeoutMs`、`streamIdleTimeoutMs`、`maxRequestImageBytes` 和 `retryPolicy`。每个 profile 的可选重试策略都会与该提供方路由一同捕获;省略时使用有界的常规默认值。流空闲间隔必须是正的有限 Node 定时器延迟,默认为五分钟,且只覆盖未完成提供方读取,不包括消费方思考时间。`maxRequestImageBytes` 约束单个请求的 base64 编码图片载荷(默认 20MiB,正整数):历史中的每张图片都会重新编码进每个请求,累积载荷超过上限时,从最老的图片开始替换为固定文本占位,直到请求装得下,使图片较多的会话保持可用,而不是被网关请求体上限永久拒绝。默认值为系统提示词、历史、工具与 JSON 保留请求容量;网关更严格的部署按路由调低该值。若已配置标头中有同名项,则以 Harness 应用归因为准。
|
||||
受支持的 profile 字段是 `apiKeyEnv`、`displayName`、`api`、`baseURL`、`models`、`modelOverrides`、`compat`、`defaultContextWindow`、`defaultMaxTokens`、`defaultInput`、`headers`、`reasoning`、`thinkingBudgets`、`cacheRetention`、`transport`、`timeoutMs`、`websocketConnectTimeoutMs`、`streamIdleTimeoutMs`、`maxRequestImageBytes` 和 `retryPolicy`。每条 profile 解析后的重试策略会随该提供方路由一同捕获;省略时使用共享的有界 normal 默认值并重试五次。流空闲间隔必须是正的有限 Node 定时器延迟,默认为五分钟,且只覆盖未完成提供方读取,不包括消费方思考时间。`maxRequestImageBytes` 约束单个请求的 base64 编码图片载荷(默认 20MiB,正整数):历史中的每张图片都会重新编码进每个请求,累积载荷超过上限时,从最老的图片开始替换为固定文本占位,直到请求装得下,使图片较多的会话保持可用,而不是被网关请求体上限永久拒绝。默认值为系统提示词、历史、工具与 JSON 保留请求容量;网关更严格的部署按路由调低该值。若已配置标头中有同名项,则以 Harness 应用归因为准。
|
||||
|
||||
适配器强制 pi-ai SDK `maxRetries` 为零,因此一次 `stream()` 调用只会发起一次提供方请求。已移除 profile 字段 `maxRetries` 和 `maxRetryDelayMs` 会使加载失败,而不是静默倍增或隐藏单独组合的 agent(智能体)级重试预算。空闲超时会 abort SDK 的稳定请求信号,并以 `TIMEOUT` 呈现;较早的调用方 abort 仍为 `ABORTED`。
|
||||
|
||||
@@ -203,4 +203,4 @@ pi-ai 事件会变为 harness 推理、文本、工具调用、usage 与 finish
|
||||
- **不支持 `GenerateOptions.stop`**:pi-ai 的通用流选项无法保证所有提供方都支持 stop sequence,因此适配器会拒绝该字段。
|
||||
- **历史中的 `system` 消息使用 pi-ai 通用上下文转换**:提供方特定位置由 pi-ai 决定,而非由 harness 拥有的协议覆盖决定。
|
||||
- **无法获取提供方 HTTP 状态**:pi-ai 错误事件不会在所有提供方上公开稳定 HTTP 状态;失败只公开稳定 harness 错误 code。
|
||||
- **重试策略由提供方持有,而不是 SDK 重试**:每个提供方 profile 都可以配置嵌套的 `retryPolicy`,由 `dsh-llm-retry` 在 agent 的失败步骤扩展点上执行;pi-ai SDK 重试仍保持禁用,因此持久化的 agent 步骤与 `llm/retry` 事件记录每次可见尝试,直接 `ctx.llm.stream()` 调用仍只尝试一次。
|
||||
- **重试策略由提供方持有,而不是 SDK 重试**:每个提供方 profile 都可以提供嵌套的 `retryPolicy`;省略时解析为 normal 模式并重试五次,`dsh-llm-retry` 会在 agent 的失败步骤扩展点上执行有效路由策略。pi-ai SDK 重试仍保持禁用,因此持久化的 agent 步骤与 `llm/retry` 事件记录每次可见尝试,直接 `ctx.llm.stream()` 调用仍只尝试一次。
|
||||
|
||||
@@ -154,7 +154,7 @@ export interface PiAiProviderProfile {
|
||||
* requests instead of being rejected by a request-size cap.
|
||||
*/
|
||||
maxRequestImageBytes?: number
|
||||
/** Provider-owned model-request retry policy; omission uses normal defaults. */
|
||||
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
|
||||
retryPolicy?: RetryPolicyConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -392,7 +392,7 @@ describe('provider profile lifecycle', () => {
|
||||
})
|
||||
expect(ctx.llm.providerRetryPolicy('anthropic')).toMatchObject({
|
||||
mode: 'normal',
|
||||
maxRetries: 2,
|
||||
maxRetries: 5,
|
||||
})
|
||||
await fiber.dispose()
|
||||
expect(ctx.llm.listProviders()).toEqual([])
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm-retry/README.md
|
||||
README.md: 0a907b5505650f7c2cd5e9933750be3701f8e34b
|
||||
README.zh.md: 024545e1ed52bb8d4aeac8114ffafc26a04a2ddb
|
||||
README.md: 1dd530051e387230510414e35701db10c7c7c063
|
||||
README.zh.md: 52badf6a6e7d2c2571fa951865f6b4b3f08ae4ad
|
||||
|
||||
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
Function plugin that applies exact-provider retry policy through the agent loop's closed-step `agent/request-error` waterfall. It does not wrap `ctx.llm.stream()`: every adapter call remains one provider attempt, and every retry opens a fresh numbered turn.
|
||||
|
||||
Each provider adapter owns an optional nested `retryPolicy`, captured when its route registers on `ctx.llm` and carried with each call that reaches that registration's final adapter boundary. An in-flight failure retains that serving policy if the route is later disposed or replaced; a failure before any final adapter is selected has no provider policy and delegates. Omission uses normal mode: two retries for `EMPTY_RESPONSE`, `RATE_LIMIT`, `SERVER`, `TIMEOUT`, and `TRANSPORT`, with bounded exponential backoff from 500 ms to 10 seconds and 10 percent jitter. `EMPTY_RESPONSE` is the adapters' classification of a degenerate provider completion that produced no durable content, so repeating it is safe. A normal policy can change its finite budget, eligible codes, and backoff. Always mode asks downstream recovery first, then retries every model-request failure without an attempt limit; success, cancellation, or plugin disposal stops it after active delegated recovery reaches quiescence.
|
||||
Each provider adapter owns an optional nested `retryPolicy`, captured when its route registers on `ctx.llm` and carried with each call that reaches that registration's final adapter boundary. An in-flight failure retains that serving policy if the route is later disposed or replaced; a failure before any final adapter is selected has no provider policy and delegates. Omission uses normal mode: five retries for `EMPTY_RESPONSE`, `RATE_LIMIT`, `SERVER`, `TIMEOUT`, and `TRANSPORT`, with bounded exponential backoff from 500 ms to 10 seconds and 10 percent jitter. `EMPTY_RESPONSE` is the adapters' classification of a degenerate provider completion that produced no durable content, so repeating it is safe. A normal policy can change its finite budget, eligible codes, and backoff. Always mode asks downstream recovery first, then retries every model-request failure without an attempt limit; success, cancellation, or plugin disposal stops it after active delegated recovery reaches quiescence.
|
||||
|
||||
Both modes use bounded exponential backoff with symmetric jitter. A valid `providerRetryAfterMs` at or below `maxDelayMs` replaces local backoff without jitter. An over-cap provider delay makes normal mode delegate, while always mode uses its configured local backoff so it cannot terminate on that instruction.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
一个函数插件,通过 agent loop(智能体循环)在已关闭步骤上触发的 `agent/request-error` waterfall(瀑布式事件)应用确切提供方重试策略。它不包装 `ctx.llm.stream()`:每次适配器调用仍是一次提供方尝试,每次重试都会开启新的编号轮次。
|
||||
|
||||
每个提供方适配器都拥有可选的嵌套 `retryPolicy`;路由在 `ctx.llm` 上注册时会捕获该策略,任何到达该注册最终适配器边界的调用都会携带它。如果之后释放或替换路由,进行中的失败仍会保留当时为其提供服务的策略;在选中任何最终适配器前发生的失败没有提供方策略,会继续委托。省略策略时使用 normal mode:为 `EMPTY_RESPONSE`、`RATE_LIMIT`、`SERVER`、`TIMEOUT` 和 `TRANSPORT` 重试两次,并采用从 500 ms 到 10 秒的有界指数退避与 10% jitter。`EMPTY_RESPONSE` 是适配器对未产生任何持久内容的退化提供方完成所作的分类,因此可安全重复。normal 策略可以更改其有限预算、符合条件的 code 和退避配置。always mode 会先请求下游恢复,再无次数上限地重试每个模型请求失败;成功、取消或插件 dispose(资源释放)会在活跃的委托恢复完全停稳后终止它。
|
||||
每个提供方适配器都拥有可选的嵌套 `retryPolicy`;路由在 `ctx.llm` 上注册时会捕获该策略,任何到达该注册最终适配器边界的调用都会携带它。如果之后释放或替换路由,进行中的失败仍会保留当时为其提供服务的策略;在选中任何最终适配器前发生的失败没有提供方策略,会继续委托。省略策略时使用 normal mode:为 `EMPTY_RESPONSE`、`RATE_LIMIT`、`SERVER`、`TIMEOUT` 和 `TRANSPORT` 重试五次,并采用从 500 ms 到 10 秒的有界指数退避与 10% jitter。`EMPTY_RESPONSE` 是适配器对未产生任何持久内容的退化提供方完成所作的分类,因此可安全重复。normal 策略可以更改其有限预算、符合条件的 code 和退避配置。always mode 会先请求下游恢复,再无次数上限地重试每个模型请求失败;成功、取消或插件 dispose(资源释放)会在活跃的委托恢复完全停稳后终止它。
|
||||
|
||||
两种 mode 都使用带对称 jitter 的有界指数退避。有效 `providerRetryAfterMs` 不超过 `maxDelayMs` 时会替换本地退避,并且不加 jitter。超出上限的提供方延迟会使 normal mode 继续委托;always mode 则改用已配置的本地退避,避免该指令终止重试。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm/README.md
|
||||
README.md: fb6bd84240b41dd730d45b3eb34c35827dc4c991
|
||||
README.zh.md: 5c22767a7c654972cbf614505382fa4755d7d318
|
||||
README.md: e6b3c4924ad4e7cf115abdbfb38d22d0f524e377
|
||||
README.zh.md: 51f034af632c19ca91c1d2f91a7c9c22714870e0
|
||||
|
||||
@@ -8,6 +8,10 @@ Provider-neutral LLM vocabulary and abstract service. This package defines the c
|
||||
|
||||
An adapter registry plus a single streaming call API, interceptable via a waterfall event.
|
||||
|
||||
### Retry policy
|
||||
|
||||
Each provider adapter supplies its resolved route policy. Omitting provider configuration uses bounded normal mode with five retries after the first request. Layered configuration may retain `maxRetries` or `retryableCodes` after changing `mode` to `always`; resolution ignores those inactive normal-mode fields and captures a pure always policy. This service stores the effective policy but does not execute retries.
|
||||
|
||||
### Public API
|
||||
|
||||
- `ctx.llm.registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle` Register one adapter instance for the given provider routes. Registration is all-or-nothing, and is disposed with the calling fiber. The returned disposer also carries `replace(providers)`: the candidate route set is validated in full before anything moves, so a conflict with another adapter leaves the current routes registered and serving, and the swap itself is one synchronous section with no observable gap. `replace([])` is legal — a registration holding zero routes — unlike an empty initial registration.
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
一个适配器注册表加单一流式调用接口,可通过 waterfall(瀑布式事件)拦截。
|
||||
|
||||
### 重试策略
|
||||
|
||||
每个提供方适配器都会提供解析后的路由策略。省略提供方配置时使用有界 normal mode,在首次请求后最多重试五次。分层配置把 `mode` 改为 `always` 后可能残留 `maxRetries` 或 `retryableCodes`;解析过程会忽略这些不再生效的 normal-mode 字段,并捕获纯 always 策略。本服务存储有效策略,但不执行重试。
|
||||
|
||||
### 公开 API
|
||||
|
||||
- `ctx.llm.registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle` 为给定提供方路由注册一个适配器实例。注册要么全部成功,要么全部不生效,并且会随调用 fiber 一起 dispose(资源释放)。返回的句柄还提供 `replace(providers)`:候选路由集合会在注册状态发生任何变化前完成整体验证,因此与另一适配器发生冲突时,当前路由仍保持注册并继续提供服务。替换会在一次同步操作中完成,不会出现可观察的空档。`replace([])` 合法,表示保留注册但不持有任何路由;初始注册则不得为空。
|
||||
|
||||
@@ -11,7 +11,7 @@ import z from '@deepseek-ai/schemastery'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import { EMPTY_RESPONSE_CODE } from './error.ts'
|
||||
|
||||
const DEFAULT_MAX_RETRIES = 2
|
||||
const DEFAULT_MAX_RETRIES = 5
|
||||
const DEFAULT_INITIAL_DELAY_MS = 500
|
||||
const DEFAULT_MAX_DELAY_MS = 10_000
|
||||
const DEFAULT_JITTER_RATIO = 0.1
|
||||
@@ -37,7 +37,7 @@ export interface BackoffConfig {
|
||||
export interface NormalRetryPolicyConfig {
|
||||
/** Retry only configured transient failure codes. */
|
||||
mode: 'normal'
|
||||
/** Maximum eligible retries after the first request (default 2). */
|
||||
/** Maximum eligible retries after the first request (default 5). */
|
||||
maxRetries?: number
|
||||
/** Stable failure codes eligible for this policy. */
|
||||
retryableCodes?: string[]
|
||||
@@ -105,7 +105,11 @@ export const RetryPolicySchema: z<RetryPolicyConfig> = z.union([
|
||||
const NORMAL_POLICY_KEYS: ReadonlySet<string> = new Set([
|
||||
'mode', 'maxRetries', 'retryableCodes', 'backoff',
|
||||
])
|
||||
const ALWAYS_POLICY_KEYS: ReadonlySet<string> = new Set(['mode', 'backoff'])
|
||||
// Layered configuration can retain normal-only fields after switching modes;
|
||||
// always mode ignores those inactive values while still rejecting unknown keys.
|
||||
const ALWAYS_POLICY_KEYS: ReadonlySet<string> = new Set([
|
||||
'mode', 'maxRetries', 'retryableCodes', 'backoff',
|
||||
])
|
||||
const BACKOFF_KEYS: ReadonlySet<string> = new Set(['initialDelayMs', 'maxDelayMs', 'jitterRatio'])
|
||||
|
||||
function validateKeys(value: object, allowed: ReadonlySet<string>, path: string): void {
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('provider retry policy', () => {
|
||||
|
||||
expect(policy).toEqual({
|
||||
mode: 'normal',
|
||||
maxRetries: 2,
|
||||
maxRetries: 5,
|
||||
retryableCodes: ['EMPTY_RESPONSE', 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT'],
|
||||
initialDelayMs: 500,
|
||||
maxDelayMs: 10_000,
|
||||
@@ -59,6 +59,21 @@ describe('provider retry policy', () => {
|
||||
expect(RetryPolicySchema).toBeDefined()
|
||||
})
|
||||
|
||||
it('ignores normal-only fields retained after switching to always mode', () => {
|
||||
const layered = {
|
||||
mode: 'always',
|
||||
maxRetries: 5,
|
||||
retryableCodes: ['SERVER'],
|
||||
} as unknown as RetryPolicyConfig
|
||||
|
||||
expect(resolveRetryPolicy(layered, 'provider.retryPolicy')).toEqual({
|
||||
mode: 'always',
|
||||
initialDelayMs: 500,
|
||||
maxDelayMs: 10_000,
|
||||
jitterRatio: 0.1,
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
[{ mode: 'normal', maxRetries: -1 }, /maxRetries/],
|
||||
[{ mode: 'normal', maxRetries: 1.5 }, /maxRetries/],
|
||||
@@ -74,7 +89,6 @@ describe('provider retry policy', () => {
|
||||
[{ mode: 'normal', retryableCodes: [''] }, /non-empty strings/],
|
||||
[{ mode: 'normal', retryableCodes: [429] }, /non-empty strings/],
|
||||
[{ mode: 'normal', maxRetires: 1 }, /unknown key "maxRetires"/],
|
||||
[{ mode: 'always', maxRetries: 1 }, /unknown key "maxRetries"/],
|
||||
[{ mode: 'always', backoff: { initialDelay: 1 } }, /unknown key "initialDelay"/],
|
||||
[{ mode: 'sometimes' }, /mode must be "normal" or "always"/],
|
||||
] as const)('rejects invalid policy %#', (config, message) => {
|
||||
|
||||
@@ -217,7 +217,7 @@ describe('LlmRuntime', () => {
|
||||
expect(ctx.llm.providerRetryPolicy('configured')).toBe(configured)
|
||||
expect(ctx.llm.providerRetryPolicy('defaulted')).toMatchObject({
|
||||
mode: 'normal',
|
||||
maxRetries: 2,
|
||||
maxRetries: 5,
|
||||
})
|
||||
expect(() => ctx.llm.providerRetryPolicy('missing')).toThrow(
|
||||
expect.objectContaining({ code: 'NO_ADAPTER' }),
|
||||
|
||||
@@ -644,7 +644,7 @@ describe('installLlmReplay (through the real LlmRuntime)', () => {
|
||||
})
|
||||
expect(ctx.llm.providerRetryPolicy('empty')).toMatchObject({
|
||||
mode: 'normal',
|
||||
maxRetries: 2,
|
||||
maxRetries: 5,
|
||||
initialDelayMs: 500,
|
||||
maxDelayMs: 10_000,
|
||||
jitterRatio: 0.1,
|
||||
|
||||
Reference in New Issue
Block a user