dsh-llm-fallbacks 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +124 -0
- package/README.zh-CN.md +119 -0
- package/bundle/cordis.patch.yml +19 -0
- package/dist/chains.d.ts +167 -0
- package/dist/client/ConversationFallbackSwitch.d.ts +87 -0
- package/dist/client/FallbacksCard.d.ts +86 -0
- package/dist/client/GeneralFallbacksRow.d.ts +19 -0
- package/dist/client/fallbacks-store.d.ts +379 -0
- package/dist/client/index.d.ts +56 -0
- package/dist/client/index.js +3916 -0
- package/dist/client/locales.d.ts +281 -0
- package/dist/client/switch-guard.d.ts +17 -0
- package/dist/commands.d.ts +176 -0
- package/dist/config.d.ts +136 -0
- package/dist/cooldown.d.ts +48 -0
- package/dist/events.d.ts +36 -0
- package/dist/gateway.d.ts +176 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +1853 -0
- package/dist/roles.d.ts +70 -0
- package/dist/selectors.d.ts +35 -0
- package/dist/state.d.ts +112 -0
- package/package.json +91 -0
- package/scripts/build-client.ts +374 -0
- package/scripts/verify-dist.mjs +68 -0
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fallbacks/switch` session event vocabulary (spec §5 table; plan Task 3).
|
|
3
|
+
*
|
|
4
|
+
* The event is appended (never rewrites history — AC-7) at every switch,
|
|
5
|
+
* including the always-mode cap path: "无事件即无切换" (Global Constraint).
|
|
6
|
+
* The module is type-only — the augmentation is erased at runtime; the
|
|
7
|
+
* plugin's runtime behavior lives in `src/index.ts`.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-llm-fallbacks/events
|
|
10
|
+
*/
|
|
11
|
+
/** Why a switch was decided (spec §5.1 `PendingSwitch.reason`). */
|
|
12
|
+
export type FallbackSwitchReason = 'trigger-code' | 'always-cap';
|
|
13
|
+
/** Durable payload of one provider/model switch (spec §5 table). */
|
|
14
|
+
export interface FallbacksSwitchEventData {
|
|
15
|
+
turn: number;
|
|
16
|
+
step: number;
|
|
17
|
+
/** The model the request was using when the switch was decided. */
|
|
18
|
+
from: {
|
|
19
|
+
provider: string;
|
|
20
|
+
model: string;
|
|
21
|
+
};
|
|
22
|
+
/** The chain candidate the switch moves to. */
|
|
23
|
+
to: {
|
|
24
|
+
provider: string;
|
|
25
|
+
model: string;
|
|
26
|
+
};
|
|
27
|
+
/** The fallback-chain role the decision resolved for the agent (ADR-3). */
|
|
28
|
+
role: string;
|
|
29
|
+
reason: FallbackSwitchReason;
|
|
30
|
+
}
|
|
31
|
+
declare module '@deepseek-ai/dsh-session/types' {
|
|
32
|
+
interface SessionEventMap {
|
|
33
|
+
/** Durable, append-only record of one provider/model switch decided by dsh-llm-fallbacks. */
|
|
34
|
+
'fallbacks/switch': FallbacksSwitchEventData;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T1 (plan llm-fallbacks-settings-gateway) — host-side `fallbacks` config
|
|
3
|
+
* gateway: the `/api/fallbacks/get` + `/api/fallbacks/set` +
|
|
4
|
+
* `/api/fallbacks/reset` endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Transport: the typertGateway `/api` interceptor is the single host-wide RPC
|
|
7
|
+
* slot (a plugin must NOT `connection.rpc.intercept('/api')` again — it would
|
|
8
|
+
* throw). The service declares a typertGateway binding (via the
|
|
9
|
+
* `TypertRemoteService` base — kept ONLY for its `typertRemote` binding, which
|
|
10
|
+
* dispatch's `validateBinding` requires on the live service) and the
|
|
11
|
+
* endpoints are registered EXPLICITLY through
|
|
12
|
+
* `ctx.typert.register(fallbacksTypertContribution())` (see `apply` in
|
|
13
|
+
* `src/index.ts`) — NOT via `@Remote` SRC markers: SRC discovery reads
|
|
14
|
+
* `remoteMethods()`, a module-private WeakMap in `@deepseek-ai/dsh-typert-protocol`,
|
|
15
|
+
* so a locally-linked plugin whose peers resolve outside the host
|
|
16
|
+
* installation never shares that table with the host typertGateway (zero
|
|
17
|
+
* claimed endpoints, `/api/fallbacks/*` 404). The explicit
|
|
18
|
+
* `TypertRegistry.register` path writes the invocation descriptors into
|
|
19
|
+
* `ctx.typert.local`, which `claimsEndpoint` checks FIRST, so claim +
|
|
20
|
+
* dispatch work regardless of module identity. The payload contract is
|
|
21
|
+
* exactly one plain-object `args` field whose keys are the method parameter
|
|
22
|
+
* names (`get()` → `{ args: {} }`; `set(patch)` → `{ args: { patch } }`;
|
|
23
|
+
* `reset()` → `{ args: {} }`).
|
|
24
|
+
*
|
|
25
|
+
* Data: `get` reads the `FallbacksSettingsBridge` source — the same live
|
|
26
|
+
* composed config the runtime reads (schema defaults → plugin-row base →
|
|
27
|
+
* settings user layer). There is NO hard-gate resolver (unlike advisor's
|
|
28
|
+
* `resolveAdvisorConfig`): the fallbacks decision path runs at
|
|
29
|
+
* `agent/request` time in `src/index.ts`, so the gateway returns the raw
|
|
30
|
+
* composed config — `enabled` is a plain config field, not a gate output.
|
|
31
|
+
* `set` validates the patch against the `Config` schema first (unknown-key
|
|
32
|
+
* rejection unchanged — the settings service itself is non-strict and would
|
|
33
|
+
* merge the unknown key through), then writes the USER layer in-process via
|
|
34
|
+
* `ctx.settings.update` (no exposed-namespace gate on the in-process write —
|
|
35
|
+
* the wire-level `exposedNamespaces()` check only guards the apiproxy path),
|
|
36
|
+
* and returns the new composed value. `reset` (fallbacks-specific third
|
|
37
|
+
* method — advisor has only get/set) clears the user layer via
|
|
38
|
+
* `ctx.settings.replace(ns, {})`: `set` is merge-only and cannot express
|
|
39
|
+
* "reset to composition defaults" (sending default VALUES as a patch would
|
|
40
|
+
* pin stale defaults into the user layer).
|
|
41
|
+
*
|
|
42
|
+
* The settings service is OPTIONAL (no settings service → the bridge source
|
|
43
|
+
* stays the entry, `get` still works; `set`/`reset` fail with a clear
|
|
44
|
+
* error — KD-G5 fallback). The gateway captures the service through a
|
|
45
|
+
* conditional `ctx.inject(['settings'], ...)` child (the same activation
|
|
46
|
+
* pattern as `installSettingsSection`), because `ctx.settings` is only
|
|
47
|
+
* resolvable from a fiber that declares it.
|
|
48
|
+
*
|
|
49
|
+
* The returned config is normalized to the typertGateway JSON wire boundary:
|
|
50
|
+
* only schema-declared keys cross the wire, and absent values are OMITTED,
|
|
51
|
+
* never present-as-undefined (the gateway's result validation rejects
|
|
52
|
+
* undefined values).
|
|
53
|
+
*
|
|
54
|
+
* @module dsh-llm-fallbacks/gateway
|
|
55
|
+
*/
|
|
56
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
57
|
+
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
58
|
+
import type { TypertContribution } from '@deepseek-ai/dsh-typert-registry';
|
|
59
|
+
import type { FallbacksConfig } from './config';
|
|
60
|
+
/** The `fallbacks` settings namespace (registered when a settings service exists). */
|
|
61
|
+
export declare const FALLBACKS_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
|
|
62
|
+
/**
|
|
63
|
+
* The live configuration source for the gateway (guide §7 — the same bridge
|
|
64
|
+
* shape the runtime reads through). `source()` returns the live composed
|
|
65
|
+
* config (schema defaults → plugin-row base → settings user layer). The
|
|
66
|
+
* gateway reads it LIVE on every call, so no change notification is needed
|
|
67
|
+
* (the bridge stays minimal: source + the settings write channel; the dead
|
|
68
|
+
* `onChange` fan-out was removed in the QC fix wave — nothing subscribed).
|
|
69
|
+
*/
|
|
70
|
+
export interface FallbacksSettingsBridge {
|
|
71
|
+
source(): FallbacksConfig;
|
|
72
|
+
}
|
|
73
|
+
/** Patch shape accepted by `fallbacks.set` — any subset of the config keys. */
|
|
74
|
+
export type FallbacksConfigPatch = Partial<FallbacksConfig>;
|
|
75
|
+
/**
|
|
76
|
+
* The host-side `fallbacks` config gateway (`/api/fallbacks/get` +
|
|
77
|
+
* `/api/fallbacks/set` + `/api/fallbacks/reset`). Registered as the cordis
|
|
78
|
+
* service key `'fallbacks'` (namespace defaults to the service key). The
|
|
79
|
+
* `TypertRemoteService` base is kept ONLY for its `typertRemote` binding —
|
|
80
|
+
* the typertGateway's dispatch `validateBinding` requires the visible binding
|
|
81
|
+
* on the live service (a pure instance property, no module-private state).
|
|
82
|
+
* Endpoints are registered EXPLICITLY through
|
|
83
|
+
* `ctx.typert.register(fallbacksTypertContribution())` (see `apply` in
|
|
84
|
+
* `src/index.ts`) instead of `@Remote` SRC markers (see the module docblock
|
|
85
|
+
* for why).
|
|
86
|
+
*/
|
|
87
|
+
export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
88
|
+
private readonly bridge;
|
|
89
|
+
/** The live settings service once the optional inject child activates. */
|
|
90
|
+
private settings;
|
|
91
|
+
/**
|
|
92
|
+
* @param ctx - owning context (the plugin fiber's ctx inside `apply`).
|
|
93
|
+
* @param bridge - the same `FallbacksSettingsBridge` the runtime reads, so
|
|
94
|
+
* get/set/reset always operate on the live composed config.
|
|
95
|
+
*/
|
|
96
|
+
constructor(ctx: Context, bridge: FallbacksSettingsBridge);
|
|
97
|
+
/**
|
|
98
|
+
* Read the current composed config (schema defaults → entry base → settings
|
|
99
|
+
* user layer). No hard-gate resolver (ADR-2): the raw composed config is
|
|
100
|
+
* the wire value — `enabled` is a plain field, not a gate output.
|
|
101
|
+
* @returns the wire-normalized composed config plus `legacyKeys` — legacy
|
|
102
|
+
* two-block-era fields (`chains` / `roles.default` / undeclared rule
|
|
103
|
+
* role refs) detected on the composed source (schemastery retains them,
|
|
104
|
+
* plan Task 1 Step 1), so the client can show a migration banner (spec
|
|
105
|
+
* §9, incremental field — old clients ignore it).
|
|
106
|
+
*/
|
|
107
|
+
get(): {
|
|
108
|
+
config: FallbacksConfig;
|
|
109
|
+
legacyKeys: string[];
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Validate a config patch and write it to the settings USER layer (live —
|
|
113
|
+
* the runtime re-reads the same bridge source; no restart needed).
|
|
114
|
+
* @param patch - any subset of the config keys; unknown keys (top-level
|
|
115
|
+
* and nested under `roles`) are rejected before anything is written.
|
|
116
|
+
* @returns the NEW composed config plus `legacyKeys` detected on the
|
|
117
|
+
* POST-WRITE composed source (W-1/F-1): `set` is a settings MERGE, so a
|
|
118
|
+
* legacy user layer (`chains` / `roles.default`) survives a new-shape
|
|
119
|
+
* save — the response must keep reporting it, or the client banner
|
|
120
|
+
* would clear against server truth. Same shape as `get`.
|
|
121
|
+
* @throws when the patch fails `Config` validation, or when no settings
|
|
122
|
+
* service is composed (KD-G5: the write channel is unavailable).
|
|
123
|
+
*/
|
|
124
|
+
set(patch: FallbacksConfigPatch): Promise<{
|
|
125
|
+
config: FallbacksConfig;
|
|
126
|
+
legacyKeys: string[];
|
|
127
|
+
}>;
|
|
128
|
+
/**
|
|
129
|
+
* Clear the fallbacks settings USER layer so the composition defaults
|
|
130
|
+
* reapply (`settings.replace(ns, {})` — the in-process removal path a
|
|
131
|
+
* merge-only `set` cannot express).
|
|
132
|
+
* @returns the new composed config plus `legacyKeys` on the post-write
|
|
133
|
+
* source — `replace` drops the user layer, but legacy keys carried by
|
|
134
|
+
* the entry base survive and are correctly re-reported (W-1/F-1).
|
|
135
|
+
* @throws when no settings service is composed (KD-G5: the write channel
|
|
136
|
+
* is unavailable).
|
|
137
|
+
*/
|
|
138
|
+
reset(): Promise<{
|
|
139
|
+
config: FallbacksConfig;
|
|
140
|
+
legacyKeys: string[];
|
|
141
|
+
}>;
|
|
142
|
+
/**
|
|
143
|
+
* Read the live composed config and normalize it to the typertGateway JSON
|
|
144
|
+
* wire boundary. Containment (guide §10): a malformed stored user layer
|
|
145
|
+
* that the non-strict settings schema let through (e.g. an unknown key)
|
|
146
|
+
* must never fail the RPC — only schema-declared keys cross the wire, and
|
|
147
|
+
* absent values are omitted, never present-as-undefined (the result
|
|
148
|
+
* validator rejects undefined values). The `roles` object is additionally
|
|
149
|
+
* normalized to its declared `list`/`rules` fields: legacy nested keys
|
|
150
|
+
* (e.g. `roles.default`) that schemastery retains on the composed source
|
|
151
|
+
* never leak past the wire boundary (reviewer finding T1 Important #1 —
|
|
152
|
+
* a consumer like Task 2's parseFallbacksConfig would misread them),
|
|
153
|
+
* even though `legacyKeys` still reports them.
|
|
154
|
+
*/
|
|
155
|
+
private readConfig;
|
|
156
|
+
/**
|
|
157
|
+
* The wire response of every read (get/set/reset — W-1/F-1): the
|
|
158
|
+
* normalized config plus `legacyKeys` detected on the live composed
|
|
159
|
+
* source. set/reset must report the POST-WRITE source: the settings
|
|
160
|
+
* merge retains legacy user-layer keys, so a save cannot clear them —
|
|
161
|
+
* the honest response keeps the migration banner until a get agrees.
|
|
162
|
+
*/
|
|
163
|
+
private readResult;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The explicit typert contribution for the `fallbacks` gateway endpoints —
|
|
167
|
+
* registered via `ctx.typert.register(...)` (see `apply` in `src/index.ts`).
|
|
168
|
+
* The descriptors mirror exactly what the former SRC discovery derived from
|
|
169
|
+
* the `@Remote` markers (`src:<ns>#<endpoint>` identity shape, direct
|
|
170
|
+
* receiver, JSON wire params with `src-json` codec), so the host
|
|
171
|
+
* typertGateway claim + dispatch behavior is the same — the only difference
|
|
172
|
+
* is the registration does not depend on the module-private `remoteMethods`
|
|
173
|
+
* marker table, which a locally-linked plugin can never share with the host
|
|
174
|
+
* installation (see the module docblock).
|
|
175
|
+
*/
|
|
176
|
+
export declare function fallbacksTypertContribution(): TypertContribution;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-llm-fallbacks host half (plan Task 3: settings + waterfall + state
|
|
3
|
+
* machine + events).
|
|
4
|
+
*
|
|
5
|
+
* Cordis function plugin mounted by the profile bundle patch row
|
|
6
|
+
* `llm-fallbacks` (see `bundle/cordis.patch.yml`), composed AFTER llm-retry.
|
|
7
|
+
*
|
|
8
|
+
* Wiring:
|
|
9
|
+
* - `fallbacks` settings namespace via {@link installSettingsSection}
|
|
10
|
+
* (composition entry as base; `scope.watch` → `onChange` re-reads the
|
|
11
|
+
* runtime and re-validates selectors — spec §4).
|
|
12
|
+
* - `agent/request-error` waterfall: `!enabled` / code ∉ `triggerCodes`
|
|
13
|
+
* (**always mode included**) → `next()`; otherwise resolve role + chain,
|
|
14
|
+
* and when a candidate survives the filter (current / cooldown /
|
|
15
|
+
* step-failed / `provider/*`-missing-id) write the pending switch +
|
|
16
|
+
* cooldown + failure bookkeeping + append `fallbacks/switch`, then return
|
|
17
|
+
* `{ kind: 'retry' }` (own recovery, no `next()`).
|
|
18
|
+
* - `agent/request` waterfall: apply a pending switch after `await next()`
|
|
19
|
+
* (provider/model override, inherited `reasoningEffort` dropped — the
|
|
20
|
+
* `installModelSelection` `withoutInheritedEffort` pattern), then the
|
|
21
|
+
* always-mode cap check (count `llm/retry` events for the current
|
|
22
|
+
* turn/step/provider; ≥ `alwaysModeRetryCap` → same decision path, reason
|
|
23
|
+
* `always-cap` — ADR-2).
|
|
24
|
+
* - Per-agent state (`FallbackStateStore`): `agent/disposed` removes it,
|
|
25
|
+
* `agent/status` idle prunes per-step state defensively, plugin dispose
|
|
26
|
+
* clears everything (spec §6 — no residual state).
|
|
27
|
+
*
|
|
28
|
+
* @module dsh-llm-fallbacks
|
|
29
|
+
*/
|
|
30
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
31
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
32
|
+
import { Config, type FallbacksConfig } from './config.ts';
|
|
33
|
+
import { FallbackStateStore } from './state.ts';
|
|
34
|
+
/** The plugin row id mounted by the profile bundle patch. */
|
|
35
|
+
export declare const name = "llm-fallbacks";
|
|
36
|
+
export { Config };
|
|
37
|
+
/** The plugin's composition config — the `fallbacks` settings schema (spec §4). */
|
|
38
|
+
export type Config = FallbacksConfig;
|
|
39
|
+
export type { FallbackSwitchReason, FallbacksSwitchEventData } from './events.ts';
|
|
40
|
+
export type { AgentFallbackState, FallbackStateStore, PendingSwitch, StepFailures } from './state.ts';
|
|
41
|
+
/**
|
|
42
|
+
* @internal Test seam (T3 review Minor 3): the per-agent fallback state store
|
|
43
|
+
* of the plugin applied to `ctx` — last apply wins. Not part of the plugin's
|
|
44
|
+
* public surface; lets tests assert the no-op purity invariant (a plain
|
|
45
|
+
* request must not grow the store) without reaching into the closure.
|
|
46
|
+
*/
|
|
47
|
+
export declare function stateStore(ctx: Context): FallbackStateStore | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Count durable `llm/retry` events for the current (turn, step, provider) in
|
|
50
|
+
* **always mode** (ADR-2; spec §2 clause 5). Normal-mode retries belong to
|
|
51
|
+
* llm-retry's bounded budget and must not preempt the fallback, so only
|
|
52
|
+
* `mode: 'always'` events count toward `alwaysModeRetryCap` (T3 review
|
|
53
|
+
* Minor 2 — the real event carries the discriminator, llm-retry types.ts).
|
|
54
|
+
*
|
|
55
|
+
* Fast path (T3 review Minor 4): the session log is append-ordered, so the
|
|
56
|
+
* scan runs backwards and stops at the first event older than the target
|
|
57
|
+
* (turn, step) — a long session's earlier turns are never scanned.
|
|
58
|
+
*
|
|
59
|
+
* Exported for direct unit testing of the counting + fast path.
|
|
60
|
+
*/
|
|
61
|
+
export declare function countRetryEvents(session: Session, turn: number, step: number, provider: string): number;
|
|
62
|
+
export declare function apply(ctx: Context, config?: FallbacksConfig): void;
|