dsh-llm-fallbacks 0.2.2 → 0.3.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  import z from "@deepseek-ai/schemastery";
4
- import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
+ import { LlmAdapter, LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
5
5
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
6
6
  //#region .build/host/selectors.js
7
7
  var SelectorError = class extends Error {
@@ -35,6 +35,202 @@ function resolveWildcardEntry(failingModel, provider) {
35
35
  raw: `${provider}/${failingModel}`
36
36
  };
37
37
  }
38
+ const PRESETS = {
39
+ "liang-peak": {
40
+ windows: [{
41
+ start: "09:00",
42
+ end: "12:00"
43
+ }, {
44
+ start: "14:00",
45
+ end: "18:00"
46
+ }],
47
+ complement: false,
48
+ label: "Liang Peak"
49
+ },
50
+ "liang-valley": {
51
+ windows: [{
52
+ start: "09:00",
53
+ end: "12:00"
54
+ }, {
55
+ start: "14:00",
56
+ end: "18:00"
57
+ }],
58
+ complement: true,
59
+ label: "Liang Valley"
60
+ },
61
+ "glm-peak": {
62
+ windows: [{
63
+ start: "14:00",
64
+ end: "18:00",
65
+ days: [
66
+ 1,
67
+ 2,
68
+ 3,
69
+ 4,
70
+ 5
71
+ ]
72
+ }],
73
+ complement: false,
74
+ label: "GLM Peak"
75
+ },
76
+ "glm-valley": {
77
+ windows: [{
78
+ start: "14:00",
79
+ end: "18:00",
80
+ days: [
81
+ 1,
82
+ 2,
83
+ 3,
84
+ 4,
85
+ 5
86
+ ]
87
+ }],
88
+ complement: true,
89
+ label: "GLM Valley"
90
+ }
91
+ };
92
+ const HHMM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
93
+ const WEEKDAY_INDEX = {
94
+ Sun: 0,
95
+ Mon: 1,
96
+ Tue: 2,
97
+ Wed: 3,
98
+ Thu: 4,
99
+ Fri: 5,
100
+ Sat: 6
101
+ };
102
+ const warnedMalformedRows = new WeakSet();
103
+ const warnedTimeZones = new Set();
104
+ function warnMalformed(row, reason) {
105
+ if (typeof row !== "object" || row === null) {
106
+ console.warn(`llm-fallbacks: skipping malformed time-slot row (${reason})`);
107
+ return;
108
+ }
109
+ if (warnedMalformedRows.has(row)) return;
110
+ warnedMalformedRows.add(row);
111
+ console.warn(`llm-fallbacks: skipping malformed time-slot row (${reason}): ${JSON.stringify(row)}`);
112
+ }
113
+ function wallClock(now, tz) {
114
+ try {
115
+ const parts = new Intl.DateTimeFormat("en-US", {
116
+ timeZone: tz,
117
+ weekday: "short",
118
+ hour: "2-digit",
119
+ minute: "2-digit",
120
+ hourCycle: "h23"
121
+ }).formatToParts(now);
122
+ let weekday = 0;
123
+ let minutes = 0;
124
+ for (const part of parts) if (part.type === "weekday") weekday = WEEKDAY_INDEX[part.value] ?? 0;
125
+ else if (part.type === "hour") minutes += Number(part.value) * 60;
126
+ else if (part.type === "minute") minutes += Number(part.value);
127
+ return {
128
+ weekday,
129
+ minutes
130
+ };
131
+ } catch (error) {
132
+ if (!warnedTimeZones.has(tz)) {
133
+ warnedTimeZones.add(tz);
134
+ console.warn(`llm-fallbacks: invalid timezone "${tz}" (${error.message}) — slot matching falls back to UTC`);
135
+ }
136
+ return {
137
+ weekday: now.getUTCDay(),
138
+ minutes: now.getUTCHours() * 60 + now.getUTCMinutes()
139
+ };
140
+ }
141
+ }
142
+ function minutesOf(hhmm) {
143
+ const [hour, minute] = hhmm.split(":").map(Number);
144
+ return hour * 60 + minute;
145
+ }
146
+ function containsWindow(window, clock) {
147
+ if (!(window.days === void 0 || window.days.length === 0 || window.days.includes(clock.weekday))) return false;
148
+ const start = minutesOf(window.start);
149
+ const end = minutesOf(window.end);
150
+ return start <= end ? start <= clock.minutes && clock.minutes < end : start <= clock.minutes || clock.minutes < end;
151
+ }
152
+ function matchesAnyWindow(windows, clock) {
153
+ return windows.some((window) => containsWindow(window, clock));
154
+ }
155
+ function describeRow(row) {
156
+ if (!Array.isArray(row.chain) || row.chain.length === 0) {
157
+ warnMalformed(row, "empty chain");
158
+ return;
159
+ }
160
+ if (row.kind === "preset") {
161
+ const preset = row.preset;
162
+ if (typeof preset !== "string" || !Object.hasOwn(PRESETS, preset)) {
163
+ warnMalformed(row, `unknown preset ${JSON.stringify(preset)}`);
164
+ return;
165
+ }
166
+ if (row.start !== void 0 || row.end !== void 0 || row.days !== void 0 && row.days.length > 0) {
167
+ warnMalformed(row, `preset windows are fixed — row "${preset}" cannot carry start/end/days`);
168
+ return;
169
+ }
170
+ return PRESETS[preset];
171
+ }
172
+ if (row.kind === "custom") {
173
+ const { start, end } = row;
174
+ if (typeof start !== "string" || typeof end !== "string" || !HHMM_RE.test(start) || !HHMM_RE.test(end)) {
175
+ warnMalformed(row, `invalid custom window ${JSON.stringify(start)}-${JSON.stringify(end)} (expected HH:mm)`);
176
+ return;
177
+ }
178
+ return {
179
+ windows: [{
180
+ start,
181
+ end,
182
+ days: row.days
183
+ }],
184
+ complement: false
185
+ };
186
+ }
187
+ warnMalformed(row, `unknown kind ${JSON.stringify(row.kind)}`);
188
+ }
189
+ function labelOf(row) {
190
+ if (row.kind === "preset" && typeof row.preset === "string" && Object.hasOwn(PRESETS, row.preset)) return PRESETS[row.preset].label;
191
+ return row.name !== void 0 && row.name.trim() !== "" ? row.name : `custom ${row.start}-${row.end}`;
192
+ }
193
+ function isAllDayConforming(chain) {
194
+ if (chain.length < 1) return false;
195
+ const tail = chain[chain.length - 1];
196
+ return tail === "deepseek-official/deepseek-v4-flash" || tail === "deepseek-official/deepseek-v4-pro";
197
+ }
198
+ function resolveEffectiveChain(config, now, tz) {
199
+ const state = resolveSlotState(config, now, tz);
200
+ return state.winner === "all-day" ? config.rootChain : state.winner.chain;
201
+ }
202
+ function resolveSlotState(config, now, tz) {
203
+ if (!isAllDayConforming(config.rootChain)) return {
204
+ winner: "all-day",
205
+ label: "all-day"
206
+ };
207
+ const clock = wallClock(now, tz);
208
+ const seenPresets = new Set();
209
+ const rows = Array.isArray(config.timeSlots) ? config.timeSlots : [];
210
+ for (const row of rows) {
211
+ if (typeof row !== "object" || row === null) {
212
+ warnMalformed(row, "row is not an object");
213
+ continue;
214
+ }
215
+ if (row.kind === "preset" && typeof row.preset === "string") {
216
+ if (seenPresets.has(row.preset)) {
217
+ warnMalformed(row, `duplicate preset "${row.preset}" — only the first row takes effect`);
218
+ continue;
219
+ }
220
+ seenPresets.add(row.preset);
221
+ }
222
+ const descriptor = describeRow(row);
223
+ if (descriptor === void 0) continue;
224
+ if (descriptor.complement ? !matchesAnyWindow(descriptor.windows, clock) : matchesAnyWindow(descriptor.windows, clock)) return {
225
+ winner: row,
226
+ label: labelOf(row)
227
+ };
228
+ }
229
+ return {
230
+ winner: "all-day",
231
+ label: "all-day"
232
+ };
233
+ }
38
234
  //#endregion
39
235
  //#region .build/host/config.js
40
236
  const defaultFallbacksConfig = {
@@ -54,7 +250,9 @@ const defaultFallbacksConfig = {
54
250
  maxSwitchesPerStep: 8,
55
251
  alwaysModeRetryCap: 5,
56
252
  presets: "bundled",
57
- roleAutoMatch: true
253
+ roleAutoMatch: true,
254
+ timeSlots: [],
255
+ tz: "Asia/Shanghai"
58
256
  };
59
257
  const INHERIT_ROLE_ID = "inherit";
60
258
  const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
@@ -74,6 +272,7 @@ function validateFallbacksConfig(config, logger) {
74
272
  if ((role.chain ?? []).length === 0) logger.warn(`llm-fallbacks: role "${role.id}" has no model config — declare at least one chain entry, or use the built-in "inherit" rule target instead`);
75
273
  if (role.fallback !== void 0 && role.fallback !== "inherit-root" && role.fallback !== "none") logger.warn(`llm-fallbacks: role "${role.id}" has invalid fallback "${String(role.fallback)}" — expected "inherit-root" or "none"`);
76
274
  }
275
+ if (config.rootChain.length > 0 && !isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: rootChain must end with exactly one official V4 model (deepseek-official/deepseek-v4-flash or deepseek-official/deepseek-v4-pro) — time-slot rows and the virtual picker row stay inert until the all-day chain tail conforms");
77
276
  for (const entry of config.rootChain) try {
78
277
  parseSelector(entry);
79
278
  } catch (error) {
@@ -81,6 +280,23 @@ function validateFallbacksConfig(config, logger) {
81
280
  }
82
281
  const validTargets = new Set([...declaredIds, INHERIT_ROLE_ID]);
83
282
  for (const rule of config.roles.rules) if (!validTargets.has(rule.role.trim())) logger.warn(`llm-fallbacks: rule references undeclared role "${rule.role}" — expected one of roles.list ids or "inherit"`);
283
+ if (Array.isArray(config.timeSlots)) {
284
+ const seenSlotPresets = new Set();
285
+ for (const row of config.timeSlots) {
286
+ if (row.kind !== "preset") continue;
287
+ const preset = row.preset;
288
+ if (typeof preset !== "string" || !Object.hasOwn(PRESETS, preset)) {
289
+ logger.warn(`llm-fallbacks: unknown time-slot preset ${JSON.stringify(preset)} — row ignored`);
290
+ continue;
291
+ }
292
+ if (seenSlotPresets.has(preset)) {
293
+ logger.warn(`llm-fallbacks: duplicate time-slot preset "${preset}" — only the first row takes effect`);
294
+ continue;
295
+ }
296
+ seenSlotPresets.add(preset);
297
+ if (row.start !== void 0 || row.end !== void 0 || row.days !== void 0 && row.days.length > 0) logger.warn(`llm-fallbacks: preset "${preset}" carries its own window/day fields — preset windows are fixed code constants; the row is ignored`);
298
+ }
299
+ }
84
300
  }
85
301
  function detectLegacyKeys(source) {
86
302
  const keys = [];
@@ -141,7 +357,16 @@ const Config = z.object({
141
357
  maxSwitchesPerStep: z.number().default(8),
142
358
  alwaysModeRetryCap: z.number().default(5),
143
359
  presets: z.union([z.const("bundled"), z.const("none")]).default("bundled"),
144
- roleAutoMatch: z.boolean().default(true)
360
+ roleAutoMatch: z.boolean().default(true),
361
+ timeSlots: z.array(z.object({
362
+ kind: z.string(),
363
+ preset: z.string(),
364
+ start: z.string(),
365
+ end: z.string(),
366
+ days: z.array(z.number()),
367
+ chain: z.array(z.string())
368
+ })).default([]),
369
+ tz: z.string().default("Asia/Shanghai")
145
370
  });
146
371
  function buildAutomatchPrompt(roles, agent) {
147
372
  const lines = [
@@ -341,9 +566,8 @@ function annotateCandidates(candidates, surviving, options) {
341
566
  //#endregion
342
567
  //#region .build/host/roles.js
343
568
  function resolveRole(agent, rules, roleIds, warn = console.warn) {
344
- const origin = agent.session?.header?.origin ?? "root";
569
+ if ((agent.session?.header?.origin ?? "root") !== "subagent") return INHERIT_ROLE_ID;
345
570
  for (const rule of rules) {
346
- if (rule.origin && rule.origin !== origin) continue;
347
571
  if (rule.provider && rule.provider !== agent.options?.provider) continue;
348
572
  if (rule.model && rule.model !== agent.options?.model) continue;
349
573
  const target = rule.role.trim();
@@ -517,12 +741,24 @@ const CONFIG_KEYS = {
517
741
  maxSwitchesPerStep: true,
518
742
  alwaysModeRetryCap: true,
519
743
  presets: true,
520
- roleAutoMatch: true
744
+ roleAutoMatch: true,
745
+ timeSlots: true,
746
+ tz: true
521
747
  };
522
748
  const ROLES_KEYS = {
523
749
  list: true,
524
750
  rules: true
525
751
  };
752
+ const SLOT_ROW_KEYS = {
753
+ kind: true,
754
+ preset: true,
755
+ start: true,
756
+ end: true,
757
+ days: true,
758
+ name: true,
759
+ chain: true
760
+ };
761
+ const SLOT_HHMM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
526
762
  var FallbacksConfigGateway = class extends TypertRemoteService {
527
763
  bridge;
528
764
  seeds;
@@ -612,9 +848,44 @@ function validateConfigPatch(patch) {
612
848
  for (const nestedKey of Object.keys(roles)) if (!Object.hasOwn(ROLES_KEYS, nestedKey)) throw new Error(`dsh-llm-fallbacks: unknown config key "roles.${nestedKey}"`);
613
849
  }
614
850
  }
851
+ if (key === "timeSlots") {
852
+ const rows = patch[key];
853
+ if (rows !== null && rows !== void 0) validateTimeSlotsPatch(rows);
854
+ }
855
+ if (key === "rootChain") {
856
+ const chain = patch[key];
857
+ if (chain !== null && chain !== void 0 && (!Array.isArray(chain) || !isAllDayConforming(chain))) throw new Error("dsh-llm-fallbacks: rootChain must end with exactly one official V4 model (deepseek-official/deepseek-v4-flash or deepseek-official/deepseek-v4-pro)");
858
+ }
615
859
  }
616
860
  Config(patch);
617
861
  }
862
+ function validateTimeSlotsPatch(rows) {
863
+ if (!Array.isArray(rows)) throw new Error("dsh-llm-fallbacks: timeSlots must be an array of slot rows");
864
+ const seenPresets = new Set();
865
+ rows.forEach((row, index) => {
866
+ const at = `timeSlots[${index}]`;
867
+ if (row === null || typeof row !== "object" || Array.isArray(row)) throw new Error(`dsh-llm-fallbacks: ${at} must be a plain object`);
868
+ const record = row;
869
+ for (const nestedKey of Object.keys(record)) if (!Object.hasOwn(SLOT_ROW_KEYS, nestedKey)) throw new Error(`dsh-llm-fallbacks: unknown config key "${at}.${nestedKey}"`);
870
+ if (record.kind === "preset") {
871
+ const preset = record.preset;
872
+ if (typeof preset !== "string" || !Object.hasOwn(PRESETS, preset)) throw new Error(`dsh-llm-fallbacks: ${at}.preset must be one of the four frozen preset ids (got ${JSON.stringify(preset)})`);
873
+ if (seenPresets.has(preset)) throw new Error(`dsh-llm-fallbacks: ${at} duplicates preset "${preset}" — at most one row per preset`);
874
+ seenPresets.add(preset);
875
+ if (record.start !== void 0 || record.end !== void 0 || Array.isArray(record.days) && record.days.length > 0) throw new Error(`dsh-llm-fallbacks: ${at} preset "${preset}" cannot carry start/end/days — preset windows are frozen code constants`);
876
+ if (record.name !== void 0) throw new Error(`dsh-llm-fallbacks: ${at} preset "${preset}" cannot carry a name — preset rows are named by the frozen label`);
877
+ } else if (record.kind === "custom") {
878
+ const { start, end } = record;
879
+ if (typeof start !== "string" || typeof end !== "string" || !SLOT_HHMM_RE.test(start) || !SLOT_HHMM_RE.test(end)) throw new Error(`dsh-llm-fallbacks: ${at} custom row requires HH:mm start and end (got ${JSON.stringify(start)}-${JSON.stringify(end)})`);
880
+ if (record.days !== void 0) {
881
+ if (!Array.isArray(record.days) || record.days.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) throw new Error(`dsh-llm-fallbacks: ${at}.days must be an array of integers 0–6`);
882
+ }
883
+ if (record.name !== void 0 && typeof record.name !== "string") throw new Error(`dsh-llm-fallbacks: ${at}.name must be a string`);
884
+ } else throw new Error(`dsh-llm-fallbacks: ${at}.kind must be "preset" or "custom" (got ${JSON.stringify(record.kind)})`);
885
+ const chain = record.chain;
886
+ if (!Array.isArray(chain) || chain.length === 0 || chain.some((entry) => typeof entry !== "string")) throw new Error(`dsh-llm-fallbacks: ${at}.chain must be a non-empty string array`);
887
+ });
888
+ }
618
889
  function fallbacksTypertContribution() {
619
890
  return {
620
891
  package: "dsh-llm-fallbacks",
@@ -685,7 +956,7 @@ const CONFIG_SUBCOMMAND_DESCRIPTION = {
685
956
  const FALLBACKS_COMMAND_LOCALES = {
686
957
  zh: {
687
958
  title: "当前会话 fallback 诊断(只读)",
688
- description: "查看当前会话的降级链、最近切换与冷却状态(只读)",
959
+ description: "查看当前会话的降级链、最近降级切换与冷却状态(只读)",
689
960
  usageConfig: CONFIG_SUBCOMMAND_DESCRIPTION.zh,
690
961
  usage: ` /fallbacks config ${CONFIG_SUBCOMMAND_DESCRIPTION.zh}`,
691
962
  origin: "会话来源",
@@ -693,7 +964,8 @@ const FALLBACKS_COMMAND_LOCALES = {
693
964
  chain: "链",
694
965
  inheritRoot: "(inherit-root)",
695
966
  chainNone: "未配置",
696
- switches: "最近切换",
967
+ slot: "分时",
968
+ switches: "最近降级切换",
697
969
  switchesNone: "本会话暂无 fallback 切换",
698
970
  switchLine: "{from} → {to}(role={role},reason={reason})",
699
971
  cooldown: "冷却",
@@ -723,7 +995,7 @@ const FALLBACKS_COMMAND_LOCALES = {
723
995
  },
724
996
  en: {
725
997
  title: "Session fallback diagnostics (read-only)",
726
- description: "Inspect fallback chain, recent switches, and cooldown for this session (read-only)",
998
+ description: "Inspect fallback chain, recent fallback switches, and cooldown for this session (read-only)",
727
999
  usageConfig: CONFIG_SUBCOMMAND_DESCRIPTION.en,
728
1000
  usage: ` /fallbacks config ${CONFIG_SUBCOMMAND_DESCRIPTION.en}`,
729
1001
  origin: "Session origin",
@@ -731,7 +1003,8 @@ const FALLBACKS_COMMAND_LOCALES = {
731
1003
  chain: "Chain",
732
1004
  inheritRoot: " (inherit-root)",
733
1005
  chainNone: "not configured",
734
- switches: "Recent switches",
1006
+ slot: "Time slot",
1007
+ switches: "Recent fallback switches",
735
1008
  switchesNone: "No fallback switches in this session",
736
1009
  switchLine: "{from} → {to} (role={role}, reason={reason})",
737
1010
  cooldown: "Cooldown",
@@ -845,6 +1118,7 @@ function fallbacksCommandText(snapshot, locale = "zh") {
845
1118
  const suffix = snapshot.inherit ? t.inheritRoot : "";
846
1119
  lines.push(`${t.chain}: ${snapshot.chain.join(" → ")}${suffix}`);
847
1120
  }
1121
+ lines.push(`${t.slot}: ${snapshot.slot.label}`);
848
1122
  if (snapshot.switches.length === 0) lines.push(`${t.switches}: ${t.switchesNone}`);
849
1123
  else {
850
1124
  lines.push(`${t.switches} (${snapshot.switches.length}):`);
@@ -1100,7 +1374,7 @@ function fallbacksChildren(canonicalPath) {
1100
1374
  if (canonicalPath.length === 1) return [FALLBACKS_CONFIG_NODE];
1101
1375
  return [];
1102
1376
  }
1103
- const FALLBACKS_PROVIDER = {
1377
+ const FALLBACKS_PROVIDER$1 = {
1104
1378
  root: FALLBACKS_TUI_ROOT,
1105
1379
  descriptions: {
1106
1380
  zh: FALLBACKS_COMMAND_LOCALES.zh.description,
@@ -1114,7 +1388,7 @@ function installTuiClient(ctx, opts) {
1114
1388
  const trees = tctx.tuiCommandTrees;
1115
1389
  if (trees === void 0) return;
1116
1390
  try {
1117
- return trees.register(FALLBACKS_PROVIDER);
1391
+ return trees.register(FALLBACKS_PROVIDER$1);
1118
1392
  } catch (error) {
1119
1393
  if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
1120
1394
  tctx.logger("llm-fallbacks").debug("llm-fallbacks: tui command tree already registered — no provider on this fiber");
@@ -1123,6 +1397,151 @@ function installTuiClient(ctx, opts) {
1123
1397
  });
1124
1398
  }
1125
1399
  //#endregion
1400
+ //#region .build/host/virtual-adapter.js
1401
+ const FALLBACKS_PROVIDER = "FallbacksChain";
1402
+ const FALLBACKS_CHAIN_MODEL = "Auto";
1403
+ const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
1404
+ const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
1405
+ const LLM_UNAVAILABLE_CODE = "LLM_UNAVAILABLE";
1406
+ function firstDispatchableExactHead(chain) {
1407
+ for (const entry of chain) {
1408
+ let selector;
1409
+ try {
1410
+ selector = parseSelector(entry);
1411
+ } catch {
1412
+ continue;
1413
+ }
1414
+ if (selector.model === void 0) continue;
1415
+ if (selector.provider === "FallbacksChain") continue;
1416
+ return {
1417
+ provider: selector.provider,
1418
+ model: selector.model
1419
+ };
1420
+ }
1421
+ }
1422
+ function effectiveHeadOf(config, now) {
1423
+ if (!isAllDayConforming(config.rootChain)) return void 0;
1424
+ return firstDispatchableExactHead(resolveEffectiveChain(config, now, config.tz ?? "Asia/Shanghai"));
1425
+ }
1426
+ function pickerDisplayName(config, now = new Date(), modelDisplayName) {
1427
+ const head = effectiveHeadOf(config, now);
1428
+ if (head === void 0) return FALLBACKS_CHAIN_MODEL;
1429
+ const slot = resolveSlotState(config, now, config.tz ?? "Asia/Shanghai");
1430
+ const model = modelDisplayName !== void 0 && modelDisplayName !== "" ? modelDisplayName : head.model;
1431
+ return `${FALLBACKS_CHAIN_MODEL}: ${model}[${slot.label}]`;
1432
+ }
1433
+ async function resolveHeadDisplayName(llm, head) {
1434
+ if (llm === void 0) return head.model;
1435
+ try {
1436
+ const row = (await llm.listModels(head.provider)).find((model) => model.id === head.model);
1437
+ if (row !== void 0 && row.name !== "") return row.name;
1438
+ } catch {}
1439
+ try {
1440
+ const info = await llm.resolveModelInfo(head.provider, head.model);
1441
+ if (info.name !== "") return info.name;
1442
+ } catch {}
1443
+ return head.model;
1444
+ }
1445
+ var FallbacksChainAdapter = class extends LlmAdapter {
1446
+ readConfig;
1447
+ getLlm;
1448
+ constructor(readConfig, getLlm) {
1449
+ super();
1450
+ this.readConfig = readConfig;
1451
+ this.getLlm = getLlm;
1452
+ }
1453
+ providerInfo(provider) {
1454
+ return {
1455
+ id: provider,
1456
+ name: FALLBACKS_PROVIDER
1457
+ };
1458
+ }
1459
+ async listModels(provider) {
1460
+ const config = this.readConfig();
1461
+ const now = new Date();
1462
+ const head = effectiveHeadOf(config, now);
1463
+ const display = head === void 0 ? void 0 : await resolveHeadDisplayName(this.getLlm(), head);
1464
+ return [{
1465
+ provider,
1466
+ id: FALLBACKS_CHAIN_MODEL,
1467
+ name: pickerDisplayName(config, now, display)
1468
+ }];
1469
+ }
1470
+ async resolveModel(provider, model, signal) {
1471
+ const head = effectiveHeadOf(this.readConfig(), new Date());
1472
+ const llm = this.getLlm();
1473
+ if (head !== void 0 && llm !== void 0) try {
1474
+ const info = await llm.resolveModelInfo(head.provider, head.model, signal);
1475
+ return {
1476
+ provider,
1477
+ id: model,
1478
+ name: info.name,
1479
+ ...info.description === void 0 ? {} : { description: info.description },
1480
+ ...info.inputModalities === void 0 ? {} : { inputModalities: info.inputModalities },
1481
+ ...info.context === void 0 ? {} : { context: info.context },
1482
+ ...info.defaultMaxTokens === void 0 ? {} : { defaultMaxTokens: info.defaultMaxTokens },
1483
+ ...info.reasoning === void 0 ? {} : { reasoning: info.reasoning }
1484
+ };
1485
+ } catch {}
1486
+ return {
1487
+ provider,
1488
+ id: model,
1489
+ name: model
1490
+ };
1491
+ }
1492
+ stream(options) {
1493
+ const config = this.readConfig();
1494
+ const now = new Date();
1495
+ if (resolveEffectiveChain(config, now, config.tz ?? "Asia/Shanghai").length === 0) throw new LlmError("llm-fallbacks: the effective chain is empty — the virtual FallbacksChain route has no head to delegate to", EMPTY_EFFECTIVE_CHAIN_CODE);
1496
+ const head = effectiveHeadOf(config, now);
1497
+ if (head === void 0) throw new LlmError("llm-fallbacks: the effective chain head cannot be dispatched (non-conforming all-day, wildcard, malformed, or self-route) — refusing to delegate", UNDISPATCHABLE_HEAD_CODE);
1498
+ const llm = this.getLlm();
1499
+ if (llm === void 0) throw new LlmError("llm-fallbacks: the llm runtime is unavailable — cannot delegate the FallbacksChain call", LLM_UNAVAILABLE_CODE);
1500
+ return llm.stream({
1501
+ ...options,
1502
+ provider: head.provider,
1503
+ model: head.model
1504
+ });
1505
+ }
1506
+ };
1507
+ function installFallbacksAdapter(ctx, readConfig) {
1508
+ let llm;
1509
+ let disposeAdapter;
1510
+ let registered = false;
1511
+ const logger = ctx.logger("llm-fallbacks");
1512
+ const adapter = new FallbacksChainAdapter(readConfig, () => llm);
1513
+ const reconcile = () => {
1514
+ const shouldRegister = readConfig().enabled;
1515
+ if (shouldRegister && !registered) {
1516
+ if (llm === void 0) return;
1517
+ try {
1518
+ disposeAdapter = llm.registerAdapter([FALLBACKS_PROVIDER], adapter);
1519
+ registered = true;
1520
+ logger.info("llm-fallbacks: virtual adapter registered — FallbacksChain/Auto is selectable in the model picker");
1521
+ } catch (error) {
1522
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "DUPLICATE_ADAPTER") throw error;
1523
+ logger.debug("llm-fallbacks: virtual adapter already registered — no route on this fiber (multi-fiber dedupe)");
1524
+ }
1525
+ } else if (!shouldRegister && registered) {
1526
+ disposeAdapter?.();
1527
+ disposeAdapter = void 0;
1528
+ registered = false;
1529
+ logger.info("llm-fallbacks: virtual adapter unregistered — FallbacksChain/Auto hidden from the model picker");
1530
+ }
1531
+ };
1532
+ ctx.inject(["llm"], (llmCtx) => {
1533
+ llm = llmCtx.llm;
1534
+ reconcile();
1535
+ return () => {
1536
+ disposeAdapter?.();
1537
+ disposeAdapter = void 0;
1538
+ registered = false;
1539
+ llm = void 0;
1540
+ };
1541
+ });
1542
+ return reconcile;
1543
+ }
1544
+ //#endregion
1126
1545
  //#region .build/host/index.js
1127
1546
  const name = "llm-fallbacks";
1128
1547
  const provide = ["llm-fallbacks"];
@@ -1163,6 +1582,10 @@ function currentModel(agent, provider) {
1163
1582
  model: agent.session.requestHeader()?.config.model ?? agent.options.model ?? ""
1164
1583
  };
1165
1584
  }
1585
+ function slotWinnerKey(winner) {
1586
+ if (winner === "all-day") return "all-day";
1587
+ return winner.kind === "preset" ? `preset:${winner.preset}` : `custom:${winner.start}-${winner.end}`;
1588
+ }
1166
1589
  function overrideConfig(seed, to) {
1167
1590
  const { reasoningEffort: _inherited, ...withoutInheritedEffort } = seed;
1168
1591
  return {
@@ -1211,6 +1634,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1211
1634
  ctx.logger("llm-fallbacks").debug("fallbacks service already registered — no service on this fiber (multi-fiber dedupe)");
1212
1635
  }
1213
1636
  let source = () => entry;
1637
+ const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source());
1214
1638
  validateFallbacksConfig(entry, logger);
1215
1639
  const legacyKeys = detectLegacyKeys(source());
1216
1640
  if (legacyKeys.length > 0) logger.warn("llm-fallbacks: legacy config keys detected (chains/roles.default/undeclared role refs); see docs/configuration.md migration table — %o", legacyKeys);
@@ -1224,6 +1648,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
1224
1648
  const current = source();
1225
1649
  roleIds = new Map(current.roles.list.map((role) => [role.id.trim(), role.id]));
1226
1650
  hasChains = current.rootChain.length > 0 || current.roles.list.some((role) => (role.chain?.length ?? 0) > 0);
1651
+ reconcileFallbacksAdapter();
1652
+ slotWinners.clear();
1227
1653
  }
1228
1654
  });
1229
1655
  const bridge = { source: () => source() };
@@ -1245,6 +1671,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1245
1671
  const states = new FallbackStateStore();
1246
1672
  stateStores.set(ctx, states);
1247
1673
  const dispatchInjected = new Set();
1674
+ const slotWinners = new Map();
1248
1675
  async function decide(agent, turn, step, current, reason, state) {
1249
1676
  const config = source();
1250
1677
  if (state !== void 0) {
@@ -1252,9 +1679,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
1252
1679
  if (state.stepFailures.switchCount >= config.maxSwitchesPerStep) return null;
1253
1680
  }
1254
1681
  const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn);
1255
- const { all, wildcard } = resolveChainViews(config.roles.list, config.rootChain, role, current.provider, current.model, logger.warn);
1682
+ const rootTail = agent.session?.header?.origin === "subagent" ? config.rootChain : resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai");
1683
+ const { all, wildcard } = resolveChainViews(config.roles.list, rootTail, role, current.provider, current.model, logger.warn);
1256
1684
  if (all.length === 0) return null;
1257
- const modelExists = hasWildcardEntry(config.roles.list, config.rootChain, role) ? await makeModelExists(ctx, [...new Set(all.map((candidate) => candidate.provider))]) : void 0;
1685
+ const modelExists = hasWildcardEntry(config.roles.list, rootTail, role) ? await makeModelExists(ctx, [...new Set(all.map((candidate) => candidate.provider))]) : void 0;
1258
1686
  const cooldown = { isSuppressed: (key) => state !== void 0 && states.isSuppressed(state, key) };
1259
1687
  const failed = { has: (key) => state !== void 0 && state.stepFailures.failed.has(key) };
1260
1688
  const surviving = selectCandidates(all, wildcard, createCandidateFilter({
@@ -1264,7 +1692,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1264
1692
  }), modelExists);
1265
1693
  const target = surviving[0];
1266
1694
  if (target === void 0 || target.model === void 0) return null;
1267
- logger.info("llm-fallbacks: agent \"%s\" switch %s/%s -> %s/%s (role=%s, reason=%s, candidates=%o)", agent.id, current.provider, current.model, target.provider, target.model, role, reason, annotateCandidates(all, surviving, {
1695
+ logger.info("llm-fallbacks: agent \"%s\" fallback switch (降级切换) %s/%s -> %s/%s (role=%s, reason=%s, candidates=%o)", agent.id, current.provider, current.model, target.provider, target.model, role, reason, annotateCandidates(all, surviving, {
1268
1696
  current,
1269
1697
  cooldown,
1270
1698
  failed
@@ -1313,6 +1741,27 @@ function apply(ctx, config = defaultFallbacksConfig) {
1313
1741
  const applied = state === void 0 ? void 0 : states.applyPending(state, turn, step);
1314
1742
  if (applied !== void 0) return overrideConfig(seed, applied.to);
1315
1743
  const config = source();
1744
+ if (config.enabled && isAllDayConforming(config.rootChain) && (config.timeSlots?.length ?? 0) > 0 && agent.session?.header?.origin !== "subagent") {
1745
+ const slot = resolveSlotState(config, new Date(), config.tz ?? "Asia/Shanghai");
1746
+ const key = slotWinnerKey(slot.winner);
1747
+ const previous = slotWinners.get(agent.id);
1748
+ if (previous !== void 0 && previous.key !== key) logger.info("llm-fallbacks: agent \"%s\" time-slot switch (分时切换): %s -> %s", agent.id, previous.label, slot.label);
1749
+ slotWinners.set(agent.id, {
1750
+ key,
1751
+ label: slot.label
1752
+ });
1753
+ }
1754
+ if (config.enabled && seed.provider === "FallbacksChain" && seed.model === "Auto" && agent.session?.header?.origin !== "subagent") {
1755
+ if (!isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: FallbacksChain/Auto selected but the all-day rootChain is not conforming (exactly one official V4 model) — no primary override");
1756
+ else {
1757
+ const head = firstDispatchableExactHead(resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai"));
1758
+ if (head === void 0) logger.warn("llm-fallbacks: FallbacksChain/Auto selected but the effective chain has no exact head (empty, wildcard-only, or self-route) — no primary override");
1759
+ else {
1760
+ logger.info("llm-fallbacks: FallbacksChain/Auto selection overrides to the effective head %s/%s", head.provider, head.model);
1761
+ return overrideConfig(seed, head);
1762
+ }
1763
+ }
1764
+ }
1316
1765
  if (config.enabled && hasChains && agent.session?.header?.origin === "subagent" && !dispatchInjected.has(agent.id)) {
1317
1766
  dispatchInjected.add(agent.id);
1318
1767
  try {
@@ -1360,10 +1809,12 @@ function apply(ctx, config = defaultFallbacksConfig) {
1360
1809
  ctx.on("agent/disposed", ({ agent }) => {
1361
1810
  states.delete(agent.id);
1362
1811
  dispatchInjected.delete(agent.id);
1812
+ slotWinners.delete(agent.id);
1363
1813
  });
1364
1814
  ctx.effect(() => () => {
1365
1815
  states.clear();
1366
1816
  dispatchInjected.clear();
1817
+ slotWinners.clear();
1367
1818
  }, "llm-fallbacks: clear per-agent state");
1368
1819
  const fallbacksCommandController = {
1369
1820
  getSnapshot(agent) {
@@ -1374,6 +1825,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1374
1825
  origin: agent.session.header?.origin ?? "root",
1375
1826
  role,
1376
1827
  ...resolveChainForDiagnostic(config.roles.list, config.rootChain, role, logger.warn),
1828
+ slot: resolveSlotState(config, new Date(), config.tz ?? "Asia/Shanghai"),
1377
1829
  switches: recentFallbacksSwitches(agent.session.events, 5),
1378
1830
  cooldown: state === void 0 ? [] : state.cooldown.snapshot()
1379
1831
  };