dsh-llm-fallbacks 0.6.13 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![preview](https://raw.githubusercontent.com/omdsh-dev/dsh-llm-fallbacks/main/docs/preview.png)
2
+
1
3
  # dsh-llm-fallbacks
2
4
 
3
5
  [English](README.md) | [Chinese](README.zh-CN.md)
@@ -7,10 +9,10 @@
7
9
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
8
10
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
9
11
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
10
- ![dsh](https://img.shields.io/badge/DSH-0.2.1--rc.4-4B32C3.svg)
12
+ ![dsh](https://img.shields.io/badge/DSH-0.2.2--rc.1-4B32C3.svg)
11
13
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-llm-fallbacks?lang=en)](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
12
14
 
13
- Automatic provider/model fallback chains for dsh (DeepSeek Harness): when an agent's LLM requests keep failing — retries exhausted, auth errors, quota exceeded, rate limiting (429) — the plugin switches provider/model along the fallback chain for the current role, and the current step/turn continues on the target model: tasks are not interrupted by model problems.
15
+ Automatic provider/model fallback chains for dsh (DeepSeek Harness): the plugin registers N **named virtual chains**, each selectable in the model picker as **`FallbacksChain / <chain id>`** — a session routed to one walks that chain, so an LLM request that keeps failing on a listed code (auth, quota, rate limiting 429, …) is retried on the next available provider/model and tasks are not interrupted by model problems. A session on any **real** provider/model has **no plugin fallback**: `llm-retry` alone handles it. Create a chain in the Fallbacks settings card, then select its `FallbacksChain / <id>` row to get it.
14
16
 
15
17
  Works in both dsh front ends: the **web** profile (Settings → Plugins → Fallbacks card) and the **dsh-tui** terminal profile (`/fallbacks` session diagnostics, `/fallbacks config` readback, and the `/settings` fallbacks section for editing).
16
18
 
@@ -18,18 +20,20 @@ Works in both dsh front ends: the **web** profile (Settings → Plugins → Fall
18
20
 
19
21
  ### Automatic model failover
20
22
 
21
- Any agent — root or subagent — that keeps failing on a listed failure code walks its chain to the next available provider/model and keeps the step alive. No manual model switching, and nothing happens until you configure content.
23
+ A session routed to `FallbacksChain / <chain id>` — the root agent by selection, or a subagent whose role points at that chain (「指定降级链」) — walks it: a request that fails on a listed code **before emitting any output** is retried on the next available provider/model inside the same request, and the step stays alive. Sessions on a real provider/model get no plugin fallback at all. No manual model switching, and nothing happens until you configure a chain (empty `chains` is the only first-run state).
22
24
 
23
- - **Triggers**: `triggerCodes` (default `AUTH` / `QUOTA` / `RATE_LIMIT`) — **any** dsh failure code is accepted, including `CONTEXT_WINDOW_EXCEEDED`, which fails over to a larger-context candidate. Retryable failures (5xx, `TRANSPORT`, `TIMEOUT`, …) stay with llm-retry first.
25
+ - **Who gets a chain**: only sessions whose model selection routes to `FallbacksChain / <chain id>`. That is the root agent when the row is selected, and a subagent the seam mounted there at dispatch (local backend, no explicit route on the request). A real provider/model selection — root or subagent — is left entirely to llm-retry.
26
+ - **Triggers**: `triggerCodes` (default `AUTH` / `QUOTA` / `RATE_LIMIT`) — **any** dsh failure code is accepted, including `CONTEXT_WINDOW_EXCEEDED`, which fails over to a larger-context candidate. Retryable failures (5xx, `TRANSPORT`, `TIMEOUT`, …) stay with llm-retry first; each of its retries re-enters the walk, which starts past the head that just went on cooldown.
27
+ - **Partial output is committed**: once an attempt has emitted a chunk, a later failure is **not** switched inside the request — yielded output cannot be unsent. The failure surfaces to the loop; recovery is llm-retry re-entry (a fresh walk) or the terminal error.
24
28
  - **Recovery**: a failed / switched-away model is held out of selection for `cooldownMs`, and `revertPolicy: cooldown-expiry` returns to the primary model on expiry. The opt-in `recovery: half-open` mode (YAML-only) makes that return evidence-driven instead of timer-based.
25
- - **Safety valves**: `maxSwitchesPerStep` caps switches per step and `alwaysModeRetryCap` caps always-mode retries — chain loops cannot amplify latency.
26
- - **Visibility**: every switch is recorded in an info-level log line (from/to/role/reason) — no silent model switching.
29
+ - **Safety valve**: `maxSwitchesPerStep` caps switches per request walk (attempts ≤ cap + 1) — chain loops cannot amplify latency.
30
+ - **Visibility**: every switch is recorded in an info-level log line (from/to/role/reason/candidates) — no silent model switching.
27
31
 
28
32
  → [Automatic model failover](docs/features.md#automatic-model-failover) · [Context-window-aware fallback](docs/features.md#context-window-aware-fallback)
29
33
 
30
34
  ### Peak/valley time slots
31
35
 
32
- Time slots rotate the **effective root chain** by wall-clock windows: each row carries its own fallback chain, and the first row whose window contains the current moment replaces the all-day chain for the next root request — the all-day chain stays the last resort when no row matches. Rotation is a routing seed, not a failure decision: it consumes no cooldown and leaves the failure walk (fallback switch) untouched.
36
+ Time slots are configured **per chain** and rotate that chain's effective entries by wall-clock windows: the first row whose window contains the current moment replaces the chain's default entries for the next request — the chain's own entries stay the last resort when no row matches. Rotation is a routing seed, not a failure decision: it consumes no cooldown and leaves the failure walk (fallback switch) untouched.
33
37
 
34
38
  ![Time slots](docs/assets/screenshot-1-en.png)
35
39
 
@@ -50,7 +54,7 @@ GLM Peak and GLM Valley are offered in the card picker only when `zai-coding-cn`
50
54
 
51
55
  A subagent's role is declared in its dispatch prompt — the Assignment header `**Execute as**: <id>` — and it is resolved against the role ids declared in `roles.list`. Only that **declared header** delivers the child's identity: the role's `persona` is installed on the child as a system-prompt section, the child's own session gets exactly one `[role: <id>]` notice row, and its session header shows a role badge.
52
56
 
53
- Role **routing** is resolved separately, in four ordered stages, first hit winning — explicit `agentPreset` → **dispatch-declared** `**Execute as**: <id>` header → rules → LLM auto-match. If the child's dispatch prompt declares `**Execute as**: <id>`, that role's persona, `[role: <id>]` notice and badge are delivered **regardless of which routing stage wins** — an earlier explicit `agentPreset` can route the child as another role while the identity carriers stay the declared header's. A child with **no declared dispatch header** gets no persona, no notice row and no role half of the badge (the model half still renders when the child has a recorded route), even when rules or auto-match resolve a role for routing. Making the resolved role's chain head the child's model is plugin-originated and conditional: with the host `subagent-model-selection` policy enabled, an explicit authorized route is preserved (inject skipped) and an injected head must be inside the effective allowlist — an empty intersection injects nothing ([host policy](docs/features.md#host-subagent-model-selection-policy-dsh-012)).
57
+ Role **identity** is resolved at dispatch in two ordered stages, first hit winning — explicit `agentPreset` → the **dispatch-declared** `**Execute as**: <id>` header. If the child's dispatch prompt declares `**Execute as**: <id>`, that role's persona, `[role: <id>]` notice and badge are delivered. A child with **no declared dispatch header** is NOT SPECIFIED: no persona, no notice row, no role half of the badge — and the plugin does not intervene. The role can also carry a **manual chain pointer** (「指定降级链」): at dispatch the seam stamps a local, non-explicit-route child onto `FallbacksChain / <role.chain>` — an explicit route is **authorized** and never touched, and an external-backend child keeps pure inheritance. There is no rule matching and no host-policy consultation: the mapping is your static config.
54
58
 
55
59
  This covers an **ordinary subagent dispatch and an Agent Team teammate** (`spawn_teammate`) alike: both are children dispatched with the same header, which the model writes — the same dispatch seam, no mechanism-specific configuration.
56
60
 
@@ -85,30 +89,30 @@ The plugin's settings live in a shared `fallbacks:` namespace, editable from thr
85
89
 
86
90
  ```yaml
87
91
  fallbacks:
88
- rootChain: # all-day chain: leading entries = fallback walk, last = Default model (official)
89
- - anthropic/claude-3-5-sonnet
90
- - deepseek-official/deepseek-flash
91
- timeSlots: # optional: rotate the effective root chain by wall-clock windows
92
- - kind: preset
93
- preset: liang-peak
94
- chain:
92
+ chains: # named virtual chains: one picker row each (FallbacksChain/<id>)
93
+ - id: Auto # the picker model id (case-sensitive, unique)
94
+ label: Default chain # tab title + picker prefix (display only)
95
+ entries: # 默认降级链 …then the 默认模型 tail (exactly one official model)
95
96
  - anthropic/claude-3-5-sonnet
96
- roles: # optional: declare role entities, then reference them from rules
97
+ - deepseek-official/deepseek-v4-pro
98
+ timeSlots: [] # optional: this chain's own wall-clock rows
99
+ - id: Coding
100
+ label: Coding
101
+ entries:
102
+ - openai/gpt-4o-mini
103
+ - deepseek-official/deepseek-flash
104
+ roles: # optional: role identities + an optional chain POINTER
97
105
  list:
98
106
  - id: reviewer
99
107
  persona: Code-review subagents
100
- chain:
101
- - openai/gpt-4o-mini
102
- fallback: inherit-root
103
- rules: # subagent-only: rules never match root requests
104
- - role: reviewer
108
+ chain: Coding # 指定降级链: children under this role mount FallbacksChain/Coding
105
109
  ```
106
110
 
107
111
  Three rules of thumb:
108
112
 
109
- - **There is no feature switch**: the plugin intervenes exactly when content is configured — a non-empty `rootChain`, at least one `timeSlots` row, or one declared role. An empty section is a complete no-op, and the Plugins-page row toggle is the master switch.
110
- - **The all-day `rootChain` tail must be exactly one official model** — `deepseek-official/deepseek-flash` **or** `deepseek-official/deepseek-v4-pro` (XOR); the leading entries are the fallback walk, walked first.
111
- - **Slots and roles are optional**: the first time-slot row whose window matches replaces the all-day chain for the next root request; rules map subagents to declared roles and never match root requests.
113
+ - **There is no feature switch**: the plugin intervenes exactly when a chain is configured (`chains` non-empty). An empty section is a complete no-op, and the Plugins-page row toggle is the master switch.
114
+ - **A chain's LAST entry (默认模型) must be exactly one official model** — `deepseek-official/deepseek-flash` **or** `deepseek-official/deepseek-v4-pro` (XOR); the leading 默认降级链 entries are walked first.
115
+ - **Slots and roles are optional**: each chain's first matching time-slot row replaces that chain's entries; a role's `chain` pointer (or nothing) decides which chain a dispatched child mounts.
112
116
 
113
117
  Full reference (keys, entry syntax, selector rules, migration) → [docs/configuration.md](docs/configuration.md).
114
118
 
@@ -120,12 +124,12 @@ Save the config and restart the session, then type `/fallbacks` — the read-onl
120
124
 
121
125
  ## Feature highlights
122
126
 
123
- - **Automatic failover** for root and subagents — any agent walks its chain on a listed failure code and the step continues. → [details](docs/features.md#automatic-model-failover)
127
+ - **Automatic failover** on a virtual route — a `FallbacksChain / <id>` session (root or role-stamped subagent) walks its chain on a listed failure code and the step continues; real-model sessions have no plugin fallback. → [details](docs/features.md#automatic-model-failover)
124
128
  - **Context-window-aware fallback** — `CONTEXT_WINDOW_EXCEEDED` fails over to a larger-context candidate; the switch is request-scoped (no cooldown). → [details](docs/features.md#context-window-aware-fallback)
125
- - **Peak/valley time slots** — four frozen UTC+8 presets or custom windows rotate the effective root chain. → [details](docs/features.md#time-slots)
126
- - **Role-based subagents** — a dispatch-declared role brings persona, notice row and badge to the child; its chain head is injected subject to host policy. → [details](docs/features.md#role-based-subagents)
127
- - **FallbacksChain in the picker** — select `FallbacksChain / Auto` to make the chain the root primary; any real model stays fallback-only. → [details](docs/features.md#fallbackschain-in-the-model-picker)
128
- - **Host subagent model policy** — the dsh 0.1.2 allowlist is a hard constraint on every plugin-originated subagent route. → [details](docs/features.md#host-subagent-model-selection-policy-dsh-012)
129
+ - **Peak/valley time slots** — four frozen UTC+8 presets or custom windows rotate a chain's own effective entries. → [details](docs/features.md#time-slots)
130
+ - **Role-based subagents** — a dispatch-declared role brings persona, notice row and badge to the child, and its optional `chain` pointer puts the child on `FallbacksChain/<id>`. → [details](docs/features.md#role-based-subagents)
131
+ - **FallbacksChain in the picker** — select `FallbacksChain / <chain id>` to make the configured chain the session route; any real model is served by llm-retry alone, with no plugin fallback. → [details](docs/features.md#fallbackschain-in-the-model-picker)
132
+ - **Host subagent model policy** — dsh enforces its 0.1.2 child-model allowlist on the dispatching side; this plugin does not consult it. An explicit child selection is limited to the parent route unless the host allowlist admits it. → [details](docs/features.md#role-based-subagents)
129
133
  - **Five bundled preset roles** — `reviewer` / `scout` / `security-reviewer` / `sonic` / `task`, declared on apply. → [details](docs/features.md#preset-roles)
130
134
  - **dsh-tui surfaces** — `/fallbacks`, `/fallbacks config`, and a `/settings` section with web-card parity. → [details](docs/features.md#dsh-tui-surfaces)
131
135
 
@@ -139,7 +143,7 @@ The plugin installs as a **pure mount**: bundle insert + client inject + its own
139
143
  |---|---|
140
144
  | [docs/install.md](docs/install.md) | desktop/web plugin page / profile install (web + dsh-tui) / registry / git / local variants / uninstall / `--dump-config` verification |
141
145
  | [docs/configuration.md](docs/configuration.md) | full `fallbacks` namespace reference, selector syntax, example YAML, plugin-config card usage, TUI readback, behavior notes, preset roles |
142
- | [docs/features.md](docs/features.md) | capability reference: failover, context-window fallback, time slots, role-based subagents, picker, host policy, preset roles, agent presets, dsh-tui |
146
+ | [docs/features.md](docs/features.md) | capability reference: failover, context-window fallback, time slots, role-based subagents, picker, preset roles, agent presets, dsh-tui |
143
147
  | [docs/repair.md](docs/repair.md) | session-log repair: refusal classes, `--apply` publishing, flags, exit codes, lossy `--drop-legacy-events` |
144
148
  | [docs/consumer-api.md](docs/consumer-api.md) | developer consumption contract: library API + named `llm-fallbacks` service + role seeds, export inventory, lifecycle, typing |
145
149
  | [docs/release.md](docs/release.md) | release process: Trusted Publishing setup, Release prep SOP, fragment format, rollback |
package/README.zh-CN.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![preview](https://raw.githubusercontent.com/omdsh-dev/dsh-llm-fallbacks/main/docs/preview.zh.png)
2
+
1
3
  # dsh-llm-fallbacks
2
4
 
3
5
  [English](README.md) | [中文](README.zh-CN.md)
@@ -7,10 +9,10 @@
7
9
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
8
10
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
9
11
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
10
- ![dsh](https://img.shields.io/badge/DSH-0.2.1--rc.4-4B32C3.svg)
12
+ ![dsh](https://img.shields.io/badge/DSH-0.2.2--rc.1-4B32C3.svg)
11
13
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-llm-fallbacks?lang=zh)](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
12
14
 
13
- dsh(DeepSeek Harness)的自动模型降级插件:当 agent 的模型请求持续失败(重试耗尽、权限错误、配额超限、限流 429)时,插件按当前角色的 fallback 链切换 provider/model,当前 step/turn 在目标模型上继续——任务不会因模型问题中断。
15
+ dsh(DeepSeek Harness)的自动模型降级插件:插件注册 N 条**命名虚拟链**,每条在模型选择器中都是一行 **`FallbacksChain / <链 id>`**——选择某条链的会话沿该链行走,llm 请求在列出的失败码上持续失败(权限错误、配额超限、限流 429 等)时,请求会在下一个可用 provider/model 上重试,任务不会因模型问题中断。选择**真实** provider/model 的会话**没有插件降级**,只由 `llm-retry` 处理。在 Fallbacks 设置卡中创建链,然后选择它的 `FallbacksChain / <id>` 行即可。
14
16
 
15
17
  两个 dsh 前端均可用:**web** profile(设置 → 插件配置 → Fallbacks 卡片)与 **dsh-tui** 终端 profile(`/fallbacks` 会话诊断、`/fallbacks config` 回读、`/settings` 的 fallbacks 分区用于编辑)。
16
18
 
@@ -18,12 +20,14 @@ dsh(DeepSeek Harness)的自动模型降级插件:当 agent 的模型请求
18
20
 
19
21
  ### 自动模型降级
20
22
 
21
- root 或 subagent,只要持续命中触发码,就沿自身链切到下一个可用 provider/model,step 继续运行。无需手动切换模型;在配置了内容之前,插件不做任何事。
23
+ 会话模型选择走 `FallbacksChain / <链 id>` 时——root agent 选中该行,或子代理的角色以「指定降级链」指向该链——沿该链行走:请求在**输出任何内容之前**命中触发码,就在同一个请求内改到下一个可用 provider/model,step 继续运行。选择真实 provider/model 的会话不获得任何插件降级。无需手动切换模型;在配置链之前(`chains` 为空是唯一初始状态)插件不做任何事。
22
24
 
23
- - **触发**:`triggerCodes`(默认 `AUTH` / `QUOTA` / `RATE_LIMIT`)——**任何** dsh 失败码都可用,包括 `CONTEXT_WINDOW_EXCEEDED`(会降级到上下文窗口更大的候选)。可重试失败(5xx、`TRANSPORT`、`TIMEOUT` 等)先交给 llm-retry。
25
+ - **谁能拿到链**:只有模型选择走 `FallbacksChain / <链 id>` 的会话。包括选中该行的 root agent,以及角色「指定降级链」指向该链、分发时被盖章到该行的 subagent(本地 backend、请求上没有显式路由)。真实 provider/model 选择——无论 root 还是 subagent——完全交给 llm-retry。
26
+ - **触发**:`triggerCodes`(默认 `AUTH` / `QUOTA` / `RATE_LIMIT`)——**任何** dsh 失败码都可用,包括 `CONTEXT_WINDOW_EXCEEDED`(会降级到上下文窗口更大的候选)。可重试失败(5xx、`TRANSPORT`、`TIMEOUT` 等)先交给 llm-retry;它的每次重试都会重新进入 walk,并从刚进入冷却的链头之后开始。
27
+ - **已输出即提交**:某个尝试一旦吐出 chunk,之后的失败**不会**在该请求内切换——已产出的输出无法撤回。失败上抛给 loop;恢复路径是 llm-retry 重入(新的一次 walk)或终止失败。
24
28
  - **恢复**:失败或被切走的模型在 `cooldownMs` 内不再被选中,`revertPolicy: cooldown-expiry` 在冷却到期后回到主模型;可选的 `recovery: half-open` 模式(YAML-only)把这次回归改为证据驱动,而非定时器驱动。
25
- - **保险丝**:`maxSwitchesPerStep` 限制单 step 的切换次数,`alwaysModeRetryCap` 限制 always 模式的重试次数——链式循环不会放大延迟。
26
- - **可见性**:每次切换都写入 info 日志(from/to/role/reason),不存在静默切换。
29
+ - **保险丝**:`maxSwitchesPerStep` 限制单次请求 walk 的切换次数(尝试数 ≤ 上限 + 1)——链式循环不会放大延迟。
30
+ - **可见性**:每次切换都写入 info 日志(from/to/role/reason/candidates),不存在静默切换。
27
31
 
28
32
  → [自动模型降级](docs/features.md#automatic-model-failover) · [上下文窗口降级](docs/features.md#context-window-aware-fallback)
29
33
 
@@ -50,7 +54,7 @@ GLM Peak / GLM Valley 仅在配置了 `zai-coding-cn` 时才会出现在卡片
50
54
 
51
55
  subagent 的角色由分发提示词声明——Assignment 头部字段 `**Execute as**: <id>`;它与 `roles.list` 中已声明的角色 id 匹配。只有这个**已声明头部**才投递子代理的身份:`persona` 以 system-prompt 分区安装到子代理,子会话恰好写入一行 `[role: <id>]` 通知行,会话头部显示角色徽章。
52
56
 
53
- 角色**路由**是另一套解析,按四个有序阶段、先命中者生效——显式 `agentPreset` → **派发声明** `**Execute as**: <id>` 头部 → 规则 → LLM 自动匹配。只要子代理的分发提示词声明了 `**Execute as**: <id>`,该角色的 persona、`[role: <id>]` 通知行与徽章就会投递,**无论哪个路由阶段胜出**——更早的显式 `agentPreset` 可以把子代理路由成另一个角色,而身份载体仍属已声明头部。完全没有声明分发头部的子代理不会获得 persona、通知行与徽章,即使规则或自动匹配为路由解析出了角色。把解析到的角色链头作为子代理模型由插件发起、且带条件:宿主 `subagent-model-selection` 策略启用时,显式授权路由会被保留(跳过注入),注入的链头必须落在生效 allowlist 内——交集为空则不注入任何内容([宿主策略](docs/features.md#host-subagent-model-selection-policy-dsh-012))。
57
+ 角色**身份**在分发时按两个有序阶段解析、先命中者生效——显式 `agentPreset` → **派发声明** `**Execute as**: <id>` 头部。只要子代理的分发提示词声明了 `**Execute as**: <id>`,该角色的 persona、`[role: <id>]` 通知行与徽章就会投递。完全没有声明分发头部的子代理是**未指定**:没有 persona、通知行与徽章角色半边——插件也不干预。角色还可携带**手动链指针**(「指定降级链」):分发时 seam 把本地、无显式路由的子代理盖到 `FallbacksChain / <role.chain>` 上——带显式路由的请求属于**已授权**,插件永不改写;外部 backend 的子代理保持纯继承。没有规则匹配,也没有宿主策略咨询:映射就是你的静态配置。
54
58
 
55
59
  这一套同时覆盖**普通 subagent 分发与 Agent Team 队友**(`spawn_teammate`):两者都是带着同一头部(由模型写入)经同一分发 seam 创建的子代理,不需要任何按机制区分的配置。
56
60
 
@@ -85,29 +89,29 @@ dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui 终端 profile
85
89
 
86
90
  ```yaml
87
91
  fallbacks:
88
- rootChain: # 全时段链:前置项 = 降级链,末项 = 默认模型(官方模型)
89
- - anthropic/claude-3-5-sonnet
90
- - deepseek-official/deepseek-flash
91
- timeSlots: # 可选:按墙钟窗口轮换生效 root 链
92
- - kind: preset
93
- preset: liang-peak
94
- chain:
92
+ chains: # 命名虚拟链:每条 = picker 一行(FallbacksChain/<id>)
93
+ - id: Auto # picker 模型 id(大小写敏感、唯一)
94
+ label: 默认链 # tab 标题 + picker 前缀(仅展示)
95
+ entries: # 默认降级链 …末尾是默认模型(恰好一个官方模型)
95
96
  - anthropic/claude-3-5-sonnet
96
- roles: # 可选:先声明角色实体,再由规则引用
97
+ - deepseek-official/deepseek-v4-pro
98
+ timeSlots: [] # 可选:该链自己的分时行
99
+ - id: Coding
100
+ label: 编码
101
+ entries:
102
+ - openai/gpt-4o-mini
103
+ - deepseek-official/deepseek-flash
104
+ roles: # 可选:角色身份 + 可选链指针
97
105
  list:
98
106
  - id: reviewer
99
107
  persona: Code-review subagents
100
- chain:
101
- - openai/gpt-4o-mini
102
- fallback: inherit-root
103
- rules: # 仅 subagent:规则永不匹配 root 请求
104
- - role: reviewer
108
+ chain: Coding # 指定降级链:该角色派发的子代理挂到 FallbacksChain/Coding
105
109
  ```
106
110
 
107
111
  三条要点:
108
112
 
109
- - **没有功能开关**:只要配置了内容——非空 `rootChain`、至少一行 `timeSlots`、或一个已声明角色——插件才介入;空配置是完全的 no-op,Plugins 页的行开关是总开关。
110
- - **全时段 `rootChain` 的末项必须恰好是一个官方模型**——`deepseek-official/deepseek-flash` **或** `deepseek-official/deepseek-v4-pro`(XOR);前置项是降级链,优先进走。
113
+ - **没有功能开关**:只要配置了链(`chains` 非空)插件才介入;空配置是完全的 no-op,Plugins 页的行开关是总开关。
114
+ - **每条链的末项(默认模型)必须恰好是一个官方模型**——`deepseek-official/deepseek-flash` **或** `deepseek-official/deepseek-v4-pro`(XOR);前部「默认降级链」条目优先进走。
111
115
  - **分时槽与角色都是可选的**:第一个窗口命中的分时行会在下一个 root 请求取代全时段链;规则把 subagent 映射到已声明角色,且永不匹配 root 请求。
112
116
 
113
117
  完整参考(字段、条目语法、选择器规则、迁移)→ [docs/configuration.md](docs/configuration.md)。
@@ -120,12 +124,12 @@ fallbacks:
120
124
 
121
125
  ## 能力一览
122
126
 
123
- - **自动降级**(root 与 subagent 通用)——命中触发码的 agent 沿链切换,step 继续运行。 → [详情](docs/features.md#automatic-model-failover)
127
+ - **自动降级**(仅虚拟路由)——`FallbacksChain / <id>` 会话(root 或角色盖章的 subagent)命中触发码时沿链切换,step 继续运行;真实模型会话没有插件降级。 → [详情](docs/features.md#automatic-model-failover)
124
128
  - **上下文窗口感知降级**——`CONTEXT_WINDOW_EXCEEDED` 会切到窗口更大的候选;该切换是请求级的(不消耗冷却)。 → [详情](docs/features.md#context-window-aware-fallback)
125
- - **峰谷无忧(分时槽)**——四个冻结的 UTC+8 预设或自定义窗口轮换生效 root 链。 → [详情](docs/features.md#time-slots)
126
- - **角色化 subagent**——派发声明的角色为子代理带来 persona、通知行与徽章;链头注入受宿主策略约束。 → [详情](docs/features.md#role-based-subagents)
127
- - **模型选择器中的 FallbacksChain**——选择 `FallbacksChain / Auto` 即把配置链作为 root 主模型;选择真实模型仍是仅降级。 → [详情](docs/features.md#fallbackschain-in-the-model-picker)
128
- - **宿主子代理模型策略**——dsh 0.1.2 的 allowlist 是插件发起的所有 subagent 路由的硬约束。 → [详情](docs/features.md#host-subagent-model-selection-policy-dsh-012)
129
+ - **峰谷无忧(分时槽)**——四个冻结的 UTC+8 预设或自定义窗口轮换该链自己的生效条目。 → [详情](docs/features.md#time-slots)
130
+ - **角色化 subagent**——派发声明的角色为子代理带来 persona、通知行与徽章,其可选的 `chain` 指针把子代理挂到 `FallbacksChain/<id>`。 → [详情](docs/features.md#role-based-subagents)
131
+ - **模型选择器中的 FallbacksChain**——选择 `FallbacksChain / <链 id>` 即由配置链承载该会话;选择真实模型则只由 llm-retry 处理,没有插件降级。 → [详情](docs/features.md#fallbackschain-in-the-model-picker)
132
+ - **宿主子代理模型策略**——dsh 在**派发侧**执行自己的 0.1.2 子模型 allowlist;本插件不读取它。显式子代理选择只允许父路由,除非该路由在宿主 allowlist 内。 → [详情](docs/features.md#role-based-subagents)
129
133
  - **五个内置预设角色**——`reviewer` / `scout` / `security-reviewer` / `sonic` / `task`,随 apply 自动声明。 → [详情](docs/features.md#preset-roles)
130
134
  - **dsh-tui 操作面**——`/fallbacks`、`/fallbacks config`,以及与 web 卡片等价的 `/settings` 分区。 → [详情](docs/features.md#dsh-tui-surfaces)
131
135
 
@@ -139,7 +143,7 @@ fallbacks:
139
143
  |---|---|
140
144
  | [docs/install.md](docs/install.md) | 桌面/Web 插件页 / profile 安装(web + dsh-tui)/ registry / git / 本地目录 / 卸载 / `--dump-config` 校验 |
141
145
  | [docs/configuration.md](docs/configuration.md) | `fallbacks` 命名空间完整参考、选择器语法、示例 YAML、插件配置卡片用法、TUI 回读、行为说明、预设角色 |
142
- | [docs/features.md](docs/features.md) | 能力参考:降级、上下文窗口降级、分时槽、角色化 subagent、选择器、宿主策略、预设角色、agent 预设、dsh-tui |
146
+ | [docs/features.md](docs/features.md) | 能力参考:降级、上下文窗口降级、分时槽、角色化 subagent、选择器、预设角色、agent 预设、dsh-tui |
143
147
  | [docs/repair.md](docs/repair.md) | 会话日志修复:拒载类别、`--apply` 发布、参数、退出码、有损 `--drop-legacy-events` |
144
148
  | [docs/consumer-api.md](docs/consumer-api.md) | 开发者消费契约:库 API + 具名 `llm-fallbacks` 服务 + 角色种子、导出清单、生命周期、类型 |
145
149
  | [docs/release.md](docs/release.md) | 发布流程:Trusted Publishing 配置、Release prep SOP、fragment 格式、回滚 |
@@ -0,0 +1,197 @@
1
+ /**
2
+ * The chain walk (design dsh-022-rc1-upgrade §2/§3/§6.1): the fallback
3
+ * decision, executed inside the virtual `FallbacksChain/Auto` adapter's
4
+ * `stream()` instead of on the `agent/request` / `agent/request-error`
5
+ * waterfalls.
6
+ *
7
+ * One walk per `stream()` call: resolve the selected named chain and its
8
+ * slot-effective tail, then attempt the chain's heads in order. Root and
9
+ * subagent walks are the SAME shape — every walk is the selected chain. A
10
+ * pre-output terminal `finish` whose failure code is a trigger code is
11
+ * swallowed, booked (cooldown / recovery) and the walk advances; anything
12
+ * else — a normal finish, an aborted finish, a non-trigger failure, or any
13
+ * failure after the attempt already yielded output — is forwarded verbatim
14
+ * and ends the walk. A `usage` chunk is metadata, so it neither commits the
15
+ * attempt nor stops the advance.
16
+ *
17
+ * A pre-output parameter rejection (`UNSUPPORTED_REASONING_EFFORT`, or a
18
+ * `max_tokens`-shaped `INVALID_REQUEST`) is repaired first: the SAME candidate
19
+ * is retried once with the offending control dropped, because a route that
20
+ * discloses no capability only reveals its real cap through that rejection.
21
+ *
22
+ * The COMMITTED rule (§3.1 / §7.5) is why a post-output failure cannot be
23
+ * replayed: yielded chunks cannot be unsent, so a second provider's output
24
+ * cannot be concatenated behind a partial one. Such a failure is still BOOKED
25
+ * (cooldown / recovery) before it is forwarded — the attempt boundary is where
26
+ * both transactions happen — and then surfaces to the loop, which settles the
27
+ * partial attempt; llm-retry (or the terminal error) takes over from there, and
28
+ * a retry re-enters this walk, which resumes past the cooled head on its own.
29
+ *
30
+ * Bookkeeping survives a walk exactly as far as the state store does: cooldown
31
+ * (`cooldownMs` / `revertPolicy` / half-open escalation) is the cross-call
32
+ * memory keyed by session, so an llm-retry re-entry starts past a failed head;
33
+ * the per-call failed set and the switch cap live only for this walk
34
+ * (`maxSwitchesPerStep` = switches per walk, attempts ≤ cap + 1).
35
+ *
36
+ * The pre-attempt guards (`UNKNOWN_CHAIN` / `EMPTY_EFFECTIVE_CHAIN` /
37
+ * `UNDISPATCHABLE_EFFECTIVE_HEAD` / `LLM_UNAVAILABLE`) are owned here, and
38
+ * `slotWinners` carries the per-(agent, chain) 分时切换 rotation log.
39
+ *
40
+ * @module dsh-llm-fallbacks/chain-walk
41
+ */
42
+ import { type GenerateOptions, type LlmRuntime, type StreamChunk } from '@deepseek-ai/dsh-llm';
43
+ import { type FailingModel } from './chains.ts';
44
+ import type { FallbacksConfig } from './config.ts';
45
+ import type { FallbackStateStore } from './state.ts';
46
+ /**
47
+ * `LlmError` code: the effective chain is empty — the selected named chain has
48
+ * no entries to walk (the pre-attempt guard; the walk is the only point that
49
+ * knows WHICH chain the session selected, `options.model` being the chain id).
50
+ */
51
+ export declare const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
52
+ /**
53
+ * `LlmError` code: the chain cannot be walked — its entries tail is not
54
+ * conforming (the last entry must be exactly one official model), or no entry
55
+ * is a dispatchable real pair (wildcard selector, malformed selector, or a
56
+ * self-route back to `FallbacksChain/*` — the recursion guard).
57
+ */
58
+ export declare const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
59
+ /**
60
+ * `LlmError` code: `options.model` names no declared chain — reachable only
61
+ * once a chain the session selected is deleted or renamed (the migrated
62
+ * default id `Auto` is unchanged, so an upgrade alone never hits this).
63
+ * Thrown synchronously before any chunk; the host surfaces a terminal error
64
+ * finish and the user re-selects in the picker. No silent fallback to
65
+ * another chain — picking the wrong named chain silently would be a
66
+ * data-routing surprise.
67
+ */
68
+ export declare const UNKNOWN_CHAIN_CODE = "UNKNOWN_CHAIN";
69
+ /** `LlmError` code (defensive): the `llm` runtime disappeared mid-flight. */
70
+ export declare const LLM_UNAVAILABLE_CODE = "LLM_UNAVAILABLE";
71
+ /**
72
+ * dsh core's canonical failure code for a request larger than the model's
73
+ * context window (`@deepseek-ai/dsh-llm` `CONTEXT_WINDOW_EXCEEDED_CODE`; the
74
+ * DeepSeek adapter's `httpErrorCode()` and llm-pi-ai's `mapStopReason()` both
75
+ * map the provider 400 "maximum context length" to it). Declared as a literal
76
+ * for the same reason `triggerCodes` is `string[]` — the plugin compares dsh
77
+ * failure codes as plain strings and never imports a peer value.
78
+ *
79
+ * It is NOT in llm-retry's retryable set, so the plugin sees it only when the
80
+ * operator lists it in `triggerCodes` — and once listed, this walk handles the
81
+ * rejection BEFORE the harness's compaction plugin compacts anything
82
+ * (measured 2026-09-09: the first overflow switches to the fallback, no
83
+ * compaction happens). Operators who want compaction tried first leave the
84
+ * code out of `triggerCodes`.
85
+ *
86
+ * A `CONTEXT_WINDOW_EXCEEDED` switch is REQUEST-scoped
87
+ * ({@link REQUEST_SCOPED_CODES}): the route is healthy and the request was too
88
+ * big, so the per-call failed key is recorded but no cooldown and no recovery
89
+ * counter are written.
90
+ */
91
+ export declare const CONTEXT_WINDOW_EXCEEDED_CODE = "CONTEXT_WINDOW_EXCEEDED";
92
+ /**
93
+ * Failure codes whose switch is REQUEST-scoped: the route is healthy and the
94
+ * request was the problem, so a switch records the per-call failed key but
95
+ * writes no cooldown and no recovery counter. Cooling the route down would
96
+ * send the next SHORT prompt to the fallback for no reason.
97
+ */
98
+ export declare const REQUEST_SCOPED_CODES: ReadonlySet<string>;
99
+ /**
100
+ * The exact-route capabilities a walk reads from `resolveModelInfo`: the
101
+ * context window (wildcard / overflow expansions) plus the two request
102
+ * controls parameter adaptation compares against. Every field is optional — a
103
+ * host that discloses nothing leaves every request untouched.
104
+ */
105
+ interface ModelInfoLike {
106
+ context?: {
107
+ contextWindow?: number;
108
+ };
109
+ defaultMaxTokens?: number;
110
+ reasoning?: {
111
+ efforts: readonly {
112
+ id: string;
113
+ name: string;
114
+ }[];
115
+ };
116
+ }
117
+ /**
118
+ * Model-catalog view the wildcard / context-window probes read. `contextWindow`
119
+ * is a host-disclosed extra on the advisory catalog row (the dsh `LlmModelInfo`
120
+ * does not type it), while `resolveModelInfo` is where the host actually
121
+ * carries it — both are optional and read structurally, so a host offering
122
+ * neither reports every window as unknown (never a skip).
123
+ */
124
+ export interface ModelCatalogLike {
125
+ listModels(provider: string): Promise<readonly {
126
+ id: string;
127
+ contextWindow?: number;
128
+ }[]>;
129
+ resolveModelInfo?(provider: string, model: string): Promise<ModelInfoLike>;
130
+ }
131
+ /**
132
+ * The `provider/*`-entry existence probe: the target provider's advertised
133
+ * catalog, fetched once per expansion and cached per provider. A
134
+ * missing/unknown provider or a failing catalog reads as "no such model", so
135
+ * wildcard candidates to it are skipped; without a catalog no filtering
136
+ * happens (`() => true`).
137
+ *
138
+ * simplify: catalog fetched per expansion, never cached across walks. Walks
139
+ * are failure-driven and rare; cache per provider in the plugin if they ever
140
+ * become hot.
141
+ */
142
+ export declare function makeModelExists(catalog: ModelCatalogLike | undefined, providers: readonly string[]): Promise<(provider: string, model: string) => boolean>;
143
+ /**
144
+ * The context-window lookup for a `CONTEXT_WINDOW_EXCEEDED` expansion: a
145
+ * `provider/model → contextWindow` map over the failing route plus every
146
+ * resolved candidate route, fetched once per expansion (the
147
+ * {@link makeModelExists} pattern — failure-driven and rare, never cached).
148
+ *
149
+ * Two sources, cheapest first: the advertised catalog row when it discloses a
150
+ * window (one `listModels` per distinct provider), then — for routes still
151
+ * unknown — the exact-route `resolveModelInfo` metadata, which is where the
152
+ * dsh host puts `context.contextWindow`. A missing catalog, a throwing probe,
153
+ * or a route that discloses nothing all read as UNKNOWN (`undefined`), and
154
+ * unknown always KEEPS the candidate: an undisclosed window must never empty a
155
+ * chain.
156
+ */
157
+ export declare function makeContextWindowOf(catalog: ModelCatalogLike | undefined, routes: readonly FailingModel[]): Promise<(provider: string, model: string) => number | undefined>;
158
+ /**
159
+ * Key of one (agent, chain) 分时切换 marker. Both the walk's write and the
160
+ * wiring's `agent/disposed` cleanup build keys through here, so the two halves
161
+ * of the contract cannot drift apart.
162
+ */
163
+ export declare function slotMarkerKey(agentId: string, chainId: string): string;
164
+ /** Key prefix covering every chain marker of one agent (the cleanup sweep). */
165
+ export declare function slotMarkerPrefix(agentId: string): string;
166
+ /** Everything one walk reads from the plugin wiring. */
167
+ export interface ChainWalkDeps {
168
+ readConfig(): FallbacksConfig;
169
+ getLlm(): LlmRuntime | undefined;
170
+ states: FallbackStateStore;
171
+ /**
172
+ * Per-(agent, chain id) last time-slot winner (P7 分时切换 marker): the
173
+ * caller owns the map (re-baselined on a settings change, cleared on
174
+ * dispose), the walk reads and writes it — the log only, never a failure
175
+ * decision. Absent ⇒ no log.
176
+ */
177
+ slotWinners?: Map<string, {
178
+ key: string;
179
+ label: string;
180
+ }>;
181
+ log: {
182
+ info(message: string, ...args: unknown[]): void;
183
+ warn(message: string, ...args: unknown[]): void;
184
+ /** Optional: the request-rewriting lines (parameter adaptation) are diagnostics only. */
185
+ debug?(message: string, ...args: unknown[]): void;
186
+ };
187
+ }
188
+ /**
189
+ * Walk one virtual call's chain (design §6.1): an async iterable of the
190
+ * attempts' chunks, ending in exactly one terminal `finish`.
191
+ *
192
+ * Throws only for the pre-attempt conditions (empty chain, undispatchable
193
+ * head, vanished runtime) — every per-attempt failure is either swallowed into
194
+ * another attempt or forwarded as that attempt's own terminal `finish`.
195
+ */
196
+ export declare function walkChain(deps: ChainWalkDeps, options: GenerateOptions): AsyncIterable<StreamChunk>;
197
+ export {};