refactor(llm): use one five-retry default

This commit is contained in:
Yichen Jiang
2026-08-18 16:33:22 +08:00
parent f4551895bc
commit 0ca0f3d0b8
39 changed files with 135 additions and 259 deletions
@@ -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: aa72e05f8ca74621d2ab37f9c2f3bea86cbcf7d9
2026-07-24-provider-retry-policies.zh.md: a3b998196133ce5880bfacb6985d6c95e64bb650
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, validates an explicit value, and exposes that resolved route override through `providerRetryPolicy()`. `LlmRuntime` accepts one deployment-wide `defaultRetryPolicy`, resolves it at service construction, and applies it only when the adapter supplies no route override; omitting both preserves the normal defaults of two retries. The resulting effective policy remains route-owned registration state rather than a retry-executor setting. 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. The shipped Web bundle configures `LlmRuntime.defaultRetryPolicy` for five normal-mode retries; every current or future Web adapter route inherits this larger bounded budget unless its provider configuration overrides it. Other profiles retain the core default unless their composition chooses another value.
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,17 +44,17 @@ Each scheduled retry appends a non-surface `llm/retry` event with the failed pro
## Alternatives considered
**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. The LLM deployment default is different: provider route overrides remain authoritative, and the effective policy is captured only after routing selects a registration.
**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-level shared defaults** — rejected because a deployment-wide budget would have to be repeated by every adapter family and every future adapter. Putting a discriminated policy object into an adapter's settings base also lets recursive settings layering retain normal-only keys when a user switches the policy to always mode.
**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.
**Raise the provider omission default to five** — rejected because it would also increase request cost and latency for headless and custom profiles. The Web bundle's provider configuration expresses the interactive deployment's larger bounded budget without changing other profiles.
**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 and settings written outside that UI would retain two retries. The deployment default belongs in the Web composition and is inherited uniformly.
**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.
@@ -62,10 +62,10 @@ Each scheduled retry appends a non-surface `llm/retry` event with the failed pro
## Verification
Adapter tests validate nested policies at provider load, prove explicit profile policies reach registration, and retain the serving policy across in-flight route replacement. LLM service tests prove adapter overrides win over the deployment default, omission retains the core two-retry behavior, and an invalid deployment policy fails before any route registers. 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 replace either with always mode without inheriting normal-only fields. 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. A Web model route using the deployment default may spend up to three more requests and their backoff time than a route using core omission defaults, 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.
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`校验显式值,并通过 `providerRetryPolicy()` 公开解析后的路由覆盖。`LlmRuntime` 接受一个部署级 `defaultRetryPolicy`,在服务构造时解析它,并且只在适配器未提供路由覆盖时应用;两者都省略时保留两次重试的 normal 默认值。最终得到的有效策略仍然是路由拥有的注册状态,而不是重试执行器设置。当调用进入最终适配器边界时,`ctx.llm` 会把实际提供服务的注册项所持不可变策略绑定到该调用;即使路由在请求进行期间被 dispose(资源释放)或替换,agent loop(智能体循环)仍会把该策略传给已关闭步骤恢复。`@deepseek-ai/dsh-llm-retry` 会把绑定到该调用的策略与失败步骤的持久化提供方标识结合起来。未到达最终适配器的调用没有实际提供服务的策略,因而会委托后续处理。随附的 Web bundle 为 `LlmRuntime.defaultRetryPolicy` 配置五次 normal 模式重试;当前及未来的每条 Web 适配器路由都会继承这项更大的有界预算,除非其提供方配置覆盖它。其他 profile 保留核心默认值,除非自身组合选择其他值。
每个具体适配器都在其提供方配置中接受可选的 `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,17 +44,17 @@ always 模式先请求下游恢复,使上下文溢出压缩(compaction)之
## 曾考虑的替代方案
**重试执行器级的单一 `always` 开关**:不予采纳,因为它无法把无界成本与延迟风险限制在确有需要的提供方,还可能在运行时重新路由后悄然生效。LLM 部署默认值不同:提供方路由覆盖仍然权威,而且只有在路由选定注册后才捕获有效策略。
**重试执行器级的单一 `always` 开关**:不予采纳,因为它无法把无界成本与延迟风险限制在确有需要的提供方,还可能在运行时重新路由后悄然生效。提供方路由策略仍然权威,而且只有在路由选定注册后才捕获有效策略。
**在 `dsh-llm-retry` 上维护单独的指定提供方列表**:不予采纳,因为它会在所属适配器配置之外重复提供方路由名称,并让提供方注册与恢复策略发生偏差。
**设置很大的有限重试次数**:不予采纳,因为它最终仍会违反持续重试的约定,并把任意选取的运维上限序列化成看似有意义的数值。
**适配器级共享默认值**:不予采纳,因为部署级预算必须在每种适配器族以及未来的每个适配器中重复配置。把可辨识策略对象放入适配器的 settings base,还会让 settings 递归分层在用户把策略切换为 always 模式时保留仅属于 normal 的字段
**适配器设置不同的省略默认值**:不予采纳,因为共享预算必须在每种适配器族以及未来的每个适配器中重复配置,同等模型路由也会因实现不同而表现不同
**将提供方省略配置时的默认值提高到五次**:不予采纳,因为这也会增加 headless 与自定义 profile 的请求成本和延迟。Web bundle 的提供方配置能表达互动式部署所需的更大有界预算,而不改变其他 profile
**LLM 部署级默认值**:不予采纳,因为这只为区分 Web 与其他组合增加了一层配置。产品默认值保持统一,提供方 settings 则保留既有的逐路由覆盖能力
**在 Web UI 写入 profile 时把五次重试写死进去**:不予采纳,因为现有 profile从该 UI 之外写入的 settings 仍会保留两次重试。部署默认值应位于 Web 组合中,并被统一继承
**在 Web UI 写入 profile 时把五次重试写死进去**:不予采纳,因为现有 profile从该 UI 之外写入的 settings 以及非 Web 组合仍会保留旧值
**使用提供方 SDK 重试**:不予采纳,因为隐藏尝试会叠加 agent 层预算,无法利用已关闭步骤的持久性边界,还可能在没有可重建重试记录的情况下拼接或丢弃流式输出。
@@ -62,10 +62,10 @@ always 模式先请求下游恢复,使上下文溢出压缩(compaction)之
## 验证
适配器测试会在提供方加载时校验嵌套策略,证明显式 profile 策略抵达注册流程,并证明请求进行期间替换路由后仍会保留实际提供服务的策略。LLM 服务测试会证明适配器覆盖优先于部署默认值、省略配置时保留核心的两次重试行为,以及无效部署策略会在任何路由注册前失败。单元测试根据失败请求实际使用的注册项选择策略、分离不同提供方和策略变更后的重试历史、验证 always 模式可越过 normal 预算、固定抖动和延迟上限、证明下游恢复顺序、证明取消与 dispose 会先排空已委托的恢复再达到完全停稳,并证明二者都会停止正在进行的退避等待。请求级覆盖会比较失败尝试与重试尝试的完整消息,并排除提供方错误文本和丢弃的部分输出。一个无密钥 headless `stream-json` 快照会通过组装后的应用执行失败、重试与成功流程,固定完整的 `llm/retry` 记录,并拒绝各次尝试之间出现任何模型消息变化。随附的 Web 组合快照会把省略配置的 DeepSeek 与 pi-ai 策略固定为五次重试,再证明 settings 可以把任一策略替换为 always 模式,且不会继承仅属于 normal 的字段。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 模式配备可取消的调用方和针对提供方的成本控制。使用该部署默认值的 Web 模型路由比使用核心省略默认值的路由最多多花费三次请求及其退避时间,以此换取从更长短暂故障中恢复的能力。重试状态保持可观察且会持久化,但不会对模型可见;捕获实际提供服务的注册项,也能防止适配器生命周期变化反过来改变进行中请求的恢复约定。
normal 模式仍是有限的默认策略;显式的 always 策略可能在永久性的身份验证、配额、无效请求、协议或上下文错误上耗费无限次请求和无限时间。运维方必须为 always 模式配备可取消的调用方和针对提供方的成本控制。任何使用省略默认值的模型路由相比原先的两次重试默认值,最多多花费三次请求及其退避时间,以此换取从更长短暂故障中恢复的能力。重试状态保持可观察且会持久化,但不会对模型可见;捕获实际提供服务的注册项,也能防止适配器生命周期变化反过来改变进行中请求的恢复约定。
本决策扩展了[瞬态 LLM(大语言模型)请求失败的有界恢复](../architecture/2026-06-21-bounded-llm-request-recovery.md)中确定的已关闭步骤恢复、单次可见适配器尝试、结构化失败与持久化状态设计。
+1 -1
View File
@@ -94,7 +94,7 @@ it('assembles the shipped Web catalog, file-reference guidance, retry policy, an
}
`)
await ctx.settings.update(settingsNamespace('llm-deepseek'), {
retryPolicy: { mode: 'always' },
retryPolicy: { mode: 'always', maxRetries: 5 },
})
expect(ctx.llm.providerRetryPolicy('deepseek-official')).toMatchInlineSnapshot(`
{
+2 -2
View File
@@ -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: 60f7f5afefb59cb4bd0d65500886bebc988c5b6b
config-catalog.zh.md: 0b9055f14b7a2bd5192b0006db7acf45cf4bcb9a
config-catalog.md: ae72c866c66d0bfc529cbd95961a3fdb95e5bcc3
config-catalog.zh.md: 028f6d15e1db9762a546e365fac92379a556a2b8
+3 -49
View File
@@ -838,53 +838,6 @@ export interface Config {
Source: [`packages/jobs/jobs-local/src/index.ts:31`](../packages/jobs/jobs-local/src/index.ts)
<a id="deepseek-aidsh-llm"></a>
## `@deepseek-ai/dsh-llm`
```ts config-catalog
/** Deployment-wide defaults applied to provider routes without adapter-owned overrides. */
export interface Config {
/** Model-request retry policy inherited by routes whose adapter omits one; omission uses normal defaults. */
defaultRetryPolicy?: RetryPolicyConfig
}
/** Model-request retry policy configuration for a provider route or deployment default. */
export type RetryPolicyConfig = NormalRetryPolicyConfig | AlwaysRetryPolicyConfig
/** Current bounded transient retry behavior for one provider route. */
export interface NormalRetryPolicyConfig {
/** Retry only configured transient failure codes. */
mode: 'normal'
/** Maximum eligible retries after the first request (default 2). */
maxRetries?: number
/** Stable failure codes eligible for this policy. */
retryableCodes?: string[]
/** Local exponential-backoff and jitter configuration. */
backoff?: BackoffConfig
}
/** Unbounded retry behavior for every model-request failure on one provider route. */
export interface AlwaysRetryPolicyConfig {
/** Retry every model-request failure until success, cancellation, or disposal. */
mode: 'always'
/** Local exponential-backoff and jitter configuration. */
backoff?: BackoffConfig
}
/** Bounded exponential backoff with symmetric jitter around each local delay. */
export interface BackoffConfig {
/** Initial local exponential-backoff delay in milliseconds (default 500). */
initialDelayMs?: number
/** Maximum locally scheduled or accepted provider delay in milliseconds (default 10000). */
maxDelayMs?: number
/** Symmetric random multiplier range around one (default 0.1). */
jitterRatio?: number
}
```
Source: [`packages/llm/llm/src/index.ts:176`](../packages/llm/llm/src/index.ts)
<a id="deepseek-aidsh-llm-deepseek"></a>
## `@deepseek-ai/dsh-llm-deepseek`
@@ -917,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 inherits the LLM deployment default. */
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
retryPolicy?: RetryPolicyConfig
}
@@ -1032,7 +985,7 @@ export interface PiAiProviderProfile {
websocketConnectTimeoutMs?: number
/** Maximum provider idle time while one stream read is outstanding. */
streamIdleTimeoutMs?: number
/** Provider-owned model-request retry policy; omission inherits the LLM deployment default. */
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
retryPolicy?: RetryPolicyConfig
}
@@ -3122,6 +3075,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-host-directory-picker-auto` — requires `webServer` · `loader` ([`packages/host/directory-picker-auto/src/index.ts`](../packages/host/directory-picker-auto/src/index.ts))
- `@deepseek-ai/dsh-host-directory-picker-native` ([`packages/host/directory-picker-native/src/index.ts`](../packages/host/directory-picker-native/src/index.ts))
- `@deepseek-ai/dsh-host-plugin-inventory` — requires `loader` ([`packages/host/plugin-inventory/src/index.ts`](../packages/host/plugin-inventory/src/index.ts))
- `@deepseek-ai/dsh-llm` ([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts))
- `@deepseek-ai/dsh-lsp` ([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts))
- `@deepseek-ai/dsh-schedule` — requires `agents` · `sessions` · `tools` · `sessionPersistence` ([`packages/schedule/schedule/src/index.ts`](../packages/schedule/schedule/src/index.ts))
- `@deepseek-ai/dsh-session` ([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts))
+3 -49
View File
@@ -840,53 +840,6 @@ export interface Config {
来源:[`packages/jobs/jobs-local/src/index.ts:31`](../packages/jobs/jobs-local/src/index.ts)
<a id="deepseek-aidsh-llm"></a>
## `@deepseek-ai/dsh-llm`
```ts config-catalog
/** Deployment-wide defaults applied to provider routes without adapter-owned overrides. */
export interface Config {
/** Model-request retry policy inherited by routes whose adapter omits one; omission uses normal defaults. */
defaultRetryPolicy?: RetryPolicyConfig
}
/** Model-request retry policy configuration for a provider route or deployment default. */
export type RetryPolicyConfig = NormalRetryPolicyConfig | AlwaysRetryPolicyConfig
/** Current bounded transient retry behavior for one provider route. */
export interface NormalRetryPolicyConfig {
/** Retry only configured transient failure codes. */
mode: 'normal'
/** Maximum eligible retries after the first request (default 2). */
maxRetries?: number
/** Stable failure codes eligible for this policy. */
retryableCodes?: string[]
/** Local exponential-backoff and jitter configuration. */
backoff?: BackoffConfig
}
/** Unbounded retry behavior for every model-request failure on one provider route. */
export interface AlwaysRetryPolicyConfig {
/** Retry every model-request failure until success, cancellation, or disposal. */
mode: 'always'
/** Local exponential-backoff and jitter configuration. */
backoff?: BackoffConfig
}
/** Bounded exponential backoff with symmetric jitter around each local delay. */
export interface BackoffConfig {
/** Initial local exponential-backoff delay in milliseconds (default 500). */
initialDelayMs?: number
/** Maximum locally scheduled or accepted provider delay in milliseconds (default 10000). */
maxDelayMs?: number
/** Symmetric random multiplier range around one (default 0.1). */
jitterRatio?: number
}
```
来源:[`packages/llm/llm/src/index.ts:176`](../packages/llm/llm/src/index.ts)
<a id="deepseek-aidsh-llm-deepseek"></a>
## `@deepseek-ai/dsh-llm-deepseek`
@@ -919,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 inherits the LLM deployment default. */
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
retryPolicy?: RetryPolicyConfig
}
@@ -1034,7 +987,7 @@ export interface PiAiProviderProfile {
websocketConnectTimeoutMs?: number
/** Maximum provider idle time while one stream read is outstanding. */
streamIdleTimeoutMs?: number
/** Provider-owned model-request retry policy; omission inherits the LLM deployment default. */
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
retryPolicy?: RetryPolicyConfig
}
@@ -3124,6 +3077,7 @@ export interface Config {
- `@deepseek-ai/dsh-host-directory-picker-auto` — 需要 `webServer` · `loader`[`packages/host/directory-picker-auto/src/index.ts`](../packages/host/directory-picker-auto/src/index.ts)
- `@deepseek-ai/dsh-host-directory-picker-native`[`packages/host/directory-picker-native/src/index.ts`](../packages/host/directory-picker-native/src/index.ts)
- `@deepseek-ai/dsh-host-plugin-inventory` — 需要 `loader`[`packages/host/plugin-inventory/src/index.ts`](../packages/host/plugin-inventory/src/index.ts)
- `@deepseek-ai/dsh-llm`[`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts)
- `@deepseek-ai/dsh-lsp`[`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts)
- `@deepseek-ai/dsh-schedule` — 需要 `agents` · `sessions` · `tools` · `sessionPersistence`[`packages/schedule/schedule/src/index.ts`](../packages/schedule/schedule/src/index.ts)
- `@deepseek-ai/dsh-session`[`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts)
+2 -2
View File
@@ -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/event-producer-consumer.md
event-producer-consumer.md: c17a6163ef829f0176163024a4632b712a7195aa
event-producer-consumer.zh.md: 5e6073ad3dbe1e240b5bc6f5fbc55b499c9373c2
event-producer-consumer.md: 6a79e6f7ce5addc64b10efa8da7a886dcfb36dc2
event-producer-consumer.zh.md: f7576a8e28e4f1db2c65c324595c05c98b8fe488
+1 -1
View File
@@ -36,7 +36,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:65`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
+1 -1
View File
@@ -38,7 +38,7 @@
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:65`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
+2 -2
View File
@@ -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: 4627f3cd9e224ef2cced3253b82d2b8461d73986
llm-streaming.zh.md: a161267162aa5773279ca4a417caec57cd721115
llm-streaming.md: c1d40bf3111bb534a5401506016256264b3767bf
llm-streaming.zh.md: 04e753d486f667eb4a63ef33d41e7103c03f4914
+8 -8
View File
@@ -240,7 +240,7 @@ Every adapter MUST obey these, and every consumer may rely on them:
## `ResolvedRetryPolicy`
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. An explicit adapter route policy overrides `LlmRuntime.defaultRetryPolicy`; omitting both uses the normal default of two retries. `LlmRuntime.providerRetryPolicy(provider)` returns that effective 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.
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
@@ -655,7 +655,7 @@ interface LlmCallConfigAdapterDefaults {
## Service and provider contracts
`LlmAdapter` is the provider contract: subclass, implement `stream()`, and register one adapter instance with `ctx.llm.registerAdapter(providers, adapter)`. `GenerateOptions.provider` selects the registered adapter; `GenerateOptions.model` is passed to that adapter and need not be registered at lifecycle start. Duplicate provider routes fail atomically. Optional `providerRetryPolicy()` supplies an explicit route override; omission inherits the LLM deployment default. `providerInfo()` and asynchronous `listModels()` feed `LlmRuntime.listProviders()` / `listModels()` with detached selector metadata. That catalog is advisory rather than a request whitelist: the adapter remains authoritative and may accept unlisted model ids. One asynchronous `resolveModel()` query returns exact model identity plus optional correctness-sensitive context capacity, an adapter-configured `defaultMaxTokens`, and ordered model-owned reasoning ids with an optional deployment default; absent fields mean unavailable metadata or provider-owned behavior, not invalid catalog membership. The resolver receives optional cancellation and must settle promptly after abort. `LlmRuntime.resolveModelInfo()` validates and detaches the aggregate. At the final adapter boundary, `resolveCallConfig()` materializes the output default only when `maxTokens` is absent and validates and materializes reasoning, so direct calls cannot bypass either configured behavior; direct dispatch captures one registration before awaiting that resolution. The agent loop instead uses `prepareCall()` to keep the same registration across model resolution, durable header logging, and dispatch, retain detached context metadata from that exact lookup, and report which config fields the adapter defaulted. Adapter lookup happens at the terminal continuation of the `llm/stream` waterfall, so a listener may short-circuit the call or route a mutable one-shot request before lookup. AgentLoop observes a request attempt once the outer waterfall returns a stream handle; that limited boundary does not prove a lazy terminal adapter was constructed or began provider I/O. The `block-start` / `block-end` `index` correlation and the assembler together mean an adapter only has to emit well-formed chunks — block reassembly is not each adapter's problem. [architecture.md](../architecture.md#turn-flow) shows where `ctx.llm.stream()` and the `llm/stream` waterfall sit in one turn.
`LlmAdapter` is the provider contract: subclass, implement `stream()`, and register one adapter instance with `ctx.llm.registerAdapter(providers, adapter)`. `GenerateOptions.provider` selects the registered adapter; `GenerateOptions.model` is passed to that adapter and need not be registered at lifecycle start. Duplicate provider routes fail atomically. Optional `providerRetryPolicy()` supplies a resolved provider policy; omission uses the shared normal defaults. `providerInfo()` and asynchronous `listModels()` feed `LlmRuntime.listProviders()` / `listModels()` with detached selector metadata. That catalog is advisory rather than a request whitelist: the adapter remains authoritative and may accept unlisted model ids. One asynchronous `resolveModel()` query returns exact model identity plus optional correctness-sensitive context capacity, an adapter-configured `defaultMaxTokens`, and ordered model-owned reasoning ids with an optional deployment default; absent fields mean unavailable metadata or provider-owned behavior, not invalid catalog membership. The resolver receives optional cancellation and must settle promptly after abort. `LlmRuntime.resolveModelInfo()` validates and detaches the aggregate. At the final adapter boundary, `resolveCallConfig()` materializes the output default only when `maxTokens` is absent and validates and materializes reasoning, so direct calls cannot bypass either configured behavior; direct dispatch captures one registration before awaiting that resolution. The agent loop instead uses `prepareCall()` to keep the same registration across model resolution, durable header logging, and dispatch, retain detached context metadata from that exact lookup, and report which config fields the adapter defaulted. Adapter lookup happens at the terminal continuation of the `llm/stream` waterfall, so a listener may short-circuit the call or route a mutable one-shot request before lookup. AgentLoop observes a request attempt once the outer waterfall returns a stream handle; that limited boundary does not prove a lazy terminal adapter was constructed or began provider I/O. The `block-start` / `block-end` `index` correlation and the assembler together mean an adapter only has to emit well-formed chunks — block reassembly is not each adapter's problem. [architecture.md](../architecture.md#turn-flow) shows where `ctx.llm.stream()` and the `llm/stream` waterfall sit in one turn.
```ts type-equiv
/** One model call whose config and adapter registration were resolved together. */
@@ -694,9 +694,9 @@ declare abstract class LlmAdapter {
*/
providerInfo(provider: string): LlmProviderInfo;
/**
* Return an explicit provider-owned retry policy override for this route.
* Return the provider-owned retry policy captured with this route.
* @param _provider - a route passed to `registerAdapter()` for this instance.
* @returns a resolved override, or `undefined` to inherit the LLM deployment default.
* @returns a resolved policy, or `undefined` to use the normal defaults.
*/
providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined;
/**
@@ -803,9 +803,9 @@ registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscover
async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, ): Promise<LlmDiscoveredModel[]>
/**
* Read the effective retry policy captured when one provider route was registered.
* Read the retry policy captured when one provider route was registered.
* @param provider - registered provider route to inspect.
* @returns the adapter override or deployment default, fully resolved.
* @returns the provider-owned policy, with normal defaults already resolved.
*/
providerRetryPolicy(provider: string): ResolvedRetryPolicy
@@ -864,7 +864,7 @@ async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<Prepared
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
```
Source: [`packages/llm/llm/src/index.ts:291`](../../packages/llm/llm/src/index.ts)
Source: [`packages/llm/llm/src/index.ts:284`](../../packages/llm/llm/src/index.ts)
<a id="llm-events"></a>
@@ -913,5 +913,5 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
'llm/stream'(this: LlmRuntime, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
```
Source: [`packages/llm/llm/src/index.ts:65`](../../packages/llm/llm/src/index.ts)
Source: [`packages/llm/llm/src/index.ts:64`](../../packages/llm/llm/src/index.ts)
<!-- END GENERATED cordis-surface -->
+8 -8
View File
@@ -242,7 +242,7 @@ interface LlmFailure {
## `ResolvedRetryPolicy`
重试配置会在路由注册前解析为不可变的可辨识联合。normal mode 携带 `mode: 'normal'`、有限的 `maxRetries`、`retryableCodes`,以及必填的 `initialDelayMs`、`maxDelayMs` 与 `jitterRatio`always mode 携带 `mode: 'always'` 和相同的必填退避字段,但没有有限上限。显式适配器路由策略会覆盖 `LlmRuntime.defaultRetryPolicy`;两者都省略时使用两次重试的 normal 默认值。`LlmRuntime.providerRetryPolicy(provider)` 返回注册的该有效值;调用选定实际提供服务的注册后,`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`:应用归属
@@ -661,7 +661,7 @@ interface LlmCallConfigAdapterDefaults {
## 服务与提供方约定
`LlmAdapter` 是提供方约定:创建子类、实现 `stream()`,再用 `ctx.llm.registerAdapter(providers, adapter)` 注册一个适配器实例。`GenerateOptions.provider` 选择已注册适配器;`GenerateOptions.model` 会传给该适配器,无需在生命周期启动时注册。重复提供方路由会原子失败。可选的 `providerRetryPolicy()` 提供显式路由覆盖;省略时继承 LLM 部署默认值。`providerInfo()` 与异步 `listModels()` 方法则为 `LlmRuntime.listProviders()` / `listModels()` 提供分离的 selector 元数据。该目录仅供参考,不是请求白名单:适配器仍是权威,并可接受未列出的模型 id。单次异步 `resolveModel()` 查询返回确切模型身份,以及可选的对正确性敏感的上下文容量、适配器配置的 `defaultMaxTokens`、由模型持有的有序推理强度 ID 和可选的部署默认值;字段缺失表示元数据不可用或保留提供方持有的行为,而不表示目录成员关系无效。解析器会接收可选的取消信号,并且必须在信号中止后迅速完成结算。`LlmRuntime.resolveModelInfo()` 会校验聚合结果并返回分离值。在最终适配器边界,`resolveCallConfig()` 仅在 `maxTokens` 缺失时填入输出默认值,并校验和填入推理强度,因此直接调用也无法绕过任何一项已配置行为;直接分派会在等待解析前捕获一项适配器注册。agent loop 则使用 `prepareCall()`,使模型解析、请求头持久记录和分派全程使用同一项注册,保留来自同一次查询的分离上下文元数据,并报告适配器填入的配置字段。适配器查找发生在 `llm/stream` waterfall 的终端 continuation,因此 listener 可以在查找前短路调用,或路由一个可变的一次性请求。AgentLoop 在外层 waterfall 返回流句柄时观察到一次请求尝试;这个有限边界不能证明惰性终端适配器已构造完成或开始提供方 I/O。`block-start` / `block-end` 的 `index` 关联与 assembler 共同意味着适配器只需 emit 格式正确的分片——块重组不是每个适配器各自的问题。`ctx.llm.stream()` 与 `llm/stream` waterfall 在一个轮次中的位置见 [architecture.md](../architecture.md#turn-flow)。
`LlmAdapter` 是提供方约定:创建子类、实现 `stream()`,再用 `ctx.llm.registerAdapter(providers, adapter)` 注册一个适配器实例。`GenerateOptions.provider` 选择已注册适配器;`GenerateOptions.model` 会传给该适配器,无需在生命周期启动时注册。重复提供方路由会原子失败。可选的 `providerRetryPolicy()` 提供解析后的提供方策略;省略时使用共享的 normal 默认值。`providerInfo()` 与异步 `listModels()` 方法则为 `LlmRuntime.listProviders()` / `listModels()` 提供分离的 selector 元数据。该目录仅供参考,不是请求白名单:适配器仍是权威,并可接受未列出的模型 id。单次异步 `resolveModel()` 查询返回确切模型身份,以及可选的对正确性敏感的上下文容量、适配器配置的 `defaultMaxTokens`、由模型持有的有序推理强度 ID 和可选的部署默认值;字段缺失表示元数据不可用或保留提供方持有的行为,而不表示目录成员关系无效。解析器会接收可选的取消信号,并且必须在信号中止后迅速完成结算。`LlmRuntime.resolveModelInfo()` 会校验聚合结果并返回分离值。在最终适配器边界,`resolveCallConfig()` 仅在 `maxTokens` 缺失时填入输出默认值,并校验和填入推理强度,因此直接调用也无法绕过任何一项已配置行为;直接分派会在等待解析前捕获一项适配器注册。agent loop 则使用 `prepareCall()`,使模型解析、请求头持久记录和分派全程使用同一项注册,保留来自同一次查询的分离上下文元数据,并报告适配器填入的配置字段。适配器查找发生在 `llm/stream` waterfall 的终端 continuation,因此 listener 可以在查找前短路调用,或路由一个可变的一次性请求。AgentLoop 在外层 waterfall 返回流句柄时观察到一次请求尝试;这个有限边界不能证明惰性终端适配器已构造完成或开始提供方 I/O。`block-start` / `block-end` 的 `index` 关联与 assembler 共同意味着适配器只需 emit 格式正确的分片——块重组不是每个适配器各自的问题。`ctx.llm.stream()` 与 `llm/stream` waterfall 在一个轮次中的位置见 [architecture.md](../architecture.md#turn-flow)。
```ts type-equiv
/** One model call whose config and adapter registration were resolved together. */
@@ -700,9 +700,9 @@ declare abstract class LlmAdapter {
*/
providerInfo(provider: string): LlmProviderInfo;
/**
* Return an explicit provider-owned retry policy override for this route.
* Return the provider-owned retry policy captured with this route.
* @param _provider - a route passed to `registerAdapter()` for this instance.
* @returns a resolved override, or `undefined` to inherit the LLM deployment default.
* @returns a resolved policy, or `undefined` to use the normal defaults.
*/
providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined;
/**
@@ -809,9 +809,9 @@ registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscover
async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, ): Promise<LlmDiscoveredModel[]>
/**
* Read the effective retry policy captured when one provider route was registered.
* Read the retry policy captured when one provider route was registered.
* @param provider - registered provider route to inspect.
* @returns the adapter override or deployment default, fully resolved.
* @returns the provider-owned policy, with normal defaults already resolved.
*/
providerRetryPolicy(provider: string): ResolvedRetryPolicy
@@ -870,7 +870,7 @@ async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<Prepared
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
```
Source: [`packages/llm/llm/src/index.ts:291`](../../packages/llm/llm/src/index.ts)
Source: [`packages/llm/llm/src/index.ts:284`](../../packages/llm/llm/src/index.ts)
<a id="llm-events"></a>
@@ -919,5 +919,5 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
'llm/stream'(this: LlmRuntime, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
```
Source: [`packages/llm/llm/src/index.ts:65`](../../packages/llm/llm/src/index.ts)
Source: [`packages/llm/llm/src/index.ts:64`](../../packages/llm/llm/src/index.ts)
<!-- END GENERATED cordis-surface -->
+2 -2
View File
@@ -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: 30ad018ef60ea2c1c8926f482dd839dec2ee61d8
README.zh.md: ccc7acff3027ce7cd973f3d9e9a48d831ade00e5
README.md: 28fb5b3dcfc7fbb912493a6b97495e2ed5a3eece
README.zh.md: 92157f05497e53c48506666a3a53d638d98a7c9e
+1 -1
View File
@@ -6,7 +6,7 @@ The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides ove
## Model retry defaults
The Web layer configures `LlmRuntime.defaultRetryPolicy` with normal mode and `maxRetries: 5`, allowing at most five eligible retries after the initial request. Every adapter route without an explicit provider policy inherits it, including `deepseek-official`, settings-added pi-ai routes, and future adapters. A `llm-deepseek` or pi-ai provider `retryPolicy` overrides the deployment default; non-Web profiles retain the core two-retry default unless their own composition chooses another value.
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
+1 -1
View File
@@ -6,7 +6,7 @@ dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在
## 模型重试默认值
Web 层会为 `LlmRuntime.defaultRetryPolicy` 配置 normal 模式与 `maxRetries: 5`,使其在初次请求后最多再重试 5 次符合条件的失败。每条没有显式提供方策略的适配器路由都会继承它,包括 `deepseek-official`由 settings 新增的 pi-ai 路由及未来适配器。`llm-deepseek` 或 pi-ai 提供方的 `retryPolicy` 会覆盖部署默认值;非 Web profile 保留核心的两次重试默认值,除非自身组合选择其他值
Web 使用共享的有界 normal 默认值,在首次请求后最多再重试次符合条件的失败。`deepseek-official`由 settings 新增的 pi-ai 路由在省略 `retryPolicy` 时使用该默认值;显式提供方策略仍然优先。Web 不再增加重试专用的组合覆盖,因此非 Web profile 的省略行为与之相同
## 模型体验
-8
View File
@@ -40,14 +40,6 @@
# once the web UI owns the choice per session.
mode: !!js process.env.DSH_TOOLS_MODE
# The Web deployment gives every model route without an explicit provider
# policy a larger bounded transient-failure budget than the core default.
- id: llm
config:
defaultRetryPolicy:
mode: normal
maxRetries: 5
# ── web-only host rows, the transport layer, and the browser roster ─────────
# `dsh.client` rows are the browser roster the modules node half scans into
@@ -832,9 +832,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
{
signature: 'providerRetryPolicy(provider: string): ResolvedRetryPolicy',
description: 'Read the effective retry policy captured when one provider route was registered.',
description: 'Read the retry policy captured when one provider route was registered.',
parameters: [{ name: 'provider', description: 'registered provider route to inspect.' }],
returns: 'the adapter override or deployment default, fully resolved.',
returns: 'the provider-owned policy, with normal defaults already resolved.',
},
{
signature: 'async listModels(provider: string): Promise<LlmModelInfo[]>',
@@ -3321,7 +3321,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'LlmRuntime',
declaration: 'export class LlmRuntime extends Service {\n static Config: z<Config>;\n constructor(ctx: Context, config: Config = {});\n registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle;\n listProviders(): LlmProviderInfo[];\n registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle;\n listConfigurableProviders(): LlmConfigurableProvider[];\n registerModelDiscovery(settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise<readonly LlmDiscoveredModel[]>): () => void;\n async discoverModels(settingsNs: string, request: LlmModelDiscoveryRequest): Promise<LlmDiscoveredModel[]>;\n providerRetryPolicy(provider: string): ResolvedRetryPolicy;\n async listModels(provider: string): Promise<LlmModelInfo[]>;\n async resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;\n async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>;\n async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>;\n stream(options: GenerateOptions): AsyncIterable<StreamChunk>;\n}',
declaration: 'export class LlmRuntime extends Service {\n constructor(ctx: Context);\n registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle;\n listProviders(): LlmProviderInfo[];\n registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle;\n listConfigurableProviders(): LlmConfigurableProvider[];\n registerModelDiscovery(settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise<readonly LlmDiscoveredModel[]>): () => void;\n async discoverModels(settingsNs: string, request: LlmModelDiscoveryRequest): Promise<LlmDiscoveredModel[]>;\n providerRetryPolicy(provider: string): ResolvedRetryPolicy;\n async listModels(provider: string): Promise<LlmModelInfo[]>;\n async resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;\n async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>;\n async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>;\n stream(options: GenerateOptions): AsyncIterable<StreamChunk>;\n}',
},
{
name: 'LspHover',
+2 -2
View File
@@ -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: 9087c57844a3502f8e1ba650979722638eb231f0
README.zh.md: aea543563d1294d953b36210327cd1bb1d92b961
README.md: 65c17f504e74408b37518149007c006d3836c4c6
README.zh.md: 217d00c66ff332b67546e3efca2025347c1d28e0
+4 -4
View File
@@ -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 inherits the LLM deployment default
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 explicit `retryPolicy` override when configured; `LlmRuntime` supplies the deployment default otherwise. 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')`.
@@ -45,7 +45,7 @@ The same exact-model result exposes ordered `off`, `low`, `high`, and `max` effo
`thinking: disabled` is a deployment lock that publishes only `off` with `off` as its default. Omitting `reasoningEffort` or configuring it as `off` is valid; configuring `low`, `high`, or `max` fails plugin loading, and a direct per-request attempt to enable thinking fails before network I/O. A request with `GenerateOptions.purpose: 'session-title'` also forces thinking disabled and omits the already-resolved effort, reserving its bounded output for visible title text without changing conversation or compaction defaults.
`streamIdleTimeoutMs` bounds each outstanding provider read, including the initial `fetch`, without counting time the consumer spends between chunks. DeepSeek SSE comments rearm an outstanding read as transport activity but never become `StreamChunk` values or session-log events. One stable abort signal reaches the request and body reader for the whole call; expiry stops the transport and throws `LlmError('TIMEOUT')`, while an earlier caller abort throws `LlmError('ABORTED')`. The adapter makes exactly one provider request per `stream()` call; it registers only an explicit provider policy override, `LlmRuntime` captures the effective route policy, and `dsh-llm-retry` separately executes it at durable agent-step boundaries.
`streamIdleTimeoutMs` bounds each outstanding provider read, including the initial `fetch`, without counting time the consumer spends between chunks. DeepSeek SSE comments rearm an outstanding read as transport activity but never become `StreamChunk` values or session-log events. One stable abort signal reaches the request and body reader for the whole call; expiry stops the transport and throws `LlmError('TIMEOUT')`, while an earlier caller abort throws `LlmError('ABORTED')`. The adapter makes exactly one provider request per `stream()` call; it registers the resolved provider policy, and `dsh-llm-retry` separately executes it at durable agent-step boundaries.
## Dynamic configuration (settings + credentials)
@@ -54,7 +54,7 @@ Connection facts are not frozen at load. `resolveAdapterOptions` is the one expl
- **`ctx.settings`** — the plugin registers the `llm-deepseek` namespace with this same `Config` schema and its `cordis.yml` entry as the composition `base`, so a `llm-deepseek:` section in the user settings document overrides any field without a restart. Without a mounted settings service the entry config alone drives the adapter, unchanged. A live settings snapshot that passes the schema but fails a beyond-schema bound (a duplicate catalog id, a broken thinking/effort pair) keeps the last good facts and logs the failure; the entry config itself still fails plugin load.
- **`ctx.credentials`** — the API key resolves per stream call, from the *same* resolved snapshot that supplies the endpoint. Configuration carries only `apiKeyEnv`, never a literal key: the reference resolves through the credential seam, and without a mounted seam through the trusted environment layers. Because credential facts travel with the connection facts, a settings snapshot the resolver rejects contributes neither its endpoint nor its key: the whole previous generation keeps serving. Every resolved key is format-checked before use, so a value no HTTP header can carry is refused with `LlmError('INVALID_CREDENTIAL')` naming the failing entry point — never any part of the key — instead of surfacing as an opaque `fetch` `TypeError`. A request with no key anywhere fails with `MISSING_CREDENTIAL` naming every configuration entry point, while the route stays registered and the catalog stays browsable — first-run onboarding is "browse models, store the key, prompt again", with no restart between.
The one adapter-owned registration fact is the explicit retry policy override: when its resolved value changes or is removed, the plugin re-registers the route in place (same adapter instance, one synchronous section). `ctx.llm.providerRetryPolicy('deepseek-official')` therefore reports either the current override or `LlmRuntime`'s deployment default.
The adapter-owned registration fact is the resolved retry policy: when its value changes, the plugin re-registers the route in place (same adapter instance, one synchronous section). `ctx.llm.providerRetryPolicy('deepseek-official')` therefore reports the current provider policy, including the shared five-retry normal default when configuration omits it.
The plugin also declares its route in the configurable-provider directory (`ctx.llm.listConfigurableProviders()`): provider `deepseek-official`, settings namespace `llm-deepseek`, empty settings path — the whole section is the profile. Configuration surfaces use that entry to offer this adapter alongside dormant pi-ai providers.
+4 -4
View File
@@ -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 inherits the LLM deployment default
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` 覆盖;否则由 `LlmRuntime` 提供部署默认值。请求使用 `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')` 公开给 ACPAgent 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')` 公开给 ACPAgent 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')`
@@ -45,7 +45,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
`thinking: disabled` 是部署锁定:它只公布 `off`,并以 `off` 为默认值。省略 `reasoningEffort` 或将其配置为 `off` 均有效;配置 `low``high``max` 会使插件加载失败,直接按请求启用思考也会在网络 I/O 前失败。携带 `GenerateOptions.purpose: 'session-title'` 的请求也会强制禁用思考并省略已解析的推理强度,将有界输出保留给可见标题文本,不改变会话或压缩(compaction)默认值。
`streamIdleTimeoutMs` 会限制每次未完成提供方读取,包括初始 `fetch`,但不计入消费方在分片间花费的时间。DeepSeek SSE 注释会作为传输活动使尚未完成的读取重新布防,但绝不会成为 `StreamChunk` 值或会话日志事件。同一个稳定的 abort 信号会在整个调用期间传递给请求与 body reader;过期会停止传输并抛出 `LlmError('TIMEOUT')`,较早的调用方 abort 则抛出 `LlmError('ABORTED')`。适配器每次 `stream()` 调用恰好发起一次提供方请求;它注册显式提供方策略覆盖,`LlmRuntime` 捕获有效路由策略,再由 `dsh-llm-retry` 在持久化的 agent(智能体)步骤边界单独执行该策略。
`streamIdleTimeoutMs` 会限制每次未完成提供方读取,包括初始 `fetch`,但不计入消费方在分片间花费的时间。DeepSeek SSE 注释会作为传输活动使尚未完成的读取重新布防,但绝不会成为 `StreamChunk` 值或会话日志事件。同一个稳定的 abort 信号会在整个调用期间传递给请求与 body reader;过期会停止传输并抛出 `LlmError('TIMEOUT')`,较早的调用方 abort 则抛出 `LlmError('ABORTED')`。适配器每次 `stream()` 调用恰好发起一次提供方请求;它注册解析后的提供方策略,再由 `dsh-llm-retry` 在持久化的 agent(智能体)步骤边界单独执行该策略。
## 动态配置(settings + credentials
@@ -54,7 +54,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
- **`ctx.settings`**——插件用同一份 `Config` schema 注册 `llm-deepseek` namespace,并以其 `cordis.yml` 条目为组合 `base`,因此用户设置文档中的 `llm-deepseek:` 分节可以免重启覆盖任何字段。未挂载 settings 服务时,仅由 entry 配置驱动适配器,行为不变。存活 settings 快照若通过 schema 却违反 schema 之外的约束(重复的 catalog id、无法成立的 thinking/推理强度组合),则保留最后可用事实并记录失败;entry 配置本身仍会使插件加载失败。
- **`ctx.credentials`**——API 密钥按每次 stream 调用解析,取自与端点*同一*份解析后的快照。配置只携带 `apiKeyEnv`,从不携带字面密钥:该引用经凭据 seam 解析,未挂载 seam 时则经受信环境层解析。由于凭据事实与连接事实同行,被 resolver 拒绝的 settings 快照既不贡献自己的端点,也不贡献自己的密钥:整个先前世代继续服务。每个解析出的密钥在使用前都会被校验格式,因此 HTTP 标头无法承载的值会以 `LlmError('INVALID_CREDENTIAL')` 被拒绝,点名失败的入口,但绝不透露密钥的任何部分,而不是以语义不明的 `fetch` `TypeError` 形式浮现。任何地方都没有密钥的请求以 `MISSING_CREDENTIAL` 失败,并点名每个配置入口,同时路由保持注册、catalog 保持可浏览——首次运行的上手流程就是「浏览模型、存入密钥、再次发起提示」,中间无需任何重启。
适配器拥有的唯一注册事实是显式重试策略覆盖:其解析值变化或被移除时,插件原地重新注册该路由(同一适配器实例、一个同步区段)。因此,`ctx.llm.providerRetryPolicy('deepseek-official')` 会报告当前覆盖或 `LlmRuntime` 的部署默认值。
适配器拥有的注册事实是解析后的重试策略:其值变化时,插件原地重新注册该路由(同一适配器实例、一个同步区段)。因此,`ctx.llm.providerRetryPolicy('deepseek-official')` 会报告当前提供方策略;省略配置时,其中包含共享的 normal 模式五次重试默认值。
该插件还会在可配置提供方目录(`ctx.llm.listConfigurableProviders()`)中声明自己的路由:提供方为 `deepseek-official`settings namespace 为 `llm-deepseek`settings path 为空——整个分节就是 profile。配置界面借助该条目,把本适配器与休眠的 pi-ai 提供方一并呈现。
+3 -3
View File
@@ -66,8 +66,8 @@ export interface DeepSeekConnectionOptions {
models: readonly DeepSeekCatalogModel[]
/** Maximum provider idle time while one stream read is outstanding. */
streamIdleTimeoutMs: number
/** Explicit provider-owned model-request retry policy, already resolved. */
retryPolicy?: ResolvedRetryPolicy
/** Provider-owned model-request retry policy, already resolved. */
retryPolicy: ResolvedRetryPolicy
}
/** Constructor options for {@link DeepSeekAdapter}: the operation-local resolution hooks the plugin owns. */
@@ -166,7 +166,7 @@ export class DeepSeekAdapter extends LlmAdapter {
return { id: provider, name: 'DeepSeek' }
}
override providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined {
override providerRetryPolicy(_provider: string): ResolvedRetryPolicy {
return this.config.options().retryPolicy
}
+2 -4
View File
@@ -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 inherits the LLM deployment default. */
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
retryPolicy?: RetryPolicyConfig
}
@@ -193,9 +193,7 @@ export function resolveAdapterOptions(config: Config, environment?: LaunchEnviro
defaultContextWindow: config.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW,
models: resolveModels(config.models),
streamIdleTimeoutMs,
...config.retryPolicy === undefined ? {} : {
retryPolicy: resolveRetryPolicy(config.retryPolicy, 'llm-deepseek: retryPolicy'),
},
retryPolicy: resolveRetryPolicy(config.retryPolicy, 'llm-deepseek: retryPolicy'),
}
}
+2 -2
View File
@@ -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: f7e9421d6c782e0419b0a1ba4db659b8b2adeaf2
README.zh.md: 4a502a504fef2c02f4647ec1b9526e2054d2d53f
README.md: 208615408b7ed450961ce5c47bafe8127b273eb8
README.zh.md: 101fd563ea391c56fac9d6c33ef01c48671f791f
+3 -3
View File
@@ -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. Each profile may set an explicit `retryPolicy`; omission inherits `LlmRuntime`'s deployment default. `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`, and `retryPolicy`. An explicit profile retry policy is captured with that provider route; omission inherits the LLM deployment default, which itself falls back to bounded normal behavior. 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. 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`, 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. 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`.
@@ -201,4 +201,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 supply nested `retryPolicy`; omission inherits `LlmRuntime`'s deployment default, 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.
- **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.
+3 -3
View File
@@ -8,7 +8,7 @@
## 配置
按提供方配置凭据、模型 catalog 与部署特定传输设置,并以提供方路由本身为键。每个 profile 都可以设置显式 `retryPolicy`;省略时继承 `LlmRuntime` 的部署默认值`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``retryPolicy`显式 profile 重试策略会随该提供方路由一同捕获;省略时继承 LLM 部署默认值,而该默认值自身会回退到有界 normal 行为。流空闲间隔必须是正的有限 Node 定时器延迟,默认为五分钟,且只覆盖未完成提供方读取,不包括消费方思考时间。若已配置标头中有同名项,则以 Harness 应用归因为准。
受支持的 profile 字段是 `apiKeyEnv``displayName``api``baseURL``models``modelOverrides``compat``defaultContextWindow``defaultMaxTokens``defaultInput``headers``reasoning``thinkingBudgets``cacheRetention``transport``timeoutMs``websocketConnectTimeoutMs``streamIdleTimeoutMs``retryPolicy`每条 profile 解析后的重试策略会随该提供方路由一同捕获;省略时使用共享的有界 normal 默认值并重试五次。流空闲间隔必须是正的有限 Node 定时器延迟,默认为五分钟,且只覆盖未完成提供方读取,不包括消费方思考时间。若已配置标头中有同名项,则以 Harness 应用归因为准。
适配器强制 pi-ai SDK `maxRetries` 为零,因此一次 `stream()` 调用只会发起一次提供方请求。已移除 profile 字段 `maxRetries``maxRetryDelayMs` 会使加载失败,而不是静默倍增或隐藏单独组合的 agent(智能体)级重试预算。空闲超时会 abort SDK 的稳定请求信号,并以 `TIMEOUT` 呈现;较早的调用方 abort 仍为 `ABORTED`
@@ -202,4 +202,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`;省略时继承 `LlmRuntime` 的部署默认值`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()` 调用仍只尝试一次。
+6 -8
View File
@@ -136,7 +136,7 @@ export interface PiAiProviderProfile {
websocketConnectTimeoutMs?: number
/** Maximum provider idle time while one stream read is outstanding. */
streamIdleTimeoutMs?: number
/** Provider-owned model-request retry policy; omission inherits the LLM deployment default. */
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
retryPolicy?: RetryPolicyConfig
}
@@ -151,8 +151,8 @@ export interface ResolvedPiAiProviderProfile
apiKeyEnv?: CredentialRef
/** Positive finite provider-idle interval after defaulting. */
streamIdleTimeoutMs: number
/** Explicit immutable retry policy captured with this provider route. */
retryPolicy?: ResolvedRetryPolicy
/** Immutable retry policy captured with this provider route. */
retryPolicy: ResolvedRetryPolicy
/**
* The pi-ai provider this route registers, built from the resolved models.
* Construction happens here so an unserviceable protocol or an underspecified
@@ -294,8 +294,8 @@ function rejectRemovedFields(provider: string, source: PiAiProviderProfile): voi
* Validate profiles and return a detached route-keyed map suitable for
* per-request reads. This is the one explicit resolve step, so an omitted dict
* resolves to the empty (dormant) route set here rather than through a hidden
* fallback, and each route's models, explicit retry policy, and pi-ai provider
* are materialized once.
* fallback, and each route's models, retry policy, and pi-ai provider are
* materialized once.
* @param providers - configured provider profiles keyed by route.
* @returns validated profiles in configuration order.
*/
@@ -355,9 +355,7 @@ export function resolveProfiles(
displayName,
...apiKeyEnv === undefined ? {} : { apiKeyEnv: credentialRef(apiKeyEnv) },
streamIdleTimeoutMs,
...retryPolicy === undefined ? {} : {
retryPolicy: resolveRetryPolicy(retryPolicy, `llm-pi-ai: provider "${provider}" retryPolicy`),
},
retryPolicy: resolveRetryPolicy(retryPolicy, `llm-pi-ai: provider "${provider}" retryPolicy`),
...rest.headers === undefined ? {} : { headers: { ...rest.headers } },
...rest.thinkingBudgets === undefined ? {} : { thinkingBudgets: { ...rest.thinkingBudgets } },
configuredMaxTokens: catalog.configuredMaxTokens,
+2 -2
View File
@@ -5,7 +5,7 @@
* pi-ai does not ship is declared outright. Profile facts resolve per request
* over the optional `llm-pi-ai` user-settings section and the optional
* credential seam, so a changed key, endpoint, model, or knob reaches the next
* request without a restart; a changed *route set* (or a route's explicit,
* request without a restart; a changed *route set* (or a route's
* registration-captured retry policy) re-registers the same adapter instance
* in place.
*
@@ -99,7 +99,7 @@ function registrationFacts(profiles: ReadonlyMap<string, ResolvedPiAiProviderPro
.map(([provider, profile]) => ({
provider,
displayName: profile.displayName,
...profile.retryPolicy === undefined ? {} : { retryPolicy: profile.retryPolicy },
retryPolicy: profile.retryPolicy,
}))
.sort((left, right) => left.provider.localeCompare(right.provider))
}
+1 -1
View File
@@ -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 -2
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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 -2
View File
@@ -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: 91d4b745c2e179c1d943628ea7c4337cb48291f1
README.zh.md: f9492e6cd3edb5b6b969cfde0fb8a759f95ae77c
README.md: 1ff98b9103f434a0d3669b13eec291ec68ba55f7
README.zh.md: 3e3683c3465d83125f455780c6a0104e3998fba5
+4 -4
View File
@@ -8,9 +8,9 @@ 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.
### Configuration
### Retry policy
`defaultRetryPolicy` is the deployment policy inherited by every provider route whose adapter supplies no explicit override. Omitting it preserves the bounded normal default of two retries. An adapter override wins, and the effective resolved policy is captured with the route registration; this service stores that policy but does not execute retries.
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
@@ -21,7 +21,7 @@ An adapter registry plus a single streaming call API, interceptable via a waterf
- `ctx.llm.registerModelDiscovery(settingsNs: string, discover): () => void` Offer to interrogate provider endpoints for the settings namespace this plugin owns. One offer per namespace (`INVALID_DISCOVERY`/`DUPLICATE_DISCOVERY`), disposed with the calling fiber.
- `ctx.llm.listModelDiscoveryNamespaces(): string[]` List the namespaces that can interrogate an endpoint, so a surface offers the action only where it works.
- `ctx.llm.discoverModels(settingsNs: string, request: LlmModelDiscoveryRequest): Promise<LlmDiscoveredModel[]>` Ask one endpoint which models it advertises.
- `ctx.llm.providerRetryPolicy(provider: string): ResolvedRetryPolicy` Return the effective retry policy captured during registration: the adapter override when present, otherwise the deployment default.
- `ctx.llm.providerRetryPolicy(provider: string): ResolvedRetryPolicy` Return the provider-owned retry policy captured during registration, with normal defaults already resolved.
- `ctx.llm.listModels(provider: string): Promise<LlmModelInfo[]>` Discover the models one registered provider currently advertises.
- `ctx.llm.resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>` Resolve validated exact-model identity plus available context, output-default, and reasoning metadata from the owning adapter, with optional cancellation for asynchronous adapters.
- `ctx.llm.resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>` Validate an explicit effort and materialize adapter-configured call defaults without clamping.
@@ -48,7 +48,7 @@ Exact-model metadata is a separate correctness query, not a catalog decoration o
### Extension points
- Subclass `LlmAdapter` and call `ctx.llm.registerAdapter(providers, adapter)` to add one or more provider routes. `GenerateOptions.provider` selects the adapter; `GenerateOptions.model` is adapter-owned and may be resolved dynamically. Override `providerRetryPolicy()` only for an explicit provider-owned recovery policy; omission inherits `LlmRuntime`'s deployment default. Override `providerInfo()` and asynchronous `listModels()` to expose selector metadata, then implement `resolveModel()` when exact identity, capacity, an output default, or selectable reasoning efforts are available; an asynchronous resolver must honor its optional cancellation signal. The defaults use the route and model ids as names, advertise no models, and return no capacity, output default, or reasoning metadata.
- Subclass `LlmAdapter` and call `ctx.llm.registerAdapter(providers, adapter)` to add one or more provider routes. `GenerateOptions.provider` selects the adapter; `GenerateOptions.model` is adapter-owned and may be resolved dynamically. Override `providerRetryPolicy()` to supply provider-owned recovery configuration, `providerInfo()` and asynchronous `listModels()` to expose selector metadata, then implement `resolveModel()` when exact identity, capacity, an output default, or selectable reasoning efforts are available; an asynchronous resolver must honor its optional cancellation signal. The defaults use bounded normal retry policy, use the route and model ids as names, advertise no models, and return no capacity, output default, or reasoning metadata.
- Wrap `llm/stream` via `ctx.on()` waterfall listeners for caching, logging, or routing. A wrapper that retries after emitting a chunk has no durable attempt boundary; shipped agent retry policy therefore uses `agent/request-error` instead.
### Messages (`message.ts`) and content blocks (`types.ts`)
+4 -4
View File
@@ -8,9 +8,9 @@
一个适配器注册表加单一流式调用接口,可通过 waterfall(瀑布式事件)拦截。
### 配置
### 重试策略
`defaultRetryPolicy` 是每条未由适配器提供显式覆盖的提供方路由所继承的部署策略。省略它会保留两次重试的有界 normal 默认值。适配器覆盖优先,解析后的有效策略会随路由注册一同捕获;本服务存储策略,但不执行重试。
每个提供方适配器都会提供解析后的路由策略。省略提供方配置时使用有界 normal mode,在首次请求后最多重试五次。分层配置把 `mode` 改为 `always` 后可能残留 `maxRetries``retryableCodes`;解析过程会忽略这些不再生效的 normal-mode 字段,并捕获纯 always 策略。本服务存储有效策略,但不执行重试。
### 公开 API
@@ -21,7 +21,7 @@
- `ctx.llm.registerModelDiscovery(settingsNs: string, discover): () => void` 为本插件拥有的 settings namespace 提供查询提供方端点的能力。每个 namespace 只能有一个(`INVALID_DISCOVERY`/`DUPLICATE_DISCOVERY`),并随调用 fiber dispose。
- `ctx.llm.listModelDiscoveryNamespaces(): string[]` 列出可以询问端点的 namespace,让界面只在可用之处提供该动作。
- `ctx.llm.discoverModels(settingsNs: string, request: LlmModelDiscoveryRequest): Promise<LlmDiscoveredModel[]>` 询问某个端点它公布了哪些模型。
- `ctx.llm.providerRetryPolicy(provider: string): ResolvedRetryPolicy` 返回注册时捕获的有效重试策略:有适配器覆盖时使用该覆盖,否则使用部署默认值
- `ctx.llm.providerRetryPolicy(provider: string): ResolvedRetryPolicy` 返回注册时捕获的提供方重试策略,其中 normal 默认值已经解析
- `ctx.llm.listModels(provider: string): Promise<LlmModelInfo[]>` 发现某个已注册提供方当前公布的模型。
- `ctx.llm.resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>` 从拥有该精确路由的适配器中,解析并校验确切模型身份,以及可用上下文、输出默认值和推理(reasoning)元数据;异步适配器可选地支持取消。
- `ctx.llm.resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>` 校验显式推理强度,并填入适配器配置的调用默认值,但不自动调整。
@@ -48,7 +48,7 @@
### 扩展点
- 继承 `LlmAdapter` 并调用 `ctx.llm.registerAdapter(providers, adapter)`,添加一条或多条提供方路由。`GenerateOptions.provider` 选择适配器;`GenerateOptions.model` 属于适配器,可以动态解析。仅在提供方拥有显式恢复策略时覆盖 `providerRetryPolicy()`;省略时继承 `LlmRuntime` 的部署默认值。覆盖 `providerInfo()` 和异步 `listModels()` 以公开 selector 元数据;精确身份、容量、输出默认值或可选推理强度可用时,实现 `resolveModel()`;异步解析器必须响应其可选的取消 signal。默认实现将路由和模型 id 用作名称,不公布模型,也不返回容量、输出默认值或推理元数据。
- 继承 `LlmAdapter` 并调用 `ctx.llm.registerAdapter(providers, adapter)`,添加一条或多条提供方路由。`GenerateOptions.provider` 选择适配器;`GenerateOptions.model` 属于适配器,可以动态解析。覆盖 `providerRetryPolicy()` 可提供由提供方持有的恢复配置;覆盖 `providerInfo()` 和异步 `listModels()` 以公开 selector 元数据;精确身份、容量、输出默认值或可选推理强度可用时,实现 `resolveModel()`;异步解析器必须响应其可选的取消 signal。默认实现使用有界 normal 重试策略,将路由和模型 id 用作名称,不公布模型,也不返回容量、输出默认值或推理元数据。
- 包装 `llm/stream` 时,通过 `ctx.on()` waterfall listener 实现缓存、日志或路由。包装层如果在已经发出分片后重试,就没有可持久记录的尝试边界;因此,随产品交付的 agent 重试策略改用 `agent/request-error`
### 消息(`message.ts`)与内容块(`types.ts`
+8 -23
View File
@@ -7,7 +7,6 @@
*/
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type {
GenerateOptions,
LlmConfigurableProvider,
@@ -22,8 +21,8 @@ import type {
StreamChunk,
} from './types.ts'
import { freezeMessage, type Message } from './message.ts'
import { resolveRetryPolicy, RetryPolicySchema } from './retry-policy.ts'
import type { ResolvedRetryPolicy, RetryPolicyConfig } from './retry-policy.ts'
import { resolveRetryPolicy } from './retry-policy.ts'
import type { ResolvedRetryPolicy } from './retry-policy.ts'
import type { ProviderRequestId } from './brand.ts'
import { callConfigEquals, deepFreeze } from './call-config.ts'
import type { LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.ts'
@@ -172,12 +171,6 @@ export interface PreparedLlmCall {
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
}
/** Deployment-wide defaults applied to provider routes without adapter-owned overrides. */
export interface Config {
/** Model-request retry policy inherited by routes whose adapter omits one; omission uses normal defaults. */
defaultRetryPolicy?: RetryPolicyConfig
}
/**
* Provider-wire adapter for the harness message and stream vocabulary. Register implementations
* with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
@@ -195,9 +188,9 @@ export abstract class LlmAdapter {
}
/**
* Return an explicit provider-owned retry policy override for this route.
* Return the provider-owned retry policy captured with this route.
* @param _provider - a route passed to `registerAdapter()` for this instance.
* @returns a resolved override, or `undefined` to inherit the LLM deployment default.
* @returns a resolved policy, or `undefined` to use the normal defaults.
*/
providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined {
return undefined
@@ -289,22 +282,14 @@ export interface DirectoryRegistrationHandle {
* API, interceptable via the `llm/stream` waterfall.
*/
export class LlmRuntime extends Service {
static Config: z<Config> = z.object({
defaultRetryPolicy: RetryPolicySchema,
})
private adapters = new Map<string, AdapterRegistration>()
private directory = new Map<string, LlmConfigurableProvider>()
private discoveries = new Map<
string,
(request: LlmModelDiscoveryRequest) => Promise<readonly LlmDiscoveredModel[]>
>()
/** Resolved once because service configuration is fixed for this runtime instance. */
private readonly defaultRetryPolicy: ResolvedRetryPolicy
constructor(ctx: Context, config: Config = {}) {
constructor(ctx: Context) {
super(ctx, 'llm')
this.defaultRetryPolicy = resolveRetryPolicy(config.defaultRetryPolicy, 'llm: defaultRetryPolicy')
}
/** Notify topology observers without letting one broken listener veto the commit. */
@@ -399,7 +384,7 @@ export class LlmRuntime extends Service {
}
unique.add(provider)
const retryPolicy = adapter.providerRetryPolicy(provider)
?? this.defaultRetryPolicy
?? resolveRetryPolicy(undefined, `llm: provider "${provider}" retryPolicy`)
registrations.push({
adapter,
provider: { id: info.id, name: info.name },
@@ -573,9 +558,9 @@ export class LlmRuntime extends Service {
}
/**
* Read the effective retry policy captured when one provider route was registered.
* Read the retry policy captured when one provider route was registered.
* @param provider - registered provider route to inspect.
* @returns the adapter override or deployment default, fully resolved.
* @returns the provider-owned policy, with normal defaults already resolved.
*/
providerRetryPolicy(provider: string): ResolvedRetryPolicy {
return this.registration(provider).retryPolicy
+9 -5
View File
@@ -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[]
@@ -53,7 +53,7 @@ export interface AlwaysRetryPolicyConfig {
backoff?: BackoffConfig
}
/** Model-request retry policy configuration for a provider route or deployment default. */
/** Model-request retry policy configuration for one provider route. */
export type RetryPolicyConfig = NormalRetryPolicyConfig | AlwaysRetryPolicyConfig
/** Fully resolved backoff shared by both retry modes. */
@@ -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 {
@@ -138,7 +142,7 @@ function resolveBackoff(config: BackoffConfig | undefined, path: string): Resolv
/**
* Validate, default, and detach one retry policy.
* @param config - optional route or deployment configuration; omission selects normal defaults.
* @param config - optional provider configuration; omission selects normal defaults.
* @param path - diagnostic path naming the configuration that owns the value.
* @returns an immutable policy safe to capture in provider registration state.
*/
+16 -2
View File
@@ -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) => {
+2 -25
View File
@@ -203,7 +203,7 @@ describe('LlmRuntime', () => {
expect(adapter.lastOptions?.messages[0]).toBe(message)
})
it('captures an adapter retry override ahead of the deployment default', async () => {
it('captures provider-owned retry policies and defaults omitted policies', async () => {
const configured = resolveRetryPolicy({ mode: 'always' }, 'test retryPolicy')
const adapter = new class extends ScriptedAdapter {
override providerRetryPolicy(provider: string) {
@@ -211,9 +211,7 @@ describe('LlmRuntime', () => {
}
}(SCRIPT)
const ctx = new Context()
await ctx.plugin(LlmRuntime, {
defaultRetryPolicy: { mode: 'normal', maxRetries: 5 },
})
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['configured', 'defaulted'], adapter)
expect(ctx.llm.providerRetryPolicy('configured')).toBe(configured)
@@ -226,27 +224,6 @@ describe('LlmRuntime', () => {
)
})
it('uses bounded normal defaults when the deployment omits a retry policy', async () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['defaulted'], new ScriptedAdapter(SCRIPT))
expect(ctx.llm.providerRetryPolicy('defaulted')).toMatchObject({
mode: 'normal',
maxRetries: 2,
})
})
it('rejects an invalid deployment retry policy before any route registers', async () => {
const ctx = new Context()
await expect(ctx.plugin(LlmRuntime, {
defaultRetryPolicy: {
mode: 'normal',
backoff: { initialDelayMs: 10, maxDelayMs: 5 },
},
})).rejects.toThrow(/llm: defaultRetryPolicy/)
})
it('keeps a prepared registration and retry policy after route replacement', async () => {
const oldPolicy = resolveRetryPolicy({ mode: 'always' }, 'old retryPolicy')
const newPolicy = resolveRetryPolicy({ mode: 'normal', maxRetries: 0 }, 'new retryPolicy')