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.
@@ -1,67 +1,38 @@
1
1
  /**
2
- * Virtual `FallbacksChain/Auto` LLM adapter (plan fallbacks-virtual-chain
3
- * Task 1, technical pins P1–P3; PR #62 feedback round): a mount-only
4
- * catalog row that makes the configured effective chain selectable as the
5
- * session **primary** in the host model picker, without patching dsh or
6
- * dsh-TUI (web and TUI share the adapter catalog).
2
+ * Virtual `FallbacksChain` LLM adapter (named virtual chains, plan
3
+ * named-virtual-chains T2): one provider carrying N model rows — ONE per
4
+ * named chain, the row id BEING the chain id (picker selection is
5
+ * `FallbacksChain/<chain id>`), without patching dsh or dsh-TUI.
7
6
  *
8
- * Wiring (P2): `installFallbacksAdapter` installs ONE conditional
7
+ * Wiring: `installFallbacksAdapter` installs ONE conditional
9
8
  * `ctx.inject(['llm'])` child — absent `llm` service (test harness) is a
10
9
  * clean no-op, and fiber unload ⇒ the child's disposer unregisters the
11
10
  * route. Registration is an idempotent transition-reconcile on COMMITTED
12
- * config snapshots: register when nothing is configured → something is
13
- * (`isFallbackActive` — content presence, plan fallbacks-web-ux-alignment
14
- * T2), unregister on the reverse, driven by the settings `onChange` hook
15
- * (the returned reconcile thunk, wired by `apply()`) plus child activation.
16
- * The row is visible whenever something is configured — a non-conforming
17
- * all-day chain does NOT hide it (PR #62 feedback); conformance still gates
18
- * a successful delegate (`effectiveHeadOf` below refuses a non-conforming
19
- * all-day). The registration gate is `isFallbackActive` content presence
20
- * (`rootChain` / `timeSlots` / declared roles): a slot-row edit that flips
21
- * that gate (e.g. removing the only slot while `rootChain` and roles are
22
- * empty) intentionally churns registration — the content-presence contract
23
- * (plan fallbacks-web-ux-alignment T2), not churn to design away; do NOT
24
- * re-narrow the condition to ignore `timeSlots`. Conformance-only chain
25
- * edits still never churn (conformance gates the delegate, not the row).
11
+ * config snapshots: register when `chains` goes empty → non-empty
12
+ * (`isFallbackActive` = `chains.length > 0`), unregister on the reverse,
13
+ * driven by the settings `onChange` hook (the returned reconcile thunk,
14
+ * wired by `apply()`) plus child activation. A chain with empty or
15
+ * non-conforming `entries` still shows its row (conformance gates the
16
+ * delegate via {@link effectiveHeadOf}, not the row). One
17
+ * `llm.registerAdapter([FALLBACKS_PROVIDER], adapter)` call serves all rows.
26
18
  *
27
- * Adapter behavior (P1/P3): `listModels` advertises exactly the one virtual
28
- * row; `stream()` is a THIN single-hop delegate to the effective chain head
29
- * through the host LLM runtime — no chain walk, no cooldown/caps/revert
30
- * bookkeeping, no state writes (a failure inside the delegate surfaces at
31
- * `agent/request-error`, where the existing engine walks from there).
32
- * `resolveModel` proxies the current effective head's metadata when
33
- * resolvable (modalities/context-window/reasoning follow the head) with a
34
- * permissive default otherwise — never throws. `providerRetryPolicy` and
35
- * `imageRequestPricing` mirror the same head for the host-captured retry
36
- * policy and the token meter — also never throwing.
19
+ * Adapter behavior: `listModels` advertises one row per chain;
20
+ * `stream()` WALKS the selected chain (cooldown / failed-set / switch cap /
21
+ * recovery bookkeeping) in `src/chain-walk.ts` — the walk is injected at
22
+ * install and owns the pre-attempt guards. `resolveModel` /
23
+ * `imageRequestPricing` resolve the chain FROM the `model` argument and
24
+ * follow its effective head (permissive default otherwise — never throw).
25
+ * `providerRetryPolicy` returns `undefined` (documented degradation): the
26
+ * host captures the policy ONCE per provider at registration, so the virtual
27
+ * route cannot mirror a per-chain head's policy — the host default applies.
37
28
  *
38
- * @module dsh-llm-fallbacks/virtual-adapter
29
+ @module dsh-llm-fallbacks/virtual-adapter
39
30
  */
