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/README.md +67 -9
- package/README.zh-CN.md +66 -8
- package/dist/client/FallbacksCard.d.ts +27 -13
- package/dist/client/GeneralFallbacksRow.d.ts +1 -1
- package/dist/client/fallbacks-store.d.ts +48 -4
- package/dist/client/index.js +2349 -938
- package/dist/client/locales.d.ts +118 -26
- package/dist/client/use-snapshot.d.ts +11 -0
- package/dist/commands.d.ts +18 -7
- package/dist/config.d.ts +22 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +467 -15
- package/dist/roles.d.ts +12 -5
- package/dist/time-slots.d.ts +104 -0
- package/dist/virtual-adapter.d.ts +90 -0
- package/package.json +26 -23
- package/scripts/build-client.ts +9 -9
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
};
|