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 +34 -30
- package/README.zh-CN.md +32 -28
- package/dist/chain-walk.d.ts +197 -0
- package/dist/chains.d.ts +35 -82
- package/dist/client/FallbacksCard.d.ts +27 -80
- package/dist/client/fallbacks-store.d.ts +71 -225
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.js +1332 -2110
- package/dist/client/locales.d.ts +61 -117
- package/dist/commands.d.ts +70 -112
- package/dist/config.d.ts +152 -117
- package/dist/cooldown.d.ts +1 -15
- package/dist/events.d.ts +8 -8
- package/dist/gateway.d.ts +66 -110
- package/dist/index.d.ts +21 -64
- package/dist/index.js +900 -1313
- package/dist/role-resolution.d.ts +15 -83
- package/dist/schema.d.ts +6 -5
- package/dist/seeds.d.ts +5 -7
- package/dist/state.d.ts +15 -141
- package/dist/subagents-seam.d.ts +51 -14
- package/dist/time-slots.d.ts +21 -24
- package/dist/tui-settings.d.ts +5 -6
- package/dist/virtual-adapter.d.ts +70 -119
- package/docs/DETAIL.md +1 -0
- package/docs/DETAIL.zh.md +1 -0
- package/docs/preview.svg +47 -0
- package/docs/preview.zh.svg +47 -0
- package/locale/en.json +2 -1
- package/locale/zh.json +2 -1
- package/package.json +29 -25
- package/dist/authorized-route.d.ts +0 -68
- package/dist/automatch.d.ts +0 -98
- package/dist/client/GeneralFallbacksRow.d.ts +0 -19
- package/dist/override.d.ts +0 -39
- package/dist/roles.d.ts +0 -90
- package/dist/route-allowlist.d.ts +0 -43
- package/dist/subagent-policy.d.ts +0 -123
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# dsh-llm-fallbacks
|
|
2
4
|
|
|
3
5
|
[English](README.md) | [Chinese](README.zh-CN.md)
|
|
@@ -7,10 +9,10 @@
|
|
|
7
9
|

|
|
8
10
|

|
|
9
11
|

|
|
10
|
-

|
|
11
13
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
12
14
|
|
|
13
|
-
Automatic provider/model fallback chains for dsh (DeepSeek Harness):
|
|
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
|
-
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|

|
|
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 **
|
|
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
|
-
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
110
|
-
- **
|
|
111
|
-
- **Slots and roles are optional**:
|
|
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**
|
|
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
|
|
126
|
-
- **Role-based subagents** — a dispatch-declared role brings persona, notice row and badge to the child
|
|
127
|
-
- **FallbacksChain in the picker** — select `FallbacksChain /
|
|
128
|
-
- **Host subagent model policy** —
|
|
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,
|
|
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
|
+

|
|
2
|
+
|
|
1
3
|
# dsh-llm-fallbacks
|
|
2
4
|
|
|
3
5
|
[English](README.md) | [中文](README.zh-CN.md)
|
|
@@ -7,10 +9,10 @@
|
|
|
7
9
|

|
|
8
10
|

|
|
9
11
|

|
|
10
|
-

|
|
11
13
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
12
14
|
|
|
13
|
-
dsh(DeepSeek Harness
|
|
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
|
|
23
|
+
会话模型选择走 `FallbacksChain / <链 id>` 时——root agent 选中该行,或子代理的角色以「指定降级链」指向该链——沿该链行走:请求在**输出任何内容之前**命中触发码,就在同一个请求内改到下一个可用 provider/model,step 继续运行。选择真实 provider/model 的会话不获得任何插件降级。无需手动切换模型;在配置链之前(`chains` 为空是唯一初始状态)插件不做任何事。
|
|
22
24
|
|
|
23
|
-
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
110
|
-
-
|
|
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
|
-
-
|
|
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
|
|
126
|
-
- **角色化 subagent**——派发声明的角色为子代理带来 persona
|
|
127
|
-
- **模型选择器中的 FallbacksChain**——选择 `FallbacksChain /
|
|
128
|
-
- **宿主子代理模型策略**——dsh 0.1.2
|
|
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
|
|
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 {};
|