40
31
  import type { Context } from '@deepseek-ai/cordis';
41
32
  import { LlmAdapter, type GenerateOptions, type LlmImageRequestPricing, type LlmModelInfo, type LlmResolvedModelInfo, type LlmRuntime, type ResolvedRetryPolicy, type StreamChunk } from '@deepseek-ai/dsh-llm';
42
- import { type FallbacksConfig } from './config.ts';
33
+ import { type FallbacksConfig, type NamedChain } from './config.ts';
43
34
  /** Provider route of the virtual adapter (exact string, spec lock). */
44
35
  export declare const FALLBACKS_PROVIDER = "FallbacksChain";
45
- /**
46
- * Model id of the virtual catalog row (exact string, spec lock). "Auto" is
47
- * the hardcoded picker id (not i18n — user decision 2026-08-18). The
48
- * catalog `name` the host picker renders is dynamic — see
49
- * {@link pickerDisplayName} (`Auto: <model>[<slot>]`).
50
- */
51
- export declare const FALLBACKS_CHAIN_MODEL = "Auto";
52
- /**
53
- * `LlmError` code: the effective chain is empty — the virtual route has no
54
- * head to delegate to (P1 guard).
55
- */
56
- export declare const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
57
- /**
58
- * `LlmError` code: the effective chain head is not a dispatchable real pair
59
- * (non-conforming all-day, wildcard selector, malformed selector, or a
60
- * self-route back to `FallbacksChain/*` — the P1 recursion guard).
61
- */
62
- export declare const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
63
- /** `LlmError` code (defensive): the `llm` runtime disappeared mid-flight. */
64
- export declare const LLM_UNAVAILABLE_CODE = "LLM_UNAVAILABLE";
65
36
  /** One dispatchable exact head: `provider/model`. Wildcards are never seeds (P3). */
66
37
  export interface EffectiveHead {
67
38
  provider: string;
@@ -69,10 +40,10 @@ export interface EffectiveHead {
69
40
  }
70
41
  /**
71
42
  * The FIRST DISPATCHABLE exact head of a chain — the single definition of
72
- * "effective head" (F-001) shared by the virtual adapter's delegate paths and
73
- * the routing engine's served-route anchor (`anchorServedRoute` in
74
- * `src/index.ts`). Walks the SAME chain `resolveEffectiveChain` produces,
75
- * skipping entries that can never
43
+ * "effective head" (F-001) shared by this module's metadata paths
44
+ * (`pickerDisplayName` / `resolveModel` / `imageRequestPricing`) and the
45
+ * walk's head resolution (`resolveWalk` in `src/chain-walk.ts`). Walks the
46
+ * SAME chain `resolveEffectiveChain` produces, skipping entries that can never
76
47
  * be dispatched: malformed selectors (config-warning path), `provider/*`
77
48
  * `provider/*` wildcards (no real pair), and self-routes back to
78
49
  * `FallbacksChain/*` (the P1 recursion guard). `undefined` when the chain
@@ -80,91 +51,69 @@ export interface EffectiveHead {
80
51
  */
81
52
  export declare function firstDispatchableExactHead(chain: readonly string[]): EffectiveHead | undefined;
82
53
  /**
83
- * The effective chain head at `now` (P1): the first DISPATCHABLE exact head
84
- * of the SAME `resolveEffectiveChain` the routing engine uses — thin
85
- * config+clock entry into the shared {@link firstDispatchableExactHead} (no
86
- * divergent skip/walk rules). `undefined` when the chain is empty or no
87
- * entry is dispatchable (wildcard, self-route to `FallbacksChain/*`,
88
- * malformed selector).
89
- *
90
- * Conformance gate (PR #62 feedback): the all-day `rootChain` must be
91
- * conforming (exactly one official model — Flash or Pro)
92
- * for a successful delegate —
93
- * a legacy multi-model (or empty) all-day chain earns no primary
94
- * semantics even though the picker row is visible. `resolveModel` then
95
- * falls back to the permissive default and `stream()` throws
96
- * {@link UNDISPATCHABLE_HEAD_CODE}.
54
+ * The effective chain head of ONE named chain at `now`: the first
55
+ * DISPATCHABLE exact head of the SAME `resolveEffectiveChain` the walk uses —
56
+ * thin chain+clock entry into the shared {@link firstDispatchableExactHead}.
57
+ * `undefined` when the entries are empty or non-conforming (the tail must be
58
+ * exactly one official model), or no entry is dispatchable (wildcard,
59
+ * self-route to `FallbacksChain/*`, malformed selector). `resolveModel` then
60
+ * falls back to the permissive default; the delegate side of the conformance
61
+ * gate is the chain walk's `UNDISPATCHABLE_EFFECTIVE_HEAD`.
97
62
  */
98
- export declare function effectiveHeadOf(config: FallbacksConfig, now: Date): EffectiveHead | undefined;
63
+ export declare function effectiveHeadOf(chain: NamedChain, now: Date, tz?: string): EffectiveHead | undefined;
99
64
  /**
100
- * Host picker label: `Auto: <displayName>[<slot>]`.
101
- * Display name first so the trigger stays readable and platforms stay
102
- * distinguishable (catalog `name`, not the model id). Bare `Auto` when
103
- * there is no dispatchable head. Slot label from {@link resolveSlotState}.
104
- * @param modelDisplayName - catalog/resolved name; falls back to the id.
65
+ * Host picker label for one chain: `` `${chain.label}: <head>[<slot>]` ``.
66
+ * Bare `chain.label` when there is no dispatchable head. The label is
67
+ * user-editable and display-only; the model id is the chain id.
105
68
  */
106
- export declare function pickerDisplayName(config: FallbacksConfig, now?: Date, modelDisplayName?: string): string;
69
+ export declare function pickerDisplayName(chain: NamedChain, now?: Date, modelDisplayName?: string, tz?: string): string;
107
70
  /**
108
- * The virtual adapter (P1/P3). `stream()` is a thin head-delegate, never a
109
- * second routing engine: no chain walk, cooldown, caps, revert bookkeeping,
110
- * or state writes live here — those stay in the `agent/request` /
111
- * `agent/request-error` listeners. Exported so the defensive guard paths
112
- * the registration lifecycle cannot reach (a registered route whose `llm`
113
- * vanished mid-teardown) stay unit-testable by direct construction.
71
+ * The virtual adapter (P1/P3). `stream()` hands the call to the injected walk
72
+ * (see `src/chain-walk.ts`) — registration metadata stays here.
114
73
  */
115
74
  export declare class FallbacksChainAdapter extends LlmAdapter {
116
75
  private readonly readConfig;
117
76
  private readonly getLlm;
118
- constructor(readConfig: () => FallbacksConfig, getLlm: () => LlmRuntime | undefined);
77
+ private readonly walk;
78
+ constructor(readConfig: () => FallbacksConfig, getLlm: () => LlmRuntime | undefined, walk: (options: GenerateOptions) => AsyncIterable<StreamChunk>);
119
79
  /** Minimal honest descriptor (P3) — the provider route IS the display name. */
120
80
  providerInfo(provider: string): {
121
81
  id: string;
122
82
  name: string;
123
83
  };
124
- /** Advisory catalog: one virtual row; `name` is the live picker label. */
84
+ /** Advisory catalog: ONE row per named chain; `name` is the live picker label. */
125
85
  listModels(provider: string): Promise<readonly LlmModelInfo[]>;
126
86
  /**
127
- * Proxy the current effective head's model metadata when resolvable (P3):
128
- * modalities/context-window/reasoning follow the head — the truthful
129
- * answer for the CURRENT head. Permissive default otherwise; never throws.
87
+ * Model-scoped metadata: resolve the chain FROM the `model` argument (the
88
+ * chain id) and follow its effective head — modalities/context-window/
89
+ * reasoning follow the head, the truthful answer for the CURRENT head.
90
+ * Permissive default for an unknown chain id or an unresolvable head;
91
+ * never throws.
130
92
  */
131
93
  resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
132
94
  /**
133
- * Route-accurate retry attribution: the virtual row declares no policy of
134
- * its own — the virtual `provider` argument is intentionally ignored. The
135
- * host captures `adapter.providerRetryPolicy(provider) ??
136
- * resolveRetryPolicy(void 0, …)` ONCE, when a route is registered, so
137
- * answering here with the SAME policy the concrete head route declared
138
- * (e.g. a user's `llm-deepseek.retryPolicy`, including `mode: 'always'`)
139
- * keeps retries behaving exactly as they do on the head `stream()`
140
- * dispatches. Never throws: an unresolvable head, a vanished `llm`, or an
141
- * unregistered head provider degrades to `undefined` — the host's own
142
- * default. That last arm matters: the runtime's lookup THROWS `NO_ADAPTER`
143
- * for an unknown provider, and this call happens inside the plugin's
144
- * `registerAdapter`, so an escaping throw would take the whole virtual
145
- * route down instead of just degrading its policy.
95
+ * DOCUMENTED DEGRADATION (named virtual chains): the adapter stays ONE
96
+ * `FallbacksChain` provider carrying N chain rows, but the host captures
97
+ * `adapter.providerRetryPolicy(provider)` ONCE per provider at
98
+ * registration, so the virtual route can no longer mirror its head's
99
+ * retry policy. It returns `undefined` — the host's own default applies.
146
100
  */
147
101
  providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined;
148
102
  /**
149
- * Route-accurate image pricing: the virtual row has no
150
- * pricing of its own — the virtual `provider`/`model` arguments are
151
- * intentionally ignored. Resolves the SAME effective head `stream()`
152
- * dispatches and delegates through the runtime lookup, so the token
153
- * meter gets exactly the answer the concrete route's adapter would give
154
- * (including `undefined`). Never throws: an unresolvable head, a
155
- * vanished `llm`, or a throwing delegate degrades to `undefined` — the
156
- * token meter's neutral estimate.
103
+ * Image pricing, model-scoped: resolve the chain FROM the `model`
104
+ * argument and delegate through the runtime lookup for its effective head
105
+ * (including `undefined`). Never throws: an unknown chain id, an
106
+ * unresolvable head, a vanished `llm`, or a throwing delegate degrades to
107
+ * `undefined` — the token meter's neutral estimate.
157
108
  */
158
- imageRequestPricing(_provider: string, _model: string): LlmImageRequestPricing | undefined;
109
+ imageRequestPricing(_provider: string, model: string): LlmImageRequestPricing | undefined;
159
110
  /**
160
- * Thin head-delegate (P1): resolve the effective chain head and dispatch
161
- * that REAL pair through the host LLM runtime. Throws an explicit
162
- * `LlmError` only when there is no real pair to delegate to: an empty
163
- * effective chain, an undispatchable head (wildcard / malformed /
164
- * self-route — the recursion guard), or a vanished `llm` runtime. The
165
- * runtime normalizes the throw into a terminal error finish chunk, so the
166
- * documented listener-order degradation stays graceful instead of a hard
167
- * outage.
111
+ * The virtual route's entry point: `options.model` IS the chain id and the
112
+ * selected chain is WALKED (`src/chain-walk.ts`) — the walk owns the
113
+ * pre-attempt guards (`UNKNOWN_CHAIN` / `EMPTY_EFFECTIVE_CHAIN` /
114
+ * `UNDISPATCHABLE_EFFECTIVE_HEAD` / `LLM_UNAVAILABLE`, thrown synchronously
115
+ * before any chunk, so the runtime normalizes them exactly like an adapter
116
+ * throw).
168
117
  */
169
118
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
170
119
  }
@@ -174,7 +123,9 @@ export declare class FallbacksChainAdapter extends LlmAdapter {
174
123
  * @param ctx - the plugin context.
175
124
  * @param readConfig - live config reader (the same `source()` the runtime
176
125
  * reads, so reconcile always sees COMMITTED composed snapshots).
126
+ * @param walk - the chain walk the adapter's `stream()` runs for one virtual
127
+ * call (built by `apply()` over its live state — see `src/chain-walk.ts`).
177
128
  * @returns the reconcile thunk — call it from the settings `onChange` hook
178
129
  * (child activation runs one reconcile on its own).
179
130
  */
180
- export declare function installFallbacksAdapter(ctx: Context, readConfig: () => FallbacksConfig): () => void;
131
+ export declare function installFallbacksAdapter(ctx: Context, readConfig: () => FallbacksConfig, walk: (options: GenerateOptions) => AsyncIterable<StreamChunk>): () => void;
package/docs/DETAIL.md ADDED
@@ -0,0 +1 @@
1
+ ![A request failing on an auth code in the FallbacksChain walk, the next provider/model serving it, the switch log line, and the FallbacksChain / Auto row in the model picker](preview.svg)
@@ -0,0 +1 @@
1
+ ![请求在降级链上命中认证失败码后由下一个 provider/model 接手、切换日志,以及模型选择器中的 FallbacksChain / Auto 行](preview.zh.svg)