dsh-llm-fallbacks 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -27
- package/README.zh-CN.md +25 -25
- package/dist/chain-walk.d.ts +63 -77
- package/dist/chains.d.ts +32 -101
- package/dist/client/FallbacksCard.d.ts +27 -80
- package/dist/client/fallbacks-store.d.ts +71 -225
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.js +1256 -1926
- package/dist/client/locales.d.ts +59 -105
- package/dist/commands.d.ts +68 -103
- package/dist/config.d.ts +152 -105
- package/dist/gateway.d.ts +63 -109
- package/dist/index.d.ts +3 -25
- package/dist/index.js +552 -775
- package/dist/role-resolution.d.ts +14 -43
- package/dist/schema.d.ts +6 -5
- package/dist/seeds.d.ts +5 -7
- package/dist/state.d.ts +0 -42
- package/dist/subagents-seam.d.ts +23 -45
- package/dist/time-slots.d.ts +21 -24
- package/dist/tui-settings.d.ts +5 -8
- package/dist/virtual-adapter.d.ts +63 -108
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
- package/dist/client/GeneralFallbacksRow.d.ts +0 -19
- package/dist/roles.d.ts +0 -89
- package/dist/route-allowlist.d.ts +0 -43
- package/dist/subagent-policy.d.ts +0 -137
package/dist/index.js
CHANGED
|
@@ -227,18 +227,18 @@ function isAllDayConforming(chain) {
|
|
|
227
227
|
const tail = chain[chain.length - 1];
|
|
228
228
|
return isOfficialAllDayId(tail);
|
|
229
229
|
}
|
|
230
|
-
function resolveEffectiveChain(
|
|
231
|
-
const state = resolveSlotState(
|
|
232
|
-
return state.winner === "all-day" ?
|
|
230
|
+
function resolveEffectiveChain(chain, now, tz) {
|
|
231
|
+
const state = resolveSlotState(chain, now, tz);
|
|
232
|
+
return state.winner === "all-day" ? chain.entries : state.winner.chain;
|
|
233
233
|
}
|
|
234
|
-
function resolveSlotState(
|
|
235
|
-
if (!isAllDayConforming(
|
|
234
|
+
function resolveSlotState(chain, now, tz) {
|
|
235
|
+
if (!isAllDayConforming(chain.entries)) return {
|
|
236
236
|
winner: "all-day",
|
|
237
237
|
label: "all-day"
|
|
238
238
|
};
|
|
239
239
|
const clock = wallClock(now, tz);
|
|
240
240
|
const seenPresets = new Set();
|
|
241
|
-
const rows = Array.isArray(
|
|
241
|
+
const rows = Array.isArray(chain.timeSlots) ? chain.timeSlots : [];
|
|
242
242
|
for (const row of rows) {
|
|
243
243
|
if (typeof row !== "object" || row === null) {
|
|
244
244
|
warnMalformed(row, "row is not an object");
|
|
@@ -324,31 +324,57 @@ var RecoveryStore = class {
|
|
|
324
324
|
};
|
|
325
325
|
//#endregion
|
|
326
326
|
//#region .build/host/config.js
|
|
327
|
+
const CHAIN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,31}$/;
|
|
327
328
|
const defaultFallbacksConfig = {
|
|
328
329
|
triggerCodes: [
|
|
329
330
|
"AUTH",
|
|
330
331
|
"QUOTA",
|
|
331
332
|
"RATE_LIMIT"
|
|
332
333
|
],
|
|
333
|
-
|
|
334
|
-
roles: {
|
|
335
|
-
list: [],
|
|
336
|
-
rules: []
|
|
337
|
-
},
|
|
334
|
+
chains: [],
|
|
335
|
+
roles: { list: [] },
|
|
338
336
|
cooldownMs: 3e5,
|
|
339
337
|
revertPolicy: "cooldown-expiry",
|
|
340
338
|
maxSwitchesPerStep: 8,
|
|
341
339
|
presets: "bundled",
|
|
342
|
-
timeSlots: [],
|
|
343
340
|
tz: "Asia/Shanghai",
|
|
344
341
|
recovery: "timer"
|
|
345
342
|
};
|
|
343
|
+
const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
346
344
|
const INHERIT_ROLE_ID = "inherit";
|
|
347
345
|
function isFallbackActive(config) {
|
|
348
|
-
return config.
|
|
346
|
+
return config.chains.length > 0;
|
|
349
347
|
}
|
|
350
|
-
const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
351
348
|
function validateFallbacksConfig(config, logger) {
|
|
349
|
+
const declaredChainIds = new Set();
|
|
350
|
+
for (const chain of config.chains) {
|
|
351
|
+
if (!CHAIN_ID_PATTERN.test(chain.id)) logger.warn(`llm-fallbacks: invalid chain id "${chain.id}" — must match /^[A-Za-z0-9][A-Za-z0-9-]{0,31}$/`);
|
|
352
|
+
if (declaredChainIds.has(chain.id)) logger.warn(`llm-fallbacks: duplicate chain id "${chain.id}" — chain ids must be unique (case-sensitive)`);
|
|
353
|
+
declaredChainIds.add(chain.id);
|
|
354
|
+
if (chain.entries.length > 0 && !isAllDayConforming(chain.entries)) logger.warn(`llm-fallbacks: chain "${chain.id}" must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro) — its time-slot rows and the virtual picker delegate stay inert until the tail conforms`);
|
|
355
|
+
for (const entry of chain.entries) try {
|
|
356
|
+
parseSelector(entry);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
logger.warn(`llm-fallbacks: ignoring invalid chain "${chain.id}" entry "${entry}": ${error.message}`);
|
|
359
|
+
}
|
|
360
|
+
if (Array.isArray(chain.timeSlots)) {
|
|
361
|
+
const seenSlotPresets = new Set();
|
|
362
|
+
for (const row of chain.timeSlots) {
|
|
363
|
+
if (row.kind !== "preset") continue;
|
|
364
|
+
const preset = row.preset;
|
|
365
|
+
if (typeof preset !== "string" || !Object.hasOwn(PRESETS, preset)) {
|
|
366
|
+
logger.warn(`llm-fallbacks: chain "${chain.id}" has an unknown time-slot preset ${JSON.stringify(preset)} — row ignored`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (seenSlotPresets.has(preset)) {
|
|
370
|
+
logger.warn(`llm-fallbacks: chain "${chain.id}" has a duplicate time-slot preset "${preset}" — only the first row takes effect`);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
seenSlotPresets.add(preset);
|
|
374
|
+
if (row.start !== void 0 || row.end !== void 0 || row.days !== void 0 && row.days.length > 0) logger.warn(`llm-fallbacks: chain "${chain.id}" preset "${preset}" carries its own window/day fields — preset windows are fixed code constants; the row is ignored`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
352
378
|
const declaredIds = new Set();
|
|
353
379
|
for (const role of config.roles.list) {
|
|
354
380
|
const id = role.id.trim();
|
|
@@ -356,106 +382,150 @@ function validateFallbacksConfig(config, logger) {
|
|
|
356
382
|
if (id === "inherit") logger.warn(`llm-fallbacks: role id "${role.id}" is reserved — "inherit" cannot be declared in roles.list`);
|
|
357
383
|
if (declaredIds.has(id)) logger.warn(`llm-fallbacks: duplicate role id "${role.id}" — role ids must be unique`);
|
|
358
384
|
declaredIds.add(id);
|
|
359
|
-
|
|
360
|
-
parseSelector(entry);
|
|
361
|
-
} catch (error) {
|
|
362
|
-
logger.warn(`llm-fallbacks: ignoring invalid chain entry "${entry}" in role "${role.id}": ${error.message}`);
|
|
363
|
-
}
|
|
364
|
-
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`);
|
|
365
|
-
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"`);
|
|
366
|
-
}
|
|
367
|
-
if (config.rootChain.length > 0 && !isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: rootChain must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro) — time-slot rows and the virtual picker row stay inert until the all-day chain tail conforms");
|
|
368
|
-
for (const entry of config.rootChain) try {
|
|
369
|
-
parseSelector(entry);
|
|
370
|
-
} catch (error) {
|
|
371
|
-
logger.warn(`llm-fallbacks: ignoring invalid rootChain entry "${entry}": ${error.message}`);
|
|
372
|
-
}
|
|
373
|
-
const validTargets = new Set([...declaredIds, INHERIT_ROLE_ID]);
|
|
374
|
-
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"`);
|
|
375
|
-
if (Array.isArray(config.timeSlots)) {
|
|
376
|
-
const seenSlotPresets = new Set();
|
|
377
|
-
for (const row of config.timeSlots) {
|
|
378
|
-
if (row.kind !== "preset") continue;
|
|
379
|
-
const preset = row.preset;
|
|
380
|
-
if (typeof preset !== "string" || !Object.hasOwn(PRESETS, preset)) {
|
|
381
|
-
logger.warn(`llm-fallbacks: unknown time-slot preset ${JSON.stringify(preset)} — row ignored`);
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (seenSlotPresets.has(preset)) {
|
|
385
|
-
logger.warn(`llm-fallbacks: duplicate time-slot preset "${preset}" — only the first row takes effect`);
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
seenSlotPresets.add(preset);
|
|
389
|
-
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`);
|
|
390
|
-
}
|
|
385
|
+
if (role.chain !== void 0 && role.chain !== "" && !declaredChainIds.has(role.chain)) logger.warn(`llm-fallbacks: role "${role.id}" specifies unknown chain "${role.chain}" — the child inherits the delegating agent's route`);
|
|
391
386
|
}
|
|
392
387
|
if ((config.recovery ?? "timer") === "half-open" && config.cooldownMs >= 36e5) logger.warn(`llm-fallbacks: recovery "half-open" escalation is inert — cooldownMs (${config.cooldownMs} ms) is at or above the 1-hour escalation cap (${ESCALATION_CAP_MS} ms), so every suppression stays flat at cooldownMs`);
|
|
393
388
|
}
|
|
394
389
|
function detectLegacyKeys(source) {
|
|
395
|
-
const keys =
|
|
396
|
-
if (Object.hasOwn(source, "
|
|
397
|
-
if (Object.hasOwn(source, "
|
|
398
|
-
if (Object.hasOwn(source, "
|
|
390
|
+
const keys = new Set();
|
|
391
|
+
if (Object.hasOwn(source, "alwaysModeRetryCap")) keys.add("alwaysModeRetryCap");
|
|
392
|
+
if (Object.hasOwn(source, "roleAutoMatch")) keys.add("roleAutoMatch");
|
|
393
|
+
if (Object.hasOwn(source, "rootChain")) keys.add("rootChain");
|
|
394
|
+
if (Object.hasOwn(source, "timeSlots")) keys.add("timeSlots");
|
|
399
395
|
const roles = source.roles;
|
|
400
396
|
if (isRecordLike(roles)) {
|
|
401
|
-
if (Object.hasOwn(roles, "default")) keys.
|
|
402
|
-
|
|
397
|
+
if (Object.hasOwn(roles, "default")) keys.add("roles.default");
|
|
398
|
+
if (Object.hasOwn(roles, "rules")) keys.add("roles.rules");
|
|
403
399
|
if (Array.isArray(roles.list)) for (const item of roles.list) {
|
|
404
400
|
if (!isRecordLike(item)) continue;
|
|
405
|
-
if (
|
|
406
|
-
if (Object.hasOwn(item, "
|
|
407
|
-
if (Object.hasOwn(item, "
|
|
408
|
-
|
|
409
|
-
if (Array.isArray(roles.rules)) {
|
|
410
|
-
for (const rule of roles.rules) if (isRecordLike(rule) && typeof rule.role === "string" && rule.role !== "inherit" && !declared.has(rule.role)) keys.push(`roles.rules[].role: ${rule.role}`);
|
|
401
|
+
if (Object.hasOwn(item, "label")) keys.add("roles.list[].label");
|
|
402
|
+
if (Object.hasOwn(item, "description")) keys.add("roles.list[].description");
|
|
403
|
+
if (Object.hasOwn(item, "fallback")) keys.add("roles.list[].fallback");
|
|
404
|
+
if (Array.isArray(item.chain)) keys.add("roles.list[].chain");
|
|
411
405
|
}
|
|
412
406
|
}
|
|
413
|
-
return keys;
|
|
407
|
+
return [...keys];
|
|
414
408
|
}
|
|
415
409
|
function isRecordLike(value) {
|
|
416
410
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
417
411
|
}
|
|
412
|
+
const MIGRATED_CHAIN_ID = "Auto";
|
|
413
|
+
const MIGRATED_CHAIN_LABEL = "默认链";
|
|
414
|
+
function migrateLegacyConfig(source) {
|
|
415
|
+
const raw = isRecordLike(source) ? source : {};
|
|
416
|
+
const defaults = defaultFallbacksConfig;
|
|
417
|
+
return {
|
|
418
|
+
triggerCodes: stringArrayOf(raw["triggerCodes"]) ?? [...defaults.triggerCodes],
|
|
419
|
+
chains: migrateChains(raw),
|
|
420
|
+
roles: { list: migrateRoles(raw["roles"]) },
|
|
421
|
+
cooldownMs: finiteNumberOr(raw["cooldownMs"], defaults.cooldownMs),
|
|
422
|
+
revertPolicy: raw["revertPolicy"] === "never" ? "never" : defaults.revertPolicy,
|
|
423
|
+
maxSwitchesPerStep: finiteNumberOr(raw["maxSwitchesPerStep"], defaults.maxSwitchesPerStep),
|
|
424
|
+
presets: raw["presets"] === "none" ? "none" : defaults.presets,
|
|
425
|
+
tz: typeof raw["tz"] === "string" && raw["tz"] !== "" ? raw["tz"] : defaults.tz,
|
|
426
|
+
recovery: raw["recovery"] === "half-open" ? "half-open" : defaults.recovery
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function migrateChains(raw) {
|
|
430
|
+
const declared = Array.isArray(raw["chains"]) ? raw["chains"] : void 0;
|
|
431
|
+
if (declared !== void 0 && declared.length > 0) {
|
|
432
|
+
const chains = [];
|
|
433
|
+
for (const entry of declared) {
|
|
434
|
+
const chain = normalizeChain(entry);
|
|
435
|
+
if (chain !== void 0) chains.push(chain);
|
|
436
|
+
}
|
|
437
|
+
return chains;
|
|
438
|
+
}
|
|
439
|
+
const rootChain = stringArrayOf(raw["rootChain"]);
|
|
440
|
+
const legacyRows = Array.isArray(raw["timeSlots"]) ? raw["timeSlots"].filter(isRecordLike).map((row) => ({ ...row })) : [];
|
|
441
|
+
if (rootChain === void 0 || rootChain.length === 0) return [];
|
|
442
|
+
return [{
|
|
443
|
+
id: MIGRATED_CHAIN_ID,
|
|
444
|
+
label: MIGRATED_CHAIN_LABEL,
|
|
445
|
+
entries: [...rootChain],
|
|
446
|
+
timeSlots: legacyRows
|
|
447
|
+
}];
|
|
448
|
+
}
|
|
449
|
+
function normalizeChain(value) {
|
|
450
|
+
if (!isRecordLike(value)) return void 0;
|
|
451
|
+
const id = typeof value["id"] === "string" ? value["id"] : void 0;
|
|
452
|
+
if (id === void 0) return void 0;
|
|
453
|
+
const timeSlots = Array.isArray(value["timeSlots"]) ? value["timeSlots"].filter(isRecordLike).map((row) => ({ ...row })) : [];
|
|
454
|
+
return {
|
|
455
|
+
id,
|
|
456
|
+
label: typeof value["label"] === "string" ? value["label"] : id,
|
|
457
|
+
entries: stringArrayOf(value["entries"]) ?? [],
|
|
458
|
+
timeSlots
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function migrateRoles(value) {
|
|
462
|
+
if (!isRecordLike(value)) return [];
|
|
463
|
+
const list = Array.isArray(value["list"]) ? value["list"] : [];
|
|
464
|
+
const roles = [];
|
|
465
|
+
for (const row of list) {
|
|
466
|
+
if (!isRecordLike(row)) continue;
|
|
467
|
+
const id = typeof row["id"] === "string" ? row["id"] : void 0;
|
|
468
|
+
if (id === void 0) continue;
|
|
469
|
+
const role = {
|
|
470
|
+
...row,
|
|
471
|
+
id,
|
|
472
|
+
persona: typeof row["persona"] === "string" ? row["persona"] : ""
|
|
473
|
+
};
|
|
474
|
+
for (const stale of [
|
|
475
|
+
"fallback",
|
|
476
|
+
"label",
|
|
477
|
+
"description"
|
|
478
|
+
]) delete role[stale];
|
|
479
|
+
if (typeof row["chain"] !== "string" || row["chain"] === "") delete role["chain"];
|
|
480
|
+
roles.push(role);
|
|
481
|
+
}
|
|
482
|
+
return roles;
|
|
483
|
+
}
|
|
484
|
+
function stringArrayOf(value) {
|
|
485
|
+
if (!Array.isArray(value)) return void 0;
|
|
486
|
+
const out = [];
|
|
487
|
+
for (const item of value) {
|
|
488
|
+
if (typeof item !== "string") return void 0;
|
|
489
|
+
out.push(item);
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
function finiteNumberOr(value, fallback) {
|
|
494
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
495
|
+
}
|
|
418
496
|
const Config = z.object({
|
|
419
497
|
triggerCodes: z.array(z.string()).default([
|
|
420
498
|
"AUTH",
|
|
421
499
|
"QUOTA",
|
|
422
500
|
"RATE_LIMIT"
|
|
423
501
|
]),
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
chain: z.array(z.string())
|
|
435
|
-
fallback: z.union([z.const("inherit-root"), z.const("none")]).default("inherit-root")
|
|
436
|
-
})).default([]),
|
|
437
|
-
rules: z.array(z.object({
|
|
438
|
-
origin: z.union([z.const("root"), z.const("subagent")]),
|
|
439
|
-
provider: z.string(),
|
|
440
|
-
model: z.string(),
|
|
441
|
-
role: z.string().required()
|
|
502
|
+
chains: z.array(z.object({
|
|
503
|
+
id: z.string().required(),
|
|
504
|
+
label: z.string().required(),
|
|
505
|
+
entries: z.array(z.string()).default([]),
|
|
506
|
+
timeSlots: z.array(z.object({
|
|
507
|
+
kind: z.string(),
|
|
508
|
+
preset: z.string(),
|
|
509
|
+
start: z.string(),
|
|
510
|
+
end: z.string(),
|
|
511
|
+
days: z.array(z.number()),
|
|
512
|
+
chain: z.array(z.string())
|
|
442
513
|
})).default([])
|
|
443
|
-
}).default(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
514
|
+
})).default([]),
|
|
515
|
+
roles: z.object({ list: z.array(z.object({
|
|
516
|
+
id: z.string().required(),
|
|
517
|
+
persona: z.string().default(""),
|
|
518
|
+
prompt: z.string(),
|
|
519
|
+
permissions: z.object({
|
|
520
|
+
allow: z.array(z.string()),
|
|
521
|
+
deny: z.array(z.string())
|
|
522
|
+
}),
|
|
523
|
+
chain: z.union([z.string(), z.array(z.string())])
|
|
524
|
+
})).default([]) }).default({ list: [] }),
|
|
447
525
|
cooldownMs: z.number().default(3e5),
|
|
448
526
|
revertPolicy: z.union([z.const("cooldown-expiry"), z.const("never")]).default("cooldown-expiry"),
|
|
449
527
|
maxSwitchesPerStep: z.number().default(8),
|
|
450
528
|
presets: z.union([z.const("bundled"), z.const("none")]).default("bundled"),
|
|
451
|
-
timeSlots: z.array(z.object({
|
|
452
|
-
kind: z.string(),
|
|
453
|
-
preset: z.string(),
|
|
454
|
-
start: z.string(),
|
|
455
|
-
end: z.string(),
|
|
456
|
-
days: z.array(z.number()),
|
|
457
|
-
chain: z.array(z.string())
|
|
458
|
-
})).default([]),
|
|
459
529
|
tz: z.string().default("Asia/Shanghai"),
|
|
460
530
|
recovery: z.union([z.const("timer"), z.const("half-open")]).default("timer")
|
|
461
531
|
}).extra("volatile", true);
|
|
@@ -475,19 +545,11 @@ function resolveCandidate(entry, failing, modelExists) {
|
|
|
475
545
|
}
|
|
476
546
|
return selector;
|
|
477
547
|
}
|
|
478
|
-
function
|
|
479
|
-
if (role.trim() === "inherit") return rootChain;
|
|
480
|
-
const roleDef = roles.find((declared) => declared.id.trim() === role.trim());
|
|
481
|
-
if (roleDef === void 0) return rootChain;
|
|
482
|
-
return [...roleDef.chain ?? [], ...roleDef.fallback === "none" ? [] : rootChain];
|
|
483
|
-
}
|
|
484
|
-
function resolveChainViews(roles, rootChain, role, provider, model, warn = console.warn) {
|
|
548
|
+
function resolveChainViews(entries, provider, model, warn = console.warn) {
|
|
485
549
|
const failing = {
|
|
486
550
|
provider,
|
|
487
551
|
model
|
|
488
552
|
};
|
|
489
|
-
const entries = roleChainEntries(roles, rootChain, role);
|
|
490
|
-
if (role.trim() !== "inherit" && !roles.some((declared) => declared.id.trim() === role.trim())) warn(`llm-fallbacks: unknown role "${role}" — falling back to rootChain`);
|
|
491
553
|
const all = [];
|
|
492
554
|
const wildcard = [];
|
|
493
555
|
for (const entry of entries) {
|
|
@@ -517,12 +579,11 @@ function selectCandidates(all, wildcard, filter, modelExists) {
|
|
|
517
579
|
}
|
|
518
580
|
return surviving;
|
|
519
581
|
}
|
|
520
|
-
function resolveChain(
|
|
521
|
-
const { all, wildcard } = resolveChainViews(
|
|
582
|
+
function resolveChain(entries, provider, model, filter, modelExists, warn = console.warn) {
|
|
583
|
+
const { all, wildcard } = resolveChainViews(entries, provider, model, warn);
|
|
522
584
|
return selectCandidates(all, wildcard, filter, modelExists);
|
|
523
585
|
}
|
|
524
|
-
function hasWildcardEntry(
|
|
525
|
-
const entries = roleChainEntries(roles, rootChain, role);
|
|
586
|
+
function hasWildcardEntry(entries) {
|
|
526
587
|
for (const entry of entries) try {
|
|
527
588
|
if (parseSelector(entry).model === void 0) return true;
|
|
528
589
|
} catch {}
|
|
@@ -574,66 +635,9 @@ function annotateCandidates(candidates, surviving, options) {
|
|
|
574
635
|
});
|
|
575
636
|
}
|
|
576
637
|
//#endregion
|
|
577
|
-
//#region .build/host/route-allowlist.js
|
|
578
|
-
function resolvedRoutes(candidates) {
|
|
579
|
-
const routes = [];
|
|
580
|
-
for (const candidate of candidates) {
|
|
581
|
-
if (candidate.model === void 0) continue;
|
|
582
|
-
routes.push({
|
|
583
|
-
provider: candidate.provider,
|
|
584
|
-
model: candidate.model
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
return routes;
|
|
588
|
-
}
|
|
589
|
-
function routeOnAllowlist(route, allowedModels) {
|
|
590
|
-
return allowedModels.some((allowed) => allowed.provider === route.provider && allowed.model === route.model);
|
|
591
|
-
}
|
|
592
|
-
function firstAllowedCandidate(resolved, allowedModels) {
|
|
593
|
-
return resolved.find((route) => routeOnAllowlist(route, allowedModels));
|
|
594
|
-
}
|
|
595
|
-
//#endregion
|
|
596
|
-
//#region .build/host/roles.js
|
|
597
|
-
function resolveRole(agent, rules, roleIds, warn = console.warn, servedRoute) {
|
|
598
|
-
if ((agent.session?.header?.origin ?? "root") !== "subagent") return INHERIT_ROLE_ID;
|
|
599
|
-
const recordedRoute = {
|
|
600
|
-
provider: agent.options?.provider,
|
|
601
|
-
model: agent.options?.model
|
|
602
|
-
};
|
|
603
|
-
for (const rule of rules) {
|
|
604
|
-
const matchesPair = (pair) => (!rule.provider || rule.provider === pair.provider) && (!rule.model || rule.model === pair.model);
|
|
605
|
-
if (!matchesPair(recordedRoute) && !(servedRoute !== void 0 && matchesPair(servedRoute))) continue;
|
|
606
|
-
const target = rule.role.trim();
|
|
607
|
-
if (target === "inherit") return INHERIT_ROLE_ID;
|
|
608
|
-
const declared = roleIds.get(target);
|
|
609
|
-
if (declared === void 0) {
|
|
610
|
-
warn(`llm-fallbacks: rule references undeclared role "${rule.role}" — falling back to "inherit"`);
|
|
611
|
-
return INHERIT_ROLE_ID;
|
|
612
|
-
}
|
|
613
|
-
return declared;
|
|
614
|
-
}
|
|
615
|
-
return INHERIT_ROLE_ID;
|
|
616
|
-
}
|
|
617
|
-
//#endregion
|
|
618
|
-
//#region .build/host/role-resolution.js
|
|
619
|
-
function resolveDeclaredRole(agent, rules, roleIds, opts) {
|
|
620
|
-
const preset = agent.session?.header?.agentPreset?.trim();
|
|
621
|
-
if (preset !== void 0 && preset !== "" && preset !== "inherit") {
|
|
622
|
-
const declared = roleIds.get(preset);
|
|
623
|
-
if (declared !== void 0) return declared;
|
|
624
|
-
}
|
|
625
|
-
const dispatch = opts.dispatchRole?.trim();
|
|
626
|
-
if (dispatch !== void 0 && dispatch !== "" && dispatch !== "inherit") {
|
|
627
|
-
const declared = roleIds.get(dispatch);
|
|
628
|
-
if (declared !== void 0) return declared;
|
|
629
|
-
}
|
|
630
|
-
return resolveRole(agent, rules, roleIds, opts.warn, opts.servedRoute);
|
|
631
|
-
}
|
|
632
|
-
//#endregion
|
|
633
638
|
//#region .build/host/virtual-adapter.js
|
|
634
639
|
const FALLBACKS_PROVIDER$1 = "FallbacksChain";
|
|
635
|
-
|
|
636
|
-
function firstDispatchableExactHead(chain, allowedModels) {
|
|
640
|
+
function firstDispatchableExactHead(chain) {
|
|
637
641
|
for (const entry of chain) {
|
|
638
642
|
let selector;
|
|
639
643
|
try {
|
|
@@ -643,24 +647,22 @@ function firstDispatchableExactHead(chain, allowedModels) {
|
|
|
643
647
|
}
|
|
644
648
|
if (selector.model === void 0) continue;
|
|
645
649
|
if (selector.provider === "FallbacksChain") continue;
|
|
646
|
-
|
|
650
|
+
return {
|
|
647
651
|
provider: selector.provider,
|
|
648
652
|
model: selector.model
|
|
649
653
|
};
|
|
650
|
-
if (allowedModels !== void 0 && !routeOnAllowlist(head, allowedModels)) continue;
|
|
651
|
-
return head;
|
|
652
654
|
}
|
|
653
655
|
}
|
|
654
|
-
function effectiveHeadOf(
|
|
655
|
-
if (!isAllDayConforming(
|
|
656
|
-
return firstDispatchableExactHead(resolveEffectiveChain(
|
|
656
|
+
function effectiveHeadOf(chain, now, tz = "Asia/Shanghai") {
|
|
657
|
+
if (!isAllDayConforming(chain.entries)) return void 0;
|
|
658
|
+
return firstDispatchableExactHead(resolveEffectiveChain(chain, now, tz));
|
|
657
659
|
}
|
|
658
|
-
function pickerDisplayName(
|
|
659
|
-
const head = effectiveHeadOf(
|
|
660
|
-
if (head === void 0) return
|
|
661
|
-
const slot = resolveSlotState(
|
|
660
|
+
function pickerDisplayName(chain, now = new Date(), modelDisplayName, tz = "Asia/Shanghai") {
|
|
661
|
+
const head = effectiveHeadOf(chain, now, tz);
|
|
662
|
+
if (head === void 0) return chain.label;
|
|
663
|
+
const slot = resolveSlotState(chain, now, tz);
|
|
662
664
|
const model = modelDisplayName !== void 0 && modelDisplayName !== "" ? modelDisplayName : head.model;
|
|
663
|
-
return `${
|
|
665
|
+
return `${chain.label}: ${model}[${slot.label}]`;
|
|
664
666
|
}
|
|
665
667
|
async function resolveHeadDisplayName(llm, head) {
|
|
666
668
|
if (llm === void 0) return head.model;
|
|
@@ -693,16 +695,20 @@ var FallbacksChainAdapter = class extends LlmAdapter {
|
|
|
693
695
|
async listModels(provider) {
|
|
694
696
|
const config = this.readConfig();
|
|
695
697
|
const now = new Date();
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
698
|
+
const tz = config.tz ?? "Asia/Shanghai";
|
|
699
|
+
return Promise.all(config.chains.map(async (chain) => {
|
|
700
|
+
const head = effectiveHeadOf(chain, now, tz);
|
|
701
|
+
const display = head === void 0 ? void 0 : await resolveHeadDisplayName(this.getLlm(), head);
|
|
702
|
+
return {
|
|
703
|
+
provider,
|
|
704
|
+
id: chain.id,
|
|
705
|
+
name: pickerDisplayName(chain, now, display, tz)
|
|
706
|
+
};
|
|
707
|
+
}));
|
|
703
708
|
}
|
|
704
709
|
async resolveModel(provider, model, signal) {
|
|
705
|
-
const
|
|
710
|
+
const chain = this.readConfig().chains.find((entry) => entry.id === model);
|
|
711
|
+
const head = chain === void 0 ? void 0 : effectiveHeadOf(chain, new Date(), this.readConfig().tz ?? "Asia/Shanghai");
|
|
706
712
|
const llm = this.getLlm();
|
|
707
713
|
if (head !== void 0 && llm !== void 0) try {
|
|
708
714
|
const info = await llm.resolveModelInfo(head.provider, head.model, signal);
|
|
@@ -723,18 +729,11 @@ var FallbacksChainAdapter = class extends LlmAdapter {
|
|
|
723
729
|
name: model
|
|
724
730
|
};
|
|
725
731
|
}
|
|
726
|
-
providerRetryPolicy(_provider) {
|
|
732
|
+
providerRetryPolicy(_provider) {}
|
|
733
|
+
imageRequestPricing(_provider, model) {
|
|
727
734
|
try {
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
return this.getLlm()?.providerRetryPolicy(head.provider);
|
|
731
|
-
} catch {
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
imageRequestPricing(_provider, _model) {
|
|
736
|
-
try {
|
|
737
|
-
const head = effectiveHeadOf(this.readConfig(), new Date());
|
|
735
|
+
const chain = this.readConfig().chains.find((entry) => entry.id === model);
|
|
736
|
+
const head = chain === void 0 ? void 0 : effectiveHeadOf(chain, new Date(), this.readConfig().tz ?? "Asia/Shanghai");
|
|
738
737
|
if (head === void 0 || head.provider === "FallbacksChain") return void 0;
|
|
739
738
|
return this.getLlm()?.imageRequestPricing(head.provider, head.model);
|
|
740
739
|
} catch {
|
|
@@ -758,7 +757,7 @@ function installFallbacksAdapter(ctx, readConfig, walk) {
|
|
|
758
757
|
try {
|
|
759
758
|
disposeAdapter = llm.registerAdapter([FALLBACKS_PROVIDER$1], adapter);
|
|
760
759
|
registered = true;
|
|
761
|
-
logger.info("llm-fallbacks: virtual adapter registered — FallbacksChain
|
|
760
|
+
logger.info("llm-fallbacks: virtual adapter registered — FallbacksChain/<chain id> rows are selectable in the model picker");
|
|
762
761
|
} catch (error) {
|
|
763
762
|
if (!(error instanceof Error) || !("code" in error) || error.code !== "DUPLICATE_ADAPTER") throw error;
|
|
764
763
|
logger.debug("llm-fallbacks: virtual adapter already registered — no route on this fiber (multi-fiber dedupe)");
|
|
@@ -767,7 +766,7 @@ function installFallbacksAdapter(ctx, readConfig, walk) {
|
|
|
767
766
|
disposeAdapter?.();
|
|
768
767
|
disposeAdapter = void 0;
|
|
769
768
|
registered = false;
|
|
770
|
-
logger.info("llm-fallbacks: virtual adapter unregistered — FallbacksChain
|
|
769
|
+
logger.info("llm-fallbacks: virtual adapter unregistered — FallbacksChain rows hidden from the model picker");
|
|
771
770
|
}
|
|
772
771
|
};
|
|
773
772
|
ctx.inject(["llm"], (llmCtx) => {
|
|
@@ -786,6 +785,7 @@ function installFallbacksAdapter(ctx, readConfig, walk) {
|
|
|
786
785
|
//#region .build/host/chain-walk.js
|
|
787
786
|
const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
|
|
788
787
|
const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
|
|
788
|
+
const UNKNOWN_CHAIN_CODE = "UNKNOWN_CHAIN";
|
|
789
789
|
const LLM_UNAVAILABLE_CODE = "LLM_UNAVAILABLE";
|
|
790
790
|
const REQUEST_SCOPED_CODES = new Set(["CONTEXT_WINDOW_EXCEEDED"]);
|
|
791
791
|
async function makeModelExists(catalog, providers) {
|
|
@@ -820,6 +820,64 @@ async function makeContextWindowOf(catalog, routes) {
|
|
|
820
820
|
}));
|
|
821
821
|
return (provider, model) => windows.get(selectorKey(provider, model));
|
|
822
822
|
}
|
|
823
|
+
function makeModelInfoOf(catalog) {
|
|
824
|
+
const resolveModelInfo = catalog?.resolveModelInfo?.bind(catalog);
|
|
825
|
+
if (resolveModelInfo === void 0) return () => Promise.resolve(void 0);
|
|
826
|
+
const cache = new Map();
|
|
827
|
+
return (provider, model) => {
|
|
828
|
+
const key = selectorKey(provider, model);
|
|
829
|
+
let info = cache.get(key);
|
|
830
|
+
if (info === void 0) {
|
|
831
|
+
info = (async () => {
|
|
832
|
+
try {
|
|
833
|
+
return await resolveModelInfo(provider, model);
|
|
834
|
+
} catch {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
})();
|
|
838
|
+
cache.set(key, info);
|
|
839
|
+
}
|
|
840
|
+
return info;
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
async function adaptForTarget(options, target, modelInfoOf, debug) {
|
|
844
|
+
if (options.reasoningEffort === void 0 && options.maxTokens === void 0) return options;
|
|
845
|
+
const info = await modelInfoOf(target.provider, target.model);
|
|
846
|
+
if (info === void 0 || info === null) return options;
|
|
847
|
+
let adapted;
|
|
848
|
+
const dropped = [];
|
|
849
|
+
try {
|
|
850
|
+
adapted = { ...options };
|
|
851
|
+
const effort = options.reasoningEffort;
|
|
852
|
+
if (effort !== void 0 && info.reasoning !== void 0 && !(info.reasoning.efforts ?? []).some((supported) => supported.id === effort)) {
|
|
853
|
+
delete adapted.reasoningEffort;
|
|
854
|
+
dropped.push("reasoningEffort");
|
|
855
|
+
}
|
|
856
|
+
const maxTokens = options.maxTokens;
|
|
857
|
+
if (maxTokens !== void 0 && info.defaultMaxTokens !== void 0 && maxTokens > info.defaultMaxTokens) {
|
|
858
|
+
delete adapted.maxTokens;
|
|
859
|
+
dropped.push("maxTokens");
|
|
860
|
+
}
|
|
861
|
+
} catch {
|
|
862
|
+
return options;
|
|
863
|
+
}
|
|
864
|
+
if (dropped.length > 0) try {
|
|
865
|
+
debug?.("llm-fallbacks: parameter adaptation candidate=%s/%s dropped=%s", target.provider, target.model, dropped.join(","));
|
|
866
|
+
} catch {}
|
|
867
|
+
return adapted;
|
|
868
|
+
}
|
|
869
|
+
const MAX_TOKENS_REJECTION = /max_tokens[\s\S]{0,40}?(非法|invalid|range|范围|exceed|too\s+large)/i;
|
|
870
|
+
function repairableControl(failure, options) {
|
|
871
|
+
if (failure.code === "UNSUPPORTED_REASONING_EFFORT") return options.reasoningEffort === void 0 ? void 0 : "reasoningEffort";
|
|
872
|
+
if (failure.code === "INVALID_REQUEST" && options.maxTokens !== void 0 && MAX_TOKENS_REJECTION.test(failure.message)) return "maxTokens";
|
|
873
|
+
}
|
|
874
|
+
const SLOT_MARKER_SEPARATOR = "\0";
|
|
875
|
+
function slotMarkerKey(agentId, chainId) {
|
|
876
|
+
return agentId + SLOT_MARKER_SEPARATOR + chainId;
|
|
877
|
+
}
|
|
878
|
+
function slotMarkerPrefix(agentId) {
|
|
879
|
+
return agentId + SLOT_MARKER_SEPARATOR;
|
|
880
|
+
}
|
|
823
881
|
function envelopeProvenance(replayState) {
|
|
824
882
|
if (typeof replayState !== "object" || replayState === null) return void 0;
|
|
825
883
|
const response = replayState.response;
|
|
@@ -837,7 +895,7 @@ function restoreEnvelopeProvenance(messages) {
|
|
|
837
895
|
const restored = messages.map((message) => {
|
|
838
896
|
const source = message.source;
|
|
839
897
|
if (message.role !== "assistant" || source?.kind !== "model") return message;
|
|
840
|
-
if (source.provider !== "FallbacksChain"
|
|
898
|
+
if (source.provider !== "FallbacksChain") return message;
|
|
841
899
|
const provenance = envelopeProvenance(source.replayState);
|
|
842
900
|
if (provenance === void 0) return message;
|
|
843
901
|
changed = true;
|
|
@@ -851,37 +909,21 @@ function restoreEnvelopeProvenance(messages) {
|
|
|
851
909
|
});
|
|
852
910
|
return changed ? restored : messages;
|
|
853
911
|
}
|
|
854
|
-
function agentLikeOf(view) {
|
|
855
|
-
return {
|
|
856
|
-
options: view?.optionsRoute,
|
|
857
|
-
session: { header: {
|
|
858
|
-
origin: view?.origin ?? "root",
|
|
859
|
-
agentPreset: view?.agentPreset
|
|
860
|
-
} }
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
function servedRouteOf(config, route, now) {
|
|
864
|
-
if (route?.provider !== "FallbacksChain" || route.model !== "Auto") return void 0;
|
|
865
|
-
const head = effectiveHeadOf(config, now);
|
|
866
|
-
return head === void 0 ? void 0 : {
|
|
867
|
-
provider: head.provider,
|
|
868
|
-
model: head.model
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
912
|
function slotWinnerKey(winner) {
|
|
872
913
|
if (winner === "all-day") return "all-day";
|
|
873
914
|
return winner.kind === "preset" ? `preset:${winner.preset}` : `custom:${winner.start}-${winner.end}`;
|
|
874
915
|
}
|
|
875
916
|
function logSlotRotation(deps, start) {
|
|
876
|
-
const { config, now,
|
|
917
|
+
const { config, now, chain, owner } = start;
|
|
877
918
|
const markers = deps.slotWinners;
|
|
878
|
-
if (markers === void 0 || owner === void 0
|
|
879
|
-
if (!isAllDayConforming(
|
|
880
|
-
const slot = resolveSlotState(
|
|
919
|
+
if (markers === void 0 || owner === void 0) return;
|
|
920
|
+
if (!isAllDayConforming(chain.entries) || (chain.timeSlots?.length ?? 0) === 0) return;
|
|
921
|
+
const slot = resolveSlotState(chain, now, config.tz ?? "Asia/Shanghai");
|
|
922
|
+
const markerKey = slotMarkerKey(owner, chain.id);
|
|
881
923
|
const key = slotWinnerKey(slot.winner);
|
|
882
|
-
const previous = markers.get(
|
|
883
|
-
if (previous !== void 0 && previous.key !== key) deps.log.info("llm-fallbacks: agent \"%s\" time-slot switch (分时切换): %s -> %s", owner, previous.label, slot.label);
|
|
884
|
-
markers.set(
|
|
924
|
+
const previous = markers.get(markerKey);
|
|
925
|
+
if (previous !== void 0 && previous.key !== key) deps.log.info("llm-fallbacks: agent \"%s\" time-slot switch (分时切换) on chain \"%s\": %s -> %s", owner, chain.id, previous.label, slot.label);
|
|
926
|
+
markers.set(markerKey, {
|
|
885
927
|
key,
|
|
886
928
|
label: slot.label
|
|
887
929
|
});
|
|
@@ -889,45 +931,33 @@ function logSlotRotation(deps, start) {
|
|
|
889
931
|
function resolveWalk(deps, options) {
|
|
890
932
|
const config = deps.readConfig();
|
|
891
933
|
const now = new Date();
|
|
892
|
-
const view = deps.resolveAgentView(options.sessionId);
|
|
893
934
|
const owner = options.sessionId;
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
servedRoute: servedRouteOf(config, view?.optionsRoute, now),
|
|
898
|
-
warn: deps.log.warn
|
|
899
|
-
});
|
|
900
|
-
const origin = view?.origin ?? "root";
|
|
901
|
-
const rootTail = origin === "subagent" ? config.rootChain : resolveEffectiveChain(config, now, config.tz ?? "Asia/Shanghai");
|
|
902
|
-
const entries = roleChainEntries(config.roles.list, rootTail, role);
|
|
935
|
+
const chain = config.chains.find((entry) => entry.id === options.model);
|
|
936
|
+
if (chain === void 0) throw new LlmError(`llm-fallbacks: unknown virtual chain "${String(options.model)}" — re-select a chain in the model picker`, UNKNOWN_CHAIN_CODE);
|
|
937
|
+
const entries = resolveEffectiveChain(chain, now, config.tz ?? "Asia/Shanghai");
|
|
903
938
|
if (entries.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);
|
|
904
|
-
if (
|
|
905
|
-
const
|
|
906
|
-
if (
|
|
907
|
-
const dispatchable = firstDispatchableExactHead(entries);
|
|
908
|
-
const allowedModels = policy?.state === "enabled" ? policy.allowedModels : void 0;
|
|
909
|
-
const head = allowedModels === void 0 ? dispatchable : firstDispatchableExactHead(entries, allowedModels);
|
|
910
|
-
if (head === void 0) throw new LlmError(dispatchable === void 0 ? "llm-fallbacks: the effective chain head cannot be dispatched (wildcard, malformed, or self-route) — refusing to delegate" : "llm-fallbacks: every dispatchable route of the effective chain is outside the subagent allowlist — refusing to delegate", UNDISPATCHABLE_HEAD_CODE);
|
|
939
|
+
if (!isAllDayConforming(chain.entries)) throw new LlmError("llm-fallbacks: the chain is not conforming (exactly one official model required) — refusing to delegate", UNDISPATCHABLE_HEAD_CODE);
|
|
940
|
+
const head = firstDispatchableExactHead(entries);
|
|
941
|
+
if (head === void 0) throw new LlmError("llm-fallbacks: the effective chain head cannot be dispatched (wildcard, malformed, or self-route) — refusing to delegate", UNDISPATCHABLE_HEAD_CODE);
|
|
911
942
|
const llm = deps.getLlm();
|
|
912
943
|
if (llm === void 0) throw new LlmError("llm-fallbacks: the llm runtime is unavailable — cannot delegate the FallbacksChain call", LLM_UNAVAILABLE_CODE);
|
|
913
944
|
return {
|
|
914
945
|
config,
|
|
915
946
|
now,
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
rootTail,
|
|
947
|
+
chain,
|
|
948
|
+
entries,
|
|
919
949
|
head,
|
|
920
950
|
llm,
|
|
921
|
-
owner
|
|
922
|
-
seam
|
|
951
|
+
owner
|
|
923
952
|
};
|
|
924
953
|
}
|
|
925
954
|
function walkChain(deps, options) {
|
|
926
955
|
return walk(deps, resolveWalk(deps, options), options);
|
|
927
956
|
}
|
|
928
957
|
async function* walk(deps, start, options) {
|
|
929
|
-
const { config,
|
|
958
|
+
const { config, chain, head, llm, owner } = start;
|
|
930
959
|
logSlotRotation(deps, start);
|
|
960
|
+
const modelInfoOf = makeModelInfoOf(llm);
|
|
931
961
|
const messages = restoreEnvelopeProvenance(options.messages);
|
|
932
962
|
const stateOf = () => owner === void 0 ? void 0 : deps.states.peek(owner);
|
|
933
963
|
const failedKeys = new Set();
|
|
@@ -946,12 +976,12 @@ async function* walk(deps, start, options) {
|
|
|
946
976
|
failedKeys.add(selectorKey(failed.provider, failed.model));
|
|
947
977
|
if (!REQUEST_SCOPED_CODES.has(code)) coolRoute(failed);
|
|
948
978
|
};
|
|
949
|
-
const advanceFrom = async (from, failureCode
|
|
950
|
-
const { all, wildcard } = resolveChainViews(
|
|
979
|
+
const advanceFrom = async (from, failureCode) => {
|
|
980
|
+
const { all, wildcard } = resolveChainViews(start.entries, from.provider, from.model, deps.log.warn);
|
|
951
981
|
const own = all.map((candidate) => candidate.provider !== FALLBACKS_PROVIDER$1);
|
|
952
982
|
const candidates = all.filter((_, index) => own[index]);
|
|
953
983
|
const wildcards = wildcard.filter((_, index) => own[index]);
|
|
954
|
-
const exists = hasWildcardEntry(
|
|
984
|
+
const exists = hasWildcardEntry(start.entries) ? await makeModelExists(llm, [...new Set(candidates.map((candidate) => candidate.provider))]) : void 0;
|
|
955
985
|
const contextWindowOf = failureCode === "CONTEXT_WINDOW_EXCEEDED" ? await makeContextWindowOf(llm, [from, ...candidates.flatMap((candidate) => candidate.model === void 0 ? [] : [{
|
|
956
986
|
provider: candidate.provider,
|
|
957
987
|
model: candidate.model
|
|
@@ -974,47 +1004,6 @@ async function* walk(deps, start, options) {
|
|
|
974
1004
|
contextWindowOf
|
|
975
1005
|
});
|
|
976
1006
|
const describeCandidates = (list) => list.map(({ candidate, skip }) => skip === void 0 ? `${candidate.provider}/${candidate.model}` : `${candidate.provider}/${candidate.model} (skipped: ${skip})`);
|
|
977
|
-
if (start.seam?.virtualRouteInjected === true && owner !== void 0) {
|
|
978
|
-
const policy = deps.policyFor(owner);
|
|
979
|
-
if (policy.state === "unprovable") {
|
|
980
|
-
deps.log.warn("llm-fallbacks: agent \"%s\" subagent model-selection policy is on but unreadable — no fallback switch (a cooled-head re-entry is refused)", owner);
|
|
981
|
-
return {
|
|
982
|
-
candidates: describeCandidates(annotated),
|
|
983
|
-
currentAdmitted: false
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
if (policy.state === "enabled") {
|
|
987
|
-
const currentAdmitted = routeOnAllowlist(from, policy.allowedModels);
|
|
988
|
-
const permitted = new Set(surviving.filter((candidate) => candidate.model !== void 0 && routeOnAllowlist({
|
|
989
|
-
provider: candidate.provider,
|
|
990
|
-
model: candidate.model
|
|
991
|
-
}, policy.allowedModels)).map((candidate) => selectorKey(candidate.provider, candidate.model)));
|
|
992
|
-
const labelled = annotated.map((entry) => entry.skip === void 0 && !permitted.has(selectorKey(entry.candidate.provider, entry.candidate.model)) ? {
|
|
993
|
-
candidate: entry.candidate,
|
|
994
|
-
skip: "allowlist"
|
|
995
|
-
} : entry);
|
|
996
|
-
const allowed = firstAllowedCandidate(resolvedRoutes(surviving), policy.allowedModels);
|
|
997
|
-
if (allowed === void 0) {
|
|
998
|
-
const wouldBe = surviving[0];
|
|
999
|
-
if (wouldBe?.model !== void 0) {
|
|
1000
|
-
deps.log.warn("llm-fallbacks: agent \"%s\" fallback switch %s/%s blocked: resolved candidates are all outside the subagent allowlist — no switch", owner, wouldBe.provider, wouldBe.model);
|
|
1001
|
-
if (blockedReason !== void 0) deps.recordBlocked?.(owner, {
|
|
1002
|
-
provider: wouldBe.provider,
|
|
1003
|
-
model: wouldBe.model
|
|
1004
|
-
}, blockedReason);
|
|
1005
|
-
}
|
|
1006
|
-
return {
|
|
1007
|
-
candidates: describeCandidates(labelled),
|
|
1008
|
-
currentAdmitted
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
return {
|
|
1012
|
-
target: allowed,
|
|
1013
|
-
candidates: describeCandidates(labelled),
|
|
1014
|
-
currentAdmitted
|
|
1015
|
-
};
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
1007
|
const first = surviving[0];
|
|
1019
1008
|
if (first?.model === void 0) return { candidates: describeCandidates(annotated) };
|
|
1020
1009
|
return {
|
|
@@ -1026,111 +1015,83 @@ async function* walk(deps, start, options) {
|
|
|
1026
1015
|
};
|
|
1027
1016
|
};
|
|
1028
1017
|
let target;
|
|
1029
|
-
if (isCooled(selectorKey(head.provider, head.model)))
|
|
1030
|
-
|
|
1031
|
-
if (advance?.target !== void 0) target = advance.target;
|
|
1032
|
-
else if (advance?.currentAdmitted === false) throw new LlmError("llm-fallbacks: the subagent model-selection policy no longer admits the chain head — refusing to delegate the FallbacksChain call", UNDISPATCHABLE_HEAD_CODE);
|
|
1033
|
-
else target = head;
|
|
1034
|
-
} else target = head;
|
|
1018
|
+
if (isCooled(selectorKey(head.provider, head.model))) target = (await advanceFrom(head))?.target ?? head;
|
|
1019
|
+
else target = head;
|
|
1035
1020
|
let switches = 0;
|
|
1036
1021
|
for (;;) {
|
|
1037
1022
|
const key = selectorKey(target.provider, target.model);
|
|
1038
1023
|
const state = stateOf();
|
|
1039
|
-
if ((config.recovery ?? "timer") === "half-open" && state !== void 0 && state.recovery.tryMarkProbeLogged(key)) deps.log.info("llm-fallbacks: agent \"%s\" half-open probe %s/%s (
|
|
1024
|
+
if ((config.recovery ?? "timer") === "half-open" && state !== void 0 && state.recovery.tryMarkProbeLogged(key)) deps.log.info("llm-fallbacks: agent \"%s\" half-open probe %s/%s (chain=%s, reason=%s)", owner ?? "", target.provider, target.model, chain.id, "trigger-code");
|
|
1040
1025
|
let committed = false;
|
|
1041
1026
|
let swallowed;
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
if (
|
|
1059
|
-
|
|
1027
|
+
let attemptOptions = await adaptForTarget(options, target, modelInfoOf, deps.log.debug);
|
|
1028
|
+
const repaired = new Set();
|
|
1029
|
+
for (;;) {
|
|
1030
|
+
committed = false;
|
|
1031
|
+
swallowed = void 0;
|
|
1032
|
+
let repair;
|
|
1033
|
+
const pendingUsage = [];
|
|
1034
|
+
for await (const chunk of llm.stream({
|
|
1035
|
+
...attemptOptions,
|
|
1036
|
+
provider: target.provider,
|
|
1037
|
+
model: target.model,
|
|
1038
|
+
messages
|
|
1039
|
+
})) {
|
|
1040
|
+
const failure = chunk.type === "finish" && chunk.reason.kind === "error" ? chunk.reason.failure : void 0;
|
|
1041
|
+
if (failure !== void 0 && !committed) {
|
|
1042
|
+
const repairable = repairableControl(failure, attemptOptions);
|
|
1043
|
+
if (repairable !== void 0 && !repaired.has(repairable)) {
|
|
1044
|
+
repair = repairable;
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
if (config.triggerCodes.includes(failure.code)) {
|
|
1048
|
+
swallowed = {
|
|
1049
|
+
chunk,
|
|
1050
|
+
code: failure.code
|
|
1051
|
+
};
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (chunk.type === "finish") {
|
|
1056
|
+
if (failure === void 0) {
|
|
1057
|
+
if (chunk.reason.kind !== "aborted" && state !== void 0) deps.states.observeSuccess(state, key);
|
|
1058
|
+
} else if (config.triggerCodes.includes(failure.code)) bookFailure(target, failure.code);
|
|
1059
|
+
for (const pending of pendingUsage) yield pending;
|
|
1060
|
+
pendingUsage.length = 0;
|
|
1061
|
+
yield chunk;
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (chunk.type === "usage" && !committed) {
|
|
1065
|
+
pendingUsage.push(chunk);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
for (const pending of pendingUsage) yield pending;
|
|
1069
|
+
pendingUsage.length = 0;
|
|
1060
1070
|
yield chunk;
|
|
1061
|
-
|
|
1071
|
+
if (chunk.type !== "usage") committed = true;
|
|
1072
|
+
}
|
|
1073
|
+
if (repair === void 0) {
|
|
1074
|
+
for (const pending of pendingUsage) yield pending;
|
|
1075
|
+
break;
|
|
1062
1076
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1077
|
+
repaired.add(repair);
|
|
1078
|
+
attemptOptions = { ...attemptOptions };
|
|
1079
|
+
delete attemptOptions[repair];
|
|
1080
|
+
deps.log.info("llm-fallbacks: agent \"%s\" parameter repair (参数修复) candidate=%s/%s dropped=%s", owner ?? "", target.provider, target.model, repair);
|
|
1065
1081
|
}
|
|
1066
1082
|
if (swallowed === void 0) return;
|
|
1067
1083
|
bookFailure(target, swallowed.code);
|
|
1068
|
-
const next = switches < config.maxSwitchesPerStep ? await advanceFrom(target, swallowed.code
|
|
1084
|
+
const next = switches < config.maxSwitchesPerStep ? await advanceFrom(target, swallowed.code) : void 0;
|
|
1069
1085
|
if (next === void 0 || next.target === void 0) {
|
|
1070
1086
|
yield swallowed.chunk;
|
|
1071
1087
|
return;
|
|
1072
1088
|
}
|
|
1073
1089
|
switches += 1;
|
|
1074
|
-
deps.log.info("llm-fallbacks: agent \"%s\" fallback switch (降级切换) %s/%s -> %s/%s (
|
|
1090
|
+
deps.log.info("llm-fallbacks: agent \"%s\" fallback switch (降级切换) %s/%s -> %s/%s (chain=%s, reason=%s, candidates=%o)", owner ?? "", target.provider, target.model, next.target.provider, next.target.model, chain.id, "trigger-code", next.candidates);
|
|
1075
1091
|
target = next.target;
|
|
1076
1092
|
}
|
|
1077
1093
|
}
|
|
1078
1094
|
//#endregion
|
|
1079
|
-
//#region .build/host/subagent-policy.js
|
|
1080
|
-
function parseRoutes(value) {
|
|
1081
|
-
if (!Array.isArray(value)) return void 0;
|
|
1082
|
-
const routes = [];
|
|
1083
|
-
for (const candidate of value) {
|
|
1084
|
-
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate) || !("provider" in candidate) || typeof candidate.provider !== "string" || candidate.provider.length === 0 || !("model" in candidate) || typeof candidate.model !== "string" || candidate.model.length === 0) return;
|
|
1085
|
-
routes.push({
|
|
1086
|
-
provider: candidate.provider,
|
|
1087
|
-
model: candidate.model
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
return routes;
|
|
1091
|
-
}
|
|
1092
|
-
function policySettingsFrom(value) {
|
|
1093
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
1094
|
-
if (!("enabled" in value) || typeof value.enabled !== "boolean") return void 0;
|
|
1095
|
-
const routes = "allowedModels" in value ? parseRoutes(value.allowedModels) : void 0;
|
|
1096
|
-
if (routes === void 0) return void 0;
|
|
1097
|
-
return {
|
|
1098
|
-
enabled: value.enabled,
|
|
1099
|
-
allowedModels: routes
|
|
1100
|
-
};
|
|
1101
|
-
}
|
|
1102
|
-
function readSessionPolicyEvent(events) {
|
|
1103
|
-
const event = events.find((candidate) => candidate.type === "subagent/model-selection-policy");
|
|
1104
|
-
if (event === void 0) return {
|
|
1105
|
-
ok: false,
|
|
1106
|
-
present: false
|
|
1107
|
-
};
|
|
1108
|
-
const data = event.data;
|
|
1109
|
-
const routes = parseRoutes(typeof data === "object" && data !== null && "allowedModels" in data ? data.allowedModels : void 0);
|
|
1110
|
-
if (routes === void 0 || routes.length === 0) return {
|
|
1111
|
-
ok: false,
|
|
1112
|
-
present: true
|
|
1113
|
-
};
|
|
1114
|
-
return {
|
|
1115
|
-
ok: true,
|
|
1116
|
-
allowedModels: routes
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
function effectivePolicy(event, settings) {
|
|
1120
|
-
if (event.ok) return {
|
|
1121
|
-
state: "enabled",
|
|
1122
|
-
allowedModels: event.allowedModels
|
|
1123
|
-
};
|
|
1124
|
-
if (event.present) return { state: "unprovable" };
|
|
1125
|
-
if (settings === void 0 || settings.enabled !== true) return { state: "disabled" };
|
|
1126
|
-
const routes = parseRoutes(settings.allowedModels);
|
|
1127
|
-
if (routes === void 0 || routes.length === 0) return { state: "unprovable" };
|
|
1128
|
-
return {
|
|
1129
|
-
state: "enabled",
|
|
1130
|
-
allowedModels: routes
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
//#endregion
|
|
1134
1095
|
//#region .build/host/role-notice.js
|
|
1135
1096
|
const ROLE_NOTICE_PLUGIN = "dsh-llm-fallbacks";
|
|
1136
1097
|
const ROLE_NOTICE_SOURCE_KIND = "llm-fallbacks-role-notice";
|
|
@@ -1346,7 +1307,7 @@ function declaredExecuteAsValues(prompt) {
|
|
|
1346
1307
|
function canonicalizeRoleReference(raw) {
|
|
1347
1308
|
const trimmed = raw.trim();
|
|
1348
1309
|
const id = (trimmed.startsWith("@") ? trimmed.slice(1) : trimmed).trim();
|
|
1349
|
-
if (id === ""
|
|
1310
|
+
if (id === "") return void 0;
|
|
1350
1311
|
return id;
|
|
1351
1312
|
}
|
|
1352
1313
|
function lookupDeclaredRole(id, roleIds) {
|
|
@@ -1444,48 +1405,32 @@ function installSubagentSeam(ctx, options) {
|
|
|
1444
1405
|
role,
|
|
1445
1406
|
at,
|
|
1446
1407
|
firstNoticePending: existing?.firstNoticePending ?? true,
|
|
1447
|
-
...existing?.personaNotApplied === true || verdict.personaMissed === true ? { personaNotApplied: true } : {}
|
|
1448
|
-
...existing?.virtualRouteInjected === true || verdict.virtualRouteInjected === true ? { virtualRouteInjected: true } : {}
|
|
1408
|
+
...existing?.personaNotApplied === true || verdict.personaMissed === true ? { personaNotApplied: true } : {}
|
|
1449
1409
|
});
|
|
1450
1410
|
};
|
|
1451
|
-
const
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
return typeof provider === "string" && provider !== "" && typeof model === "string" && model !== "" ? {
|
|
1455
|
-
provider,
|
|
1456
|
-
model
|
|
1457
|
-
} : void 0;
|
|
1411
|
+
const hasExplicitAgentOptions = (request) => {
|
|
1412
|
+
const agentOptions = request?.agentOptions;
|
|
1413
|
+
return typeof agentOptions?.provider === "string" || typeof agentOptions?.model === "string";
|
|
1458
1414
|
};
|
|
1459
|
-
const
|
|
1415
|
+
const injectRoleChain = (spec, role) => {
|
|
1460
1416
|
try {
|
|
1461
1417
|
if (options.source === void 0) return void 0;
|
|
1462
|
-
|
|
1463
|
-
if (typeof agentOptions?.provider === "string" || typeof agentOptions?.model === "string") return void 0;
|
|
1418
|
+
if (hasExplicitAgentOptions(spec.request)) return void 0;
|
|
1464
1419
|
const config = options.source();
|
|
1465
1420
|
if (!isFallbackActive(config)) return void 0;
|
|
1466
|
-
const
|
|
1467
|
-
if (
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
}
|
|
1471
|
-
const chain = roleChainEntries(config.roles.list, config.rootChain, role);
|
|
1472
|
-
const allowedModels = policy?.state === "enabled" ? policy.allowedModels : void 0;
|
|
1473
|
-
const dispatchable = firstDispatchableExactHead(chain);
|
|
1474
|
-
const head = allowedModels === void 0 ? dispatchable : firstDispatchableExactHead(chain, allowedModels);
|
|
1475
|
-
if (head === void 0) {
|
|
1476
|
-
if (allowedModels !== void 0 && dispatchable !== void 0) debug(`llm-fallbacks: subagent seam — no route of role "${role}" is on the subagent allowlist; not mounting the virtual chain route`);
|
|
1421
|
+
const chainId = config.roles.list.find((entry) => entry.id === role)?.chain;
|
|
1422
|
+
if (chainId === void 0 || chainId === "") return void 0;
|
|
1423
|
+
if (!config.chains.some((chain) => chain.id === chainId)) {
|
|
1424
|
+
debug(`llm-fallbacks: subagent seam — role "${role}" names undeclared chain "${chainId}"; the child inherits the delegating agent's route`);
|
|
1477
1425
|
return;
|
|
1478
1426
|
}
|
|
1479
1427
|
return {
|
|
1480
|
-
request
|
|
1481
|
-
|
|
1482
|
-
agentOptions
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
}
|
|
1487
|
-
},
|
|
1488
|
-
head
|
|
1428
|
+
...spec.request,
|
|
1429
|
+
agentOptions: {
|
|
1430
|
+
...spec.request?.agentOptions,
|
|
1431
|
+
provider: FALLBACKS_PROVIDER$1,
|
|
1432
|
+
model: chainId
|
|
1433
|
+
}
|
|
1489
1434
|
};
|
|
1490
1435
|
} catch (error) {
|
|
1491
1436
|
reportDegrade(error);
|
|
@@ -1511,7 +1456,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1511
1456
|
const endCorrelation = (key) => {
|
|
1512
1457
|
if (key !== void 0) pending.delete(key);
|
|
1513
1458
|
};
|
|
1514
|
-
const
|
|
1459
|
+
const resolveSeamRole = (request) => {
|
|
1515
1460
|
try {
|
|
1516
1461
|
const prompt = promptTextOf(request?.prompt);
|
|
1517
1462
|
if (prompt.trim() === "") return void 0;
|
|
@@ -1577,7 +1522,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1577
1522
|
if (cached !== void 0) return cached;
|
|
1578
1523
|
const wrapper = Object.create(value);
|
|
1579
1524
|
wrapper.startActivation = (spec) => {
|
|
1580
|
-
const role =
|
|
1525
|
+
const role = resolveSeamRole(spec?.request) ?? (typeof spec?.childId === "string" ? records.get(spec.childId)?.role : void 0);
|
|
1581
1526
|
if (role === void 0) return service.startActivation(spec);
|
|
1582
1527
|
const at = Date.now();
|
|
1583
1528
|
let effectiveSpec = spec;
|
|
@@ -1586,26 +1531,16 @@ function installSubagentSeam(ctx, options) {
|
|
|
1586
1531
|
try {
|
|
1587
1532
|
if (isLocalBackend(service, spec.provider)) {
|
|
1588
1533
|
reservedId = spec.childId ?? randomUUID();
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
effectiveSpec = injected === void 0 ? reservedId === spec.childId ? spec : {
|
|
1534
|
+
const stamped = spec.childId !== void 0 && records.has(spec.childId) ? void 0 : injectRoleChain(spec, role);
|
|
1535
|
+
effectiveSpec = stamped === void 0 ? reservedId === spec.childId ? spec : {
|
|
1592
1536
|
...spec,
|
|
1593
1537
|
childId: reservedId
|
|
1594
1538
|
} : {
|
|
1595
1539
|
...spec,
|
|
1596
1540
|
childId: reservedId,
|
|
1597
|
-
request:
|
|
1541
|
+
request: stamped
|
|
1598
1542
|
};
|
|
1599
|
-
recordChild(reservedId, role, at
|
|
1600
|
-
const head = injected?.head ?? authorized;
|
|
1601
|
-
if (head !== void 0) options.chainHeads?.set(reservedId, {
|
|
1602
|
-
at,
|
|
1603
|
-
route: {
|
|
1604
|
-
provider: head.provider,
|
|
1605
|
-
model: head.model
|
|
1606
|
-
},
|
|
1607
|
-
source: injected === void 0 ? "authorized" : "injected"
|
|
1608
|
-
});
|
|
1543
|
+
recordChild(reservedId, role, at);
|
|
1609
1544
|
}
|
|
1610
1545
|
} catch (error) {
|
|
1611
1546
|
if (reservedId !== void 0) records.delete(reservedId);
|
|
@@ -1906,28 +1841,29 @@ var FallbackStateStore = class {
|
|
|
1906
1841
|
//#endregion
|
|
1907
1842
|
//#region .build/host/gateway.js
|
|
1908
1843
|
const FALLBACKS_PROFILE_ENTRY = "llm-fallbacks";
|
|
1909
|
-
const SUBAGENT_POLICY_SETTINGS_NS = "subagent-model-selection-settings";
|
|
1910
|
-
const VIRTUAL_ALL_ROUTE = {
|
|
1911
|
-
provider: "FallbacksChain",
|
|
1912
|
-
model: "Auto"
|
|
1913
|
-
};
|
|
1914
1844
|
const CONFIG_KEYS = {
|
|
1915
1845
|
triggerCodes: true,
|
|
1916
|
-
|
|
1846
|
+
chains: true,
|
|
1917
1847
|
roles: true,
|
|
1918
1848
|
cooldownMs: true,
|
|
1919
1849
|
revertPolicy: true,
|
|
1920
1850
|
maxSwitchesPerStep: true,
|
|
1921
1851
|
presets: true,
|
|
1922
|
-
timeSlots: true,
|
|
1923
1852
|
tz: true,
|
|
1924
1853
|
recovery: true
|
|
1925
1854
|
};
|
|
1926
|
-
const ROLES_KEYS = {
|
|
1927
|
-
|
|
1928
|
-
|
|
1855
|
+
const ROLES_KEYS = { list: true };
|
|
1856
|
+
const LEGACY_KEYS = new Set([
|
|
1857
|
+
"enabled",
|
|
1858
|
+
"rootChain",
|
|
1859
|
+
"timeSlots"
|
|
1860
|
+
]);
|
|
1861
|
+
const CHAIN_KEYS = {
|
|
1862
|
+
id: true,
|
|
1863
|
+
label: true,
|
|
1864
|
+
entries: true,
|
|
1865
|
+
timeSlots: true
|
|
1929
1866
|
};
|
|
1930
|
-
const LEGACY_KEYS = new Set(["enabled"]);
|
|
1931
1867
|
const SLOT_ROW_KEYS = {
|
|
1932
1868
|
kind: true,
|
|
1933
1869
|
preset: true,
|
|
@@ -1942,12 +1878,10 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1942
1878
|
bridge;
|
|
1943
1879
|
seeds;
|
|
1944
1880
|
settings;
|
|
1945
|
-
|
|
1946
|
-
constructor(ctx, bridge, seeds, subagentPolicy) {
|
|
1881
|
+
constructor(ctx, bridge, seeds) {
|
|
1947
1882
|
super(ctx, "fallbacks");
|
|
1948
1883
|
this.bridge = bridge;
|
|
1949
1884
|
this.seeds = seeds;
|
|
1950
|
-
this.subagentPolicy = subagentPolicy;
|
|
1951
1885
|
ctx.inject(["settings"], (sctx) => {
|
|
1952
1886
|
this.settings = sctx.settings;
|
|
1953
1887
|
return () => {
|
|
@@ -1961,11 +1895,10 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1961
1895
|
async set(patch) {
|
|
1962
1896
|
validateConfigPatch(patch);
|
|
1963
1897
|
if (Object.keys(patch).length === 0) return this.readResult();
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
await settings.update(FALLBACKS_PROFILE_ENTRY, normalized);
|
|
1898
|
+
if (this.settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
1899
|
+
const normalized = stripLegacyForWrite(Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== null)));
|
|
1900
|
+
if (Object.keys(normalized).length === 0 && Object.keys(patch).length === 0) return this.readResult();
|
|
1901
|
+
await this.writeSection(normalized);
|
|
1969
1902
|
return this.readResult();
|
|
1970
1903
|
}
|
|
1971
1904
|
async reset() {
|
|
@@ -1982,40 +1915,23 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1982
1915
|
outcome
|
|
1983
1916
|
};
|
|
1984
1917
|
}
|
|
1985
|
-
async syncAllowlist() {
|
|
1986
|
-
const snapshot = this.subagentPolicy;
|
|
1987
|
-
if (snapshot === void 0) throw new Error("fallbacks: subagent policy is unavailable — allowlist cannot be synced");
|
|
1988
|
-
const { policy } = snapshot();
|
|
1989
|
-
if (policy.state !== "enabled") throw new Error(`fallbacks: subagent policy is ${policy.state} — allowlist cannot be synced`);
|
|
1990
|
-
const settings = this.settings;
|
|
1991
|
-
if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
1992
|
-
const merged = mergedAllowlist(policy.allowedModels, this.bridge.source());
|
|
1993
|
-
await settings.mutate(SUBAGENT_POLICY_SETTINGS_NS, [{
|
|
1994
|
-
op: "set",
|
|
1995
|
-
path: ["allowedModels"],
|
|
1996
|
-
value: merged
|
|
1997
|
-
}]);
|
|
1998
|
-
return this.readResult();
|
|
1999
|
-
}
|
|
2000
1918
|
readConfig(source = this.bridge.source()) {
|
|
1919
|
+
const migrated = migrateLegacyConfig(source);
|
|
2001
1920
|
const wire = {};
|
|
2002
1921
|
for (const key of Object.keys(CONFIG_KEYS)) {
|
|
2003
|
-
const value =
|
|
1922
|
+
const value = migrated[key];
|
|
2004
1923
|
if (value === void 0) continue;
|
|
2005
1924
|
wire[key] = key === "roles" ? normalizeRoles(value) : value;
|
|
2006
1925
|
}
|
|
2007
1926
|
return wire;
|
|
2008
1927
|
}
|
|
2009
1928
|
readResult() {
|
|
2010
|
-
const
|
|
2011
|
-
|
|
2012
|
-
config: this.readConfig(source),
|
|
2013
|
-
legacyKeys: detectLegacyKeys(
|
|
1929
|
+
const raw = this.bridge.rawSource?.() ?? this.bridge.source();
|
|
1930
|
+
return {
|
|
1931
|
+
config: this.readConfig(this.bridge.source()),
|
|
1932
|
+
legacyKeys: detectLegacyKeys(raw),
|
|
2014
1933
|
seeds: this.seeds.wireStatus(this.seedsIo())
|
|
2015
1934
|
};
|
|
2016
|
-
const projected = projectSubagentPolicy(this.subagentPolicy);
|
|
2017
|
-
if (projected !== void 0) result.subagentPolicy = projected;
|
|
2018
|
-
return result;
|
|
2019
1935
|
}
|
|
2020
1936
|
seedsIo() {
|
|
2021
1937
|
const settings = this.settings;
|
|
@@ -2023,83 +1939,100 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
2023
1939
|
read: () => this.bridge.source(),
|
|
2024
1940
|
writeRoles: (roles) => {
|
|
2025
1941
|
if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
2026
|
-
return
|
|
1942
|
+
return this.writeSection(stripLegacyForWrite({ roles }));
|
|
2027
1943
|
}
|
|
2028
1944
|
};
|
|
2029
1945
|
}
|
|
1946
|
+
async writeSection(patch) {
|
|
1947
|
+
const settings = this.settings;
|
|
1948
|
+
if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
1949
|
+
await writeFallbacksSection(settings, patch);
|
|
1950
|
+
}
|
|
2030
1951
|
};
|
|
2031
|
-
function
|
|
2032
|
-
const
|
|
2033
|
-
const
|
|
2034
|
-
const
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
1952
|
+
async function writeFallbacksSection(settings, patch, ns = FALLBACKS_PROFILE_ENTRY) {
|
|
1953
|
+
const stored = storedSection(settings, ns);
|
|
1954
|
+
const cleaned = stripLegacyForWrite(patch);
|
|
1955
|
+
const patchHasRoles = cleaned["roles"] !== void 0;
|
|
1956
|
+
const removals = legacySectionRemovals(stored).filter((op) => !(patchHasRoles && op.path[0] === "roles"));
|
|
1957
|
+
const ops = [];
|
|
1958
|
+
const storedChains = stored?.["chains"];
|
|
1959
|
+
const storedHasUsableChains = Array.isArray(storedChains) && storedChains.length > 0;
|
|
1960
|
+
if (cleaned["chains"] === void 0 && !storedHasUsableChains && stored !== void 0 && (Object.hasOwn(stored, "rootChain") || Object.hasOwn(stored, "timeSlots"))) {
|
|
1961
|
+
const migrated = migrateLegacyConfig({
|
|
1962
|
+
...stored,
|
|
1963
|
+
...cleaned
|
|
2041
1964
|
});
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
let parsed;
|
|
2047
|
-
try {
|
|
2048
|
-
parsed = parseSelector(entry);
|
|
2049
|
-
} catch {
|
|
2050
|
-
continue;
|
|
2051
|
-
}
|
|
2052
|
-
if (parsed.model === void 0) continue;
|
|
2053
|
-
push({
|
|
2054
|
-
provider: parsed.provider,
|
|
2055
|
-
model: parsed.model
|
|
1965
|
+
if (migrated.chains.length > 0) ops.push({
|
|
1966
|
+
op: "set",
|
|
1967
|
+
path: ["chains"],
|
|
1968
|
+
value: migrated.chains
|
|
2056
1969
|
});
|
|
2057
1970
|
}
|
|
2058
|
-
push(
|
|
2059
|
-
|
|
1971
|
+
for (const [key, value] of Object.entries(cleaned)) ops.push({
|
|
1972
|
+
op: "set",
|
|
1973
|
+
path: [key],
|
|
1974
|
+
value
|
|
1975
|
+
});
|
|
1976
|
+
ops.push(...removals);
|
|
1977
|
+
if (ops.length > 0) await settings.mutate(ns, ops);
|
|
1978
|
+
}
|
|
1979
|
+
function storedSection(settings, ns) {
|
|
1980
|
+
const user = settings.describe().find((candidate) => candidate.ns === ns)?.user;
|
|
1981
|
+
if (typeof user !== "object" || user === null || Array.isArray(user)) return void 0;
|
|
1982
|
+
return user;
|
|
1983
|
+
}
|
|
1984
|
+
function legacySectionRemovals(section) {
|
|
1985
|
+
if (section === void 0) return [];
|
|
1986
|
+
const removals = [];
|
|
1987
|
+
for (const key of LEGACY_KEYS) if (Object.hasOwn(section, key)) removals.push({
|
|
1988
|
+
op: "unset",
|
|
1989
|
+
path: [key]
|
|
1990
|
+
});
|
|
1991
|
+
const roles = section["roles"];
|
|
1992
|
+
if (typeof roles !== "object" || roles === null || Array.isArray(roles)) return removals;
|
|
1993
|
+
const declared = roles;
|
|
1994
|
+
for (const key of ["rules", "default"]) if (Object.hasOwn(declared, key)) removals.push({
|
|
1995
|
+
op: "unset",
|
|
1996
|
+
path: ["roles", key]
|
|
1997
|
+
});
|
|
1998
|
+
if (!Array.isArray(declared["list"])) return removals;
|
|
1999
|
+
if (declared["list"].some((row) => typeof row === "object" && row !== null && !Array.isArray(row) && (Object.hasOwn(row, "fallback") || Array.isArray(row["chain"])))) {
|
|
2000
|
+
const cleanedRoles = normalizeRoles({ list: declared["list"] });
|
|
2001
|
+
removals.push({
|
|
2002
|
+
op: "set",
|
|
2003
|
+
path: ["roles", "list"],
|
|
2004
|
+
value: cleanedRoles["list"] ?? []
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
return removals;
|
|
2060
2008
|
}
|
|
2061
|
-
function
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
state: "enabled",
|
|
2074
|
-
allowedModels: policy.allowedModels.map((route) => ({
|
|
2075
|
-
provider: route.provider,
|
|
2076
|
-
model: route.model
|
|
2077
|
-
}))
|
|
2078
|
-
};
|
|
2079
|
-
if (head !== void 0) wire.head = {
|
|
2080
|
-
route: {
|
|
2081
|
-
provider: head.route.provider,
|
|
2082
|
-
model: head.route.model
|
|
2083
|
-
},
|
|
2084
|
-
source: head.source
|
|
2085
|
-
};
|
|
2086
|
-
if (blockedAttempt !== void 0) wire.blockedAttempt = {
|
|
2087
|
-
at: blockedAttempt.at,
|
|
2088
|
-
route: {
|
|
2089
|
-
provider: blockedAttempt.route.provider,
|
|
2090
|
-
model: blockedAttempt.route.model
|
|
2091
|
-
},
|
|
2092
|
-
reason: blockedAttempt.reason
|
|
2093
|
-
};
|
|
2094
|
-
return wire;
|
|
2009
|
+
function stripLegacyForWrite(value) {
|
|
2010
|
+
const cleaned = {};
|
|
2011
|
+
for (const [key, member] of Object.entries(value)) {
|
|
2012
|
+
if (LEGACY_KEYS.has(key)) continue;
|
|
2013
|
+
if (key === "roles") {
|
|
2014
|
+
const roles = normalizeRoles(member);
|
|
2015
|
+
if (roles !== void 0) cleaned[key] = roles;
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
cleaned[key] = member;
|
|
2019
|
+
}
|
|
2020
|
+
return cleaned;
|
|
2095
2021
|
}
|
|
2096
2022
|
function normalizeRoles(value) {
|
|
2097
2023
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
2098
2024
|
const roles = {};
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
if (
|
|
2102
|
-
|
|
2025
|
+
const list = value["list"];
|
|
2026
|
+
if (Array.isArray(list)) roles["list"] = list.map((item) => {
|
|
2027
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) return item;
|
|
2028
|
+
const row = {};
|
|
2029
|
+
for (const [key, member] of Object.entries(item)) {
|
|
2030
|
+
if (key === "fallback") continue;
|
|
2031
|
+
if (key === "chain" && Array.isArray(member)) continue;
|
|
2032
|
+
row[key] = member;
|
|
2033
|
+
}
|
|
2034
|
+
return row;
|
|
2035
|
+
});
|
|
2103
2036
|
return roles;
|
|
2104
2037
|
}
|
|
2105
2038
|
function validateConfigPatch(patch) {
|
|
@@ -2112,17 +2045,28 @@ function validateConfigPatch(patch) {
|
|
|
2112
2045
|
for (const nestedKey of Object.keys(roles)) if (!Object.hasOwn(ROLES_KEYS, nestedKey)) throw new Error(`dsh-llm-fallbacks: unknown config key "roles.${nestedKey}"`);
|
|
2113
2046
|
}
|
|
2114
2047
|
}
|
|
2115
|
-
if (key === "
|
|
2116
|
-
const rows = patch[key];
|
|
2117
|
-
if (rows !== null && rows !== void 0) validateTimeSlotsPatch(rows);
|
|
2118
|
-
}
|
|
2119
|
-
if (key === "rootChain") {
|
|
2120
|
-
const chain = patch[key];
|
|
2121
|
-
if (chain !== null && chain !== void 0 && (!Array.isArray(chain) || !isAllDayConforming(chain))) throw new Error("dsh-llm-fallbacks: rootChain must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro)");
|
|
2122
|
-
}
|
|
2048
|
+
if (key === "chains") validateChainsPatch(patch[key]);
|
|
2123
2049
|
}
|
|
2124
2050
|
Config(patch);
|
|
2125
2051
|
}
|
|
2052
|
+
function validateChainsPatch(chains) {
|
|
2053
|
+
if (!Array.isArray(chains)) throw new Error("dsh-llm-fallbacks: chains must be an array of named chains");
|
|
2054
|
+
const seenIds = new Set();
|
|
2055
|
+
chains.forEach((chain, index) => {
|
|
2056
|
+
const at = `chains[${index}]`;
|
|
2057
|
+
if (chain === null || typeof chain !== "object" || Array.isArray(chain)) throw new Error(`dsh-llm-fallbacks: ${at} must be a plain object`);
|
|
2058
|
+
const record = chain;
|
|
2059
|
+
for (const nestedKey of Object.keys(record)) if (!Object.hasOwn(CHAIN_KEYS, nestedKey)) throw new Error(`dsh-llm-fallbacks: unknown config key "${at}.${nestedKey}"`);
|
|
2060
|
+
if (typeof record.id !== "string" || !CHAIN_ID_PATTERN.test(record.id)) throw new Error(`dsh-llm-fallbacks: ${at}.id must match /^[A-Za-z0-9][A-Za-z0-9-]{0,31}$/ (got ${JSON.stringify(record.id)})`);
|
|
2061
|
+
if (seenIds.has(record.id)) throw new Error(`dsh-llm-fallbacks: ${at} duplicates chain id "${record.id}" — chain ids must be unique (case-sensitive)`);
|
|
2062
|
+
seenIds.add(record.id);
|
|
2063
|
+
if (typeof record.label !== "string") throw new Error(`dsh-llm-fallbacks: ${at}.label must be a string`);
|
|
2064
|
+
const entries = record.entries;
|
|
2065
|
+
if (entries !== void 0 && entries !== null && (!Array.isArray(entries) || entries.some((entry) => typeof entry !== "string"))) throw new Error(`dsh-llm-fallbacks: ${at}.entries must be a string array`);
|
|
2066
|
+
if (Array.isArray(entries) && entries.length > 0 && !isAllDayConforming(entries)) throw new Error(`dsh-llm-fallbacks: ${at}.entries must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro)`);
|
|
2067
|
+
if (record.timeSlots !== void 0 && record.timeSlots !== null) validateTimeSlotsPatch(record.timeSlots);
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2126
2070
|
function validateTimeSlotsPatch(rows) {
|
|
2127
2071
|
if (!Array.isArray(rows)) throw new Error("dsh-llm-fallbacks: timeSlots must be an array of slot rows");
|
|
2128
2072
|
const seenPresets = new Set();
|
|
@@ -2207,16 +2151,6 @@ function fallbacksTypertContribution() {
|
|
|
2207
2151
|
codec: { mode: "src-json" }
|
|
2208
2152
|
}],
|
|
2209
2153
|
result: { mode: "src-json" }
|
|
2210
|
-
},
|
|
2211
|
-
{
|
|
2212
|
-
id: "dsh-llm-fallbacks#fallbacks/sync-allowlist",
|
|
2213
|
-
service: "fallbacks",
|
|
2214
|
-
namespace: "fallbacks",
|
|
2215
|
-
method: "sync-allowlist",
|
|
2216
|
-
implementation: "syncAllowlist",
|
|
2217
|
-
invocation: { kind: "direct" },
|
|
2218
|
-
parameters: [],
|
|
2219
|
-
result: { mode: "src-json" }
|
|
2220
2154
|
}
|
|
2221
2155
|
]
|
|
2222
2156
|
};
|
|
@@ -2241,7 +2175,6 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
2241
2175
|
origin: "会话来源",
|
|
2242
2176
|
role: "角色",
|
|
2243
2177
|
chain: "链",
|
|
2244
|
-
inheritRoot: "(inherit-root)",
|
|
2245
2178
|
chainNone: "未配置",
|
|
2246
2179
|
slot: "分时",
|
|
2247
2180
|
switches: "最近降级切换",
|
|
@@ -2260,16 +2193,16 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
2260
2193
|
configEnabled: "已启用",
|
|
2261
2194
|
configDisabled: "未启用",
|
|
2262
2195
|
configTriggerCodes: "触发码",
|
|
2263
|
-
|
|
2196
|
+
configChains: "虚拟链",
|
|
2264
2197
|
configEmpty: "(空)",
|
|
2265
|
-
|
|
2198
|
+
configChainItem: "{id}({label}): {entries}|分时槽: {slots}",
|
|
2266
2199
|
configSlotPresetItem: "{preset}(chain: {n}, window {window})",
|
|
2267
2200
|
configSlotPresetBare: "{preset}(chain: {n})",
|
|
2268
2201
|
configSlotCustomItem: "custom {start}-{end}(chain: {n})",
|
|
2269
2202
|
configTz: "时区",
|
|
2270
2203
|
configRoles: "角色",
|
|
2271
|
-
configRoleItem: "{id}
|
|
2272
|
-
|
|
2204
|
+
configRoleItem: "{id}(指定降级链: {chain})",
|
|
2205
|
+
configRoleUnspecified: "未指定(沿用 dsh 默认)",
|
|
2273
2206
|
configCooldown: "冷却",
|
|
2274
2207
|
configRevert: "回主策略",
|
|
2275
2208
|
configMaxSwitches: "单次请求行走最大切换",
|
|
@@ -2294,7 +2227,6 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
2294
2227
|
origin: "Session origin",
|
|
2295
2228
|
role: "Role",
|
|
2296
2229
|
chain: "Chain",
|
|
2297
|
-
inheritRoot: " (inherit-root)",
|
|
2298
2230
|
chainNone: "not configured",
|
|
2299
2231
|
slot: "Time slot",
|
|
2300
2232
|
switches: "Recent fallback switches",
|
|
@@ -2313,16 +2245,16 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
2313
2245
|
configEnabled: "enabled",
|
|
2314
2246
|
configDisabled: "disabled",
|
|
2315
2247
|
configTriggerCodes: "Trigger codes",
|
|
2316
|
-
|
|
2248
|
+
configChains: "Chains",
|
|
2317
2249
|
configEmpty: "(empty)",
|
|
2318
|
-
|
|
2250
|
+
configChainItem: "{id} ({label}): {entries} | slots: {slots}",
|
|
2319
2251
|
configSlotPresetItem: "{preset} (chain: {n}, window {window})",
|
|
2320
2252
|
configSlotPresetBare: "{preset} (chain: {n})",
|
|
2321
2253
|
configSlotCustomItem: "custom {start}-{end} (chain: {n})",
|
|
2322
2254
|
configTz: "TZ",
|
|
2323
2255
|
configRoles: "Roles",
|
|
2324
|
-
configRoleItem: "{id} (chain: {
|
|
2325
|
-
|
|
2256
|
+
configRoleItem: "{id} (specified chain: {chain})",
|
|
2257
|
+
configRoleUnspecified: "not specified (dsh default)",
|
|
2326
2258
|
configCooldown: "Cooldown",
|
|
2327
2259
|
configRevert: "Revert",
|
|
2328
2260
|
configMaxSwitches: "Max switches per request walk",
|
|
@@ -2372,23 +2304,6 @@ function recentFallbacksSwitches(events, limit) {
|
|
|
2372
2304
|
}
|
|
2373
2305
|
return found;
|
|
2374
2306
|
}
|
|
2375
|
-
function resolveChainForDiagnostic(roles, rootChain, role, warn = console.warn) {
|
|
2376
|
-
if (role.trim() === "inherit") return {
|
|
2377
|
-
chainRole: false,
|
|
2378
|
-
chain: rootChain,
|
|
2379
|
-
inherit: rootChain.length > 0
|
|
2380
|
-
};
|
|
2381
|
-
const roleDef = roles.find((declared) => declared.id.trim() === role.trim());
|
|
2382
|
-
if (roleDef === void 0) warn(`llm-fallbacks: unknown role "${role}" — falling back to rootChain`);
|
|
2383
|
-
const roleChain = roleDef?.chain ?? [];
|
|
2384
|
-
const chain = roleChain.length > 0 ? roleChain : roleDef?.fallback === "none" ? [] : rootChain;
|
|
2385
|
-
const inherit = rootChain.length > 0 && (roleDef === void 0 || roleDef.fallback !== "none");
|
|
2386
|
-
return {
|
|
2387
|
-
chainRole: roleChain.length > 0,
|
|
2388
|
-
chain,
|
|
2389
|
-
inherit
|
|
2390
|
-
};
|
|
2391
|
-
}
|
|
2392
2307
|
function formatSwitch(entry, t) {
|
|
2393
2308
|
const from = `${entry.from.provider}/${entry.from.model}`;
|
|
2394
2309
|
const to = `${entry.to.provider}/${entry.to.model}`;
|
|
@@ -2404,7 +2319,7 @@ function formatConfigList(items) {
|
|
|
2404
2319
|
return [...items.slice(0, 5), "…"].join(", ");
|
|
2405
2320
|
}
|
|
2406
2321
|
function formatConfigRoles(roles, t) {
|
|
2407
|
-
const items = roles.slice(0, 5).map((role) => t.configRoleItem.replace("{id}", role.id).replace("{
|
|
2322
|
+
const items = roles.slice(0, 5).map((role) => t.configRoleItem.replace("{id}", role.id).replace("{chain}", role.chain === void 0 || role.chain === "" ? t.configRoleUnspecified : role.chain));
|
|
2408
2323
|
const list = items.length === 0 ? "" : `${items.join(", ")}${roles.length > 5 ? ", …" : ""}`;
|
|
2409
2324
|
return `${roles.length}${list.length === 0 ? "" : ` — ${list}`}`;
|
|
2410
2325
|
}
|
|
@@ -2459,21 +2374,14 @@ function formatConfigTimeSlots(slots, t) {
|
|
|
2459
2374
|
const list = items.length === 0 ? "" : `${items.join(", ")}${slots.length > 5 ? ", …" : ""}`;
|
|
2460
2375
|
return `${slots.length}${list.length === 0 ? "" : ` — ${list}`}`;
|
|
2461
2376
|
}
|
|
2462
|
-
function formatConfigRules(rules, t) {
|
|
2463
|
-
const items = rules.slice(0, 5).map((rule) => `${rule.provider === "" ? "*" : rule.provider}/${rule.model === "" ? "*" : rule.model} → ${rule.role}`);
|
|
2464
|
-
const list = items.length === 0 ? "" : `${items.join(", ")}${rules.length > 5 ? ", …" : ""}`;
|
|
2465
|
-
return `${rules.length}${list.length === 0 ? "" : ` — ${list}`}`;
|
|
2466
|
-
}
|
|
2467
2377
|
function fallbacksConfigText(summary, locale = "zh") {
|
|
2468
2378
|
const t = FALLBACKS_COMMAND_LOCALES[locale];
|
|
2469
2379
|
return [
|
|
2470
2380
|
`${t.configTitle}: ${summary.active ? t.configEnabled : t.configDisabled}`,
|
|
2471
2381
|
`${t.configTriggerCodes}: ${summary.triggerCodes.length === 0 ? t.configEmpty : formatConfigList(summary.triggerCodes)}`,
|
|
2472
|
-
|
|
2473
|
-
`${t.configTimeSlots}: ${summary.timeSlots.length === 0 ? t.configEmpty : formatConfigTimeSlots(summary.timeSlots, t)}`,
|
|
2382
|
+
...summary.chains.map((chain) => t.configChainItem.replace("{id}", chain.id).replace("{label}", chain.label).replace("{entries}", chain.entries.length === 0 ? t.configEmpty : chain.entries.join(" → ")).replace("{slots}", chain.timeSlots.length === 0 ? t.configEmpty : formatConfigTimeSlots(chain.timeSlots, t))),
|
|
2474
2383
|
`${t.configTz}: ${summary.tz}`,
|
|
2475
2384
|
`${t.configRoles}: ${formatConfigRoles(summary.roles, t)}`,
|
|
2476
|
-
`${t.configRules}: ${summary.rules.length === 0 ? t.configEmpty : formatConfigRules(summary.rules, t)}`,
|
|
2477
2385
|
`${t.configCooldown}: ${summary.cooldownMs} ms`,
|
|
2478
2386
|
`${t.configRevert}: ${summary.revertPolicy}`,
|
|
2479
2387
|
`${t.configMaxSwitches}: ${summary.maxSwitchesPerStep}`,
|
|
@@ -2487,13 +2395,13 @@ function fallbacksCommandText(snapshot, locale = "zh") {
|
|
|
2487
2395
|
const t = FALLBACKS_COMMAND_LOCALES[locale];
|
|
2488
2396
|
const lines = [t.title];
|
|
2489
2397
|
lines.push(`${t.origin}: ${snapshot.origin}`);
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2398
|
+
const roleChain = snapshot.roleChain === void 0 || snapshot.roleChain === "" ? t.configRoleUnspecified : snapshot.roleChain;
|
|
2399
|
+
lines.push(`${t.role}: ${snapshot.role === "" ? t.configRoleUnspecified : snapshot.role}(${roleChain})`);
|
|
2400
|
+
if (snapshot.chains.length === 0) lines.push(`${t.chain}: ${t.chainNone}`);
|
|
2401
|
+
else for (const chain of snapshot.chains) {
|
|
2402
|
+
const marker = chain.selected ? " ●" : "";
|
|
2403
|
+
lines.push(`${t.chain}${marker}: ${chain.id}(${chain.label}): ${chain.entries.length === 0 ? t.chainNone : chain.entries.join(" → ")}|${t.slot}: ${chain.slotLabel}`);
|
|
2495
2404
|
}
|
|
2496
|
-
lines.push(`${t.slot}: ${snapshot.slot.label}`);
|
|
2497
2405
|
if (snapshot.switches.length === 0) lines.push(`${t.switches}: ${t.switchesNone}`);
|
|
2498
2406
|
else {
|
|
2499
2407
|
lines.push(`${t.switches} (${snapshot.switches.length}):`);
|
|
@@ -2588,9 +2496,7 @@ var FallbacksSeedManager = class {
|
|
|
2588
2496
|
});
|
|
2589
2497
|
outcome.applied.push(seed.id);
|
|
2590
2498
|
}
|
|
2591
|
-
const
|
|
2592
|
-
const currentList = roleRows(config);
|
|
2593
|
-
const currentRules = roleRules(config);
|
|
2499
|
+
const currentList = roleRows(io.read());
|
|
2594
2500
|
const candidate = new Map(this.registry);
|
|
2595
2501
|
if (entries.size === 0) candidate.delete(source);
|
|
2596
2502
|
else candidate.set(source, {
|
|
@@ -2607,10 +2513,7 @@ var FallbacksSeedManager = class {
|
|
|
2607
2513
|
}
|
|
2608
2514
|
const newList = materialize(currentList, incoming, prior, outcome.conflicts);
|
|
2609
2515
|
for (const conflict of outcome.conflicts) this.logger.warn(`llm-fallbacks: seeds: persona-source conflict for seed id ${JSON.stringify(conflict.id)} — operator row persona kept (never overwritten)`);
|
|
2610
|
-
const computed = {
|
|
2611
|
-
list: newList,
|
|
2612
|
-
rules: currentRules
|
|
2613
|
-
};
|
|
2516
|
+
const computed = { list: newList };
|
|
2614
2517
|
if (!deepEqual(newList, currentList)) await io.writeRoles(computed);
|
|
2615
2518
|
if (entries.size === 0) this.registry.delete(source);
|
|
2616
2519
|
else this.registry.set(source, {
|
|
@@ -2654,8 +2557,7 @@ var FallbacksSeedManager = class {
|
|
|
2654
2557
|
source: seeded ? declared.source : "user"
|
|
2655
2558
|
};
|
|
2656
2559
|
if (seeded) effective.seedPersona = declared.persona;
|
|
2657
|
-
if (row.chain !== void 0) effective.chain = row.chain;
|
|
2658
|
-
if (row.fallback !== void 0) effective.fallback = row.fallback;
|
|
2560
|
+
if (row.chain !== void 0 && row.chain !== "") effective.chain = row.chain;
|
|
2659
2561
|
return effective;
|
|
2660
2562
|
}) };
|
|
2661
2563
|
}
|
|
@@ -2679,9 +2581,7 @@ var FallbacksSeedManager = class {
|
|
|
2679
2581
|
reverted: false,
|
|
2680
2582
|
reason: "not-seeded"
|
|
2681
2583
|
};
|
|
2682
|
-
const
|
|
2683
|
-
const rows = roleRows(config);
|
|
2684
|
-
const rules = roleRules(config);
|
|
2584
|
+
const rows = roleRows(io.read());
|
|
2685
2585
|
const index = rows.findIndex((row) => row.id.trim() === seedId);
|
|
2686
2586
|
if (index === -1) return {
|
|
2687
2587
|
reverted: false,
|
|
@@ -2695,10 +2595,7 @@ var FallbacksSeedManager = class {
|
|
|
2695
2595
|
...row,
|
|
2696
2596
|
persona: declared.persona
|
|
2697
2597
|
} : row);
|
|
2698
|
-
await io.writeRoles({
|
|
2699
|
-
list: nextList,
|
|
2700
|
-
rules
|
|
2701
|
-
});
|
|
2598
|
+
await io.writeRoles({ list: nextList });
|
|
2702
2599
|
return {
|
|
2703
2600
|
reverted: true,
|
|
2704
2601
|
persona: declared.persona
|
|
@@ -2767,10 +2664,6 @@ function roleRows(config) {
|
|
|
2767
2664
|
const list = config.roles?.list;
|
|
2768
2665
|
return Array.isArray(list) ? list : [];
|
|
2769
2666
|
}
|
|
2770
|
-
function roleRules(config) {
|
|
2771
|
-
const rules = config.roles?.rules;
|
|
2772
|
-
return Array.isArray(rules) ? rules : [];
|
|
2773
|
-
}
|
|
2774
2667
|
function deepEqual(a, b) {
|
|
2775
2668
|
if (a === b) return true;
|
|
2776
2669
|
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
@@ -2919,8 +2812,8 @@ function buildFallbacksTuiSection() {
|
|
|
2919
2812
|
ns: FALLBACKS_TUI_SECTION_NS,
|
|
2920
2813
|
title: "fallbacks",
|
|
2921
2814
|
descriptions: {
|
|
2922
|
-
zh: "
|
|
2923
|
-
en: "Fallback settings:
|
|
2815
|
+
zh: "回退设置:虚拟链(默认降级链 + 链尾 + 分时槽)、角色与高级选项(与 Web 设置卡片完全一致)。主代理需选中 FallbacksChain/<链 id> 才会执行降级链;子代理沿用派发代理的链。",
|
|
2816
|
+
en: "Fallback settings: named chains (default degradation chain + tail + time slots), roles, and advanced options (full parity with the web settings card). The main agent must select FallbacksChain/<chain id> to walk a chain; subagents inherit the delegating agent's chain."
|
|
2924
2817
|
},
|
|
2925
2818
|
groups: [
|
|
2926
2819
|
{
|
|
@@ -3016,63 +2909,34 @@ function buildFallbacksTuiSection() {
|
|
|
3016
2909
|
parse: tzParse
|
|
3017
2910
|
},
|
|
3018
2911
|
{
|
|
3019
|
-
path: ["
|
|
3020
|
-
label: "
|
|
3021
|
-
descriptions: {
|
|
3022
|
-
zh: "根降级链(JSON)",
|
|
3023
|
-
en: "Root chain (JSON)"
|
|
3024
|
-
},
|
|
3025
|
-
hint: "JSON array of model selectors; the last entry must be an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro). Pro is available in the catalog — the settings card no longer disables it.",
|
|
3026
|
-
hintDescriptions: {
|
|
3027
|
-
zh: "模型选择器 JSON 数组;末项必须是官方模型(deepseek-official/deepseek-flash 或 deepseek-official/deepseek-v4-pro)。Pro 已在目录中提供——设置卡中不再禁用。",
|
|
3028
|
-
en: "JSON array of model selectors; the last entry must be an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro). Pro is available in the catalog — the settings card no longer disables it."
|
|
3029
|
-
},
|
|
3030
|
-
group: "chain",
|
|
3031
|
-
kind: "text",
|
|
3032
|
-
format: jsonFieldFormat,
|
|
3033
|
-
parse: jsonFieldParse((parsed) => ({ rootChain: parsed }))
|
|
3034
|
-
},
|
|
3035
|
-
{
|
|
3036
|
-
path: ["timeSlots"],
|
|
3037
|
-
label: "Time slots (JSON)",
|
|
2912
|
+
path: ["chains"],
|
|
2913
|
+
label: "Chains (JSON)",
|
|
3038
2914
|
descriptions: {
|
|
3039
|
-
zh: "
|
|
3040
|
-
en: "
|
|
2915
|
+
zh: "虚拟链(JSON)",
|
|
2916
|
+
en: "Chains (JSON)"
|
|
3041
2917
|
},
|
|
3042
|
-
hint: "JSON array of
|
|
2918
|
+
hint: "JSON array of named chains ({ id, label, entries, timeSlots }); each entries array must end with an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro).",
|
|
3043
2919
|
hintDescriptions: {
|
|
3044
|
-
zh: "
|
|
3045
|
-
en: "JSON array of
|
|
2920
|
+
zh: "命名虚拟链 JSON 数组({ id, label, entries, timeSlots });每条 entries 末项必须是官方模型(deepseek-official/deepseek-flash 或 deepseek-official/deepseek-v4-pro)。",
|
|
2921
|
+
en: "JSON array of named chains ({ id, label, entries, timeSlots }); each entries array must end with an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro)."
|
|
3046
2922
|
},
|
|
3047
2923
|
group: "chain",
|
|
3048
2924
|
kind: "text",
|
|
3049
2925
|
format: jsonFieldFormat,
|
|
3050
|
-
parse: jsonFieldParse((parsed) => ({
|
|
2926
|
+
parse: jsonFieldParse((parsed) => ({ chains: parsed }))
|
|
3051
2927
|
},
|
|
3052
2928
|
{
|
|
3053
2929
|
path: ["roles", "list"],
|
|
3054
2930
|
label: "Roles (JSON)",
|
|
3055
2931
|
descriptions: {
|
|
3056
|
-
zh: "角色列表(JSON
|
|
3057
|
-
en: "Roles (JSON)"
|
|
2932
|
+
zh: "角色列表(JSON,chain 为指定降级链指针)",
|
|
2933
|
+
en: "Roles (JSON; chain is the specified-chain pointer)"
|
|
3058
2934
|
},
|
|
3059
2935
|
group: "roles",
|
|
3060
2936
|
kind: "text",
|
|
3061
2937
|
format: jsonFieldFormat,
|
|
3062
2938
|
parse: jsonFieldParse((parsed) => ({ roles: { list: parsed } }))
|
|
3063
2939
|
},
|
|
3064
|
-
{
|
|
3065
|
-
path: ["roles", "rules"],
|
|
3066
|
-
label: "Role rules (JSON)",
|
|
3067
|
-
descriptions: {
|
|
3068
|
-
zh: "角色规则(JSON)",
|
|
3069
|
-
en: "Role rules (JSON)"
|
|
3070
|
-
},
|
|
3071
|
-
group: "roles",
|
|
3072
|
-
kind: "text",
|
|
3073
|
-
format: jsonFieldFormat,
|
|
3074
|
-
parse: jsonFieldParse((parsed) => ({ roles: { rules: parsed } }))
|
|
3075
|
-
},
|
|
3076
2940
|
{
|
|
3077
2941
|
path: ["cooldownMs"],
|
|
3078
2942
|
label: "Cooldown (ms)",
|
|
@@ -3144,65 +3008,6 @@ const stateStores = new WeakMap();
|
|
|
3144
3008
|
function stateStore(ctx) {
|
|
3145
3009
|
return stateStores.get(ctx);
|
|
3146
3010
|
}
|
|
3147
|
-
const blockedAttemptStores = new WeakMap();
|
|
3148
|
-
function blockedAttempts(ctx) {
|
|
3149
|
-
return blockedAttemptStores.get(ctx);
|
|
3150
|
-
}
|
|
3151
|
-
const chainHeadStores = new WeakMap();
|
|
3152
|
-
function chainHeads(ctx) {
|
|
3153
|
-
return chainHeadStores.get(ctx);
|
|
3154
|
-
}
|
|
3155
|
-
function latestByAt(map) {
|
|
3156
|
-
let latest;
|
|
3157
|
-
for (const value of map.values()) if (latest === void 0 || value.at >= latest.at) latest = value;
|
|
3158
|
-
return latest;
|
|
3159
|
-
}
|
|
3160
|
-
function agentChainView(ctx, sessionId) {
|
|
3161
|
-
if (sessionId === void 0) return void 0;
|
|
3162
|
-
const agent = ctx.get("agents")?.get(sessionId);
|
|
3163
|
-
if (agent === void 0) return void 0;
|
|
3164
|
-
const header = agent.session?.header;
|
|
3165
|
-
const route = agent.options;
|
|
3166
|
-
return {
|
|
3167
|
-
origin: header?.origin ?? "root",
|
|
3168
|
-
...header?.agentPreset === void 0 ? {} : { agentPreset: header.agentPreset },
|
|
3169
|
-
...typeof route?.provider === "string" && typeof route?.model === "string" ? { optionsRoute: {
|
|
3170
|
-
provider: route.provider,
|
|
3171
|
-
model: route.model
|
|
3172
|
-
} } : {},
|
|
3173
|
-
sessionId
|
|
3174
|
-
};
|
|
3175
|
-
}
|
|
3176
|
-
function readSubagentSettings(ctx) {
|
|
3177
|
-
const settings = ctx.get("settings");
|
|
3178
|
-
if (settings === void 0) return void 0;
|
|
3179
|
-
const descriptor = settings.describe().find((d) => d.ns === SUBAGENT_POLICY_SETTINGS_NS);
|
|
3180
|
-
if (descriptor === void 0) return void 0;
|
|
3181
|
-
const policy = policySettingsFrom(descriptor.value);
|
|
3182
|
-
if (policy === void 0) throw new Error("subagent model-selection settings are malformed");
|
|
3183
|
-
return policy;
|
|
3184
|
-
}
|
|
3185
|
-
function readGuardedPolicySettings(ctx, agentId, lastKnown, warn) {
|
|
3186
|
-
try {
|
|
3187
|
-
const settings = readSubagentSettings(ctx);
|
|
3188
|
-
if (settings === void 0) return {
|
|
3189
|
-
ok: true,
|
|
3190
|
-
settings: lastKnown.get(agentId)
|
|
3191
|
-
};
|
|
3192
|
-
lastKnown.set(agentId, settings);
|
|
3193
|
-
return {
|
|
3194
|
-
ok: true,
|
|
3195
|
-
settings
|
|
3196
|
-
};
|
|
3197
|
-
} catch (error) {
|
|
3198
|
-
warn("llm-fallbacks: agent \"%s\" subagent model-selection settings read failed — %s", agentId, error?.message ?? String(error));
|
|
3199
|
-
const known = lastKnown.get(agentId);
|
|
3200
|
-
return known === void 0 ? { ok: false } : {
|
|
3201
|
-
ok: true,
|
|
3202
|
-
settings: known
|
|
3203
|
-
};
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
3011
|
function isLiveConfigRef(value) {
|
|
3207
3012
|
return typeof value === "object" && value !== null && typeof value.get === "function";
|
|
3208
3013
|
}
|
|
@@ -3230,12 +3035,11 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3230
3035
|
throw new Error(seedsSettingsUnavailable);
|
|
3231
3036
|
};
|
|
3232
3037
|
ctx.inject(["settings"], (sctx) => {
|
|
3233
|
-
writeRoles = (roles) => sctx.settings
|
|
3038
|
+
writeRoles = (roles) => writeFallbacksSection(sctx.settings, { roles });
|
|
3234
3039
|
try {
|
|
3235
3040
|
sctx.provide("llm-fallbacks", {
|
|
3236
3041
|
name: "llm-fallbacks",
|
|
3237
3042
|
version,
|
|
3238
|
-
resolveRole,
|
|
3239
3043
|
resolveChain,
|
|
3240
3044
|
validateFallbacksConfig,
|
|
3241
3045
|
detectLegacyKeys,
|
|
@@ -3262,14 +3066,15 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3262
3066
|
};
|
|
3263
3067
|
let serviceOwned = ctx.get("llm-fallbacks") === void 0;
|
|
3264
3068
|
let settingsComposed;
|
|
3265
|
-
|
|
3069
|
+
const rawSource = () => {
|
|
3266
3070
|
if (isLiveConfigRef(config)) return config.get();
|
|
3267
3071
|
if (settingsComposed !== void 0) return settingsComposed;
|
|
3268
3072
|
return entry;
|
|
3269
3073
|
};
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3074
|
+
const source = () => migrateLegacyConfig(rawSource());
|
|
3075
|
+
validateFallbacksConfig(migrateLegacyConfig(entry), logger);
|
|
3076
|
+
const legacyKeys = detectLegacyKeys(rawSource());
|
|
3077
|
+
if (legacyKeys.length > 0) logger.warn("llm-fallbacks: legacy config keys detected (v1 two-block keys); see docs/configuration.md migration table — %o", legacyKeys);
|
|
3273
3078
|
let roleIds = new Map(entry.roles.list.map((role) => [role.id.trim(), role.id]));
|
|
3274
3079
|
ctx.inject(["settings"], (sctx) => {
|
|
3275
3080
|
const settings = sctx.settings;
|
|
@@ -3291,23 +3096,14 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3291
3096
|
settingsComposed = void 0;
|
|
3292
3097
|
};
|
|
3293
3098
|
});
|
|
3294
|
-
const bridge = {
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
const blockedAttemptMap = new Map();
|
|
3298
|
-
blockedAttemptStores.set(ctx, blockedAttemptMap);
|
|
3299
|
-
const lastKnownPolicySettings = new Map();
|
|
3300
|
-
const policyFor = (sessionId) => {
|
|
3301
|
-
const settingsRead = readGuardedPolicySettings(ctx, sessionId, lastKnownPolicySettings, logger.warn);
|
|
3302
|
-
if (!settingsRead.ok) return { state: "unprovable" };
|
|
3303
|
-
return effectivePolicy(readSessionPolicyEvent(ctx.get("agents")?.get(sessionId)?.session.snapshotEvents() ?? []), settingsRead.settings);
|
|
3099
|
+
const bridge = {
|
|
3100
|
+
source: () => source(),
|
|
3101
|
+
rawSource: () => rawSource()
|
|
3304
3102
|
};
|
|
3305
3103
|
const subagentSeam = installSubagentSeam(ctx, {
|
|
3306
3104
|
roleIds: () => roleIds,
|
|
3307
3105
|
roles: () => source().roles.list,
|
|
3308
3106
|
source: () => source(),
|
|
3309
|
-
policyFor,
|
|
3310
|
-
chainHeads: chainHeadMap,
|
|
3311
3107
|
debug: (message) => logger.debug(message)
|
|
3312
3108
|
});
|
|
3313
3109
|
if (!subagentSeam.ownsWrapper) ctx.logger("llm-fallbacks").debug("subagent role seam wrapper already installed on this context root — this fiber registers the notice emitter and the projection unit only (multi-fiber dedupe)");
|
|
@@ -3317,20 +3113,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3317
3113
|
debug: (message) => logger.debug(message)
|
|
3318
3114
|
});
|
|
3319
3115
|
try {
|
|
3320
|
-
new FallbacksConfigGateway(ctx, bridge, seeds
|
|
3321
|
-
try {
|
|
3322
|
-
return {
|
|
3323
|
-
policy: effectivePolicy({
|
|
3324
|
-
ok: false,
|
|
3325
|
-
present: false
|
|
3326
|
-
}, readSubagentSettings(ctx)),
|
|
3327
|
-
head: latestByAt(chainHeadMap),
|
|
3328
|
-
blockedAttempt: latestByAt(blockedAttemptMap)
|
|
3329
|
-
};
|
|
3330
|
-
} catch {
|
|
3331
|
-
return { policy: { state: "unprovable" } };
|
|
3332
|
-
}
|
|
3333
|
-
});
|
|
3116
|
+
new FallbacksConfigGateway(ctx, bridge, seeds);
|
|
3334
3117
|
} catch (error) {
|
|
3335
3118
|
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
3336
3119
|
ctx.logger("llm-fallbacks").debug("fallbacks gateway already registered — no gateway on this fiber (multi-fiber dedupe)");
|
|
@@ -3350,49 +3133,44 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3350
3133
|
const walkDeps = {
|
|
3351
3134
|
readConfig: () => source(),
|
|
3352
3135
|
getLlm: () => ctx.get("llm"),
|
|
3353
|
-
resolveAgentView: (sessionId) => agentChainView(ctx, sessionId),
|
|
3354
3136
|
states,
|
|
3355
|
-
roleIds: () => roleIds,
|
|
3356
|
-
seamRecord: (sessionId) => subagentSeam.records.get(sessionId),
|
|
3357
|
-
policyFor,
|
|
3358
|
-
recordBlocked: (sessionId, route, reason) => {
|
|
3359
|
-
blockedAttemptMap.set(sessionId, {
|
|
3360
|
-
at: Date.now(),
|
|
3361
|
-
route,
|
|
3362
|
-
reason
|
|
3363
|
-
});
|
|
3364
|
-
},
|
|
3365
3137
|
slotWinners,
|
|
3366
3138
|
log: logger
|
|
3367
3139
|
};
|
|
3368
3140
|
const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source(), (options) => walkChain(walkDeps, options));
|
|
3369
3141
|
ctx.on("agent/disposed", ({ agent }) => {
|
|
3370
3142
|
states.delete(agent.id);
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
chainHeadMap.delete(agent.id);
|
|
3143
|
+
const markerPrefix = slotMarkerPrefix(agent.id);
|
|
3144
|
+
for (const key of slotWinners.keys()) if (key.startsWith(markerPrefix)) slotWinners.delete(key);
|
|
3374
3145
|
subagentSeam.records.delete(agent.id);
|
|
3375
|
-
lastKnownPolicySettings.delete(agent.id);
|
|
3376
3146
|
});
|
|
3377
3147
|
ctx.effect(() => () => {
|
|
3378
3148
|
states.clear();
|
|
3379
3149
|
slotWinners.clear();
|
|
3380
|
-
blockedAttemptMap.clear();
|
|
3381
|
-
chainHeadMap.clear();
|
|
3382
|
-
lastKnownPolicySettings.clear();
|
|
3383
3150
|
}, "llm-fallbacks: clear per-agent state");
|
|
3384
3151
|
const fallbacksCommandController = {
|
|
3385
3152
|
getSnapshot(agent) {
|
|
3386
3153
|
const config = source();
|
|
3387
|
-
const role = subagentSeam.records.get(agent.id)?.role ?? resolveRole(agent, config.roles.rules, roleIds, logger.warn, servedRouteOf(config, agent.options, new Date()));
|
|
3388
3154
|
const state = states.peek(agent.id);
|
|
3389
3155
|
const recovery = config.recovery ?? "timer";
|
|
3390
3156
|
if (state !== void 0 && recovery === "half-open") states.syncRecovery(state, Date.now(), recovery);
|
|
3157
|
+
const roleRecord = subagentSeam.records.get(agent.id);
|
|
3158
|
+
const role = roleRecord?.role ?? "";
|
|
3159
|
+
const roleChain = roleRecord === void 0 ? void 0 : source().roles.list.find((entry) => entry.id === role)?.chain;
|
|
3160
|
+
const selectedChainId = agent.options?.provider === "FallbacksChain" ? agent.options?.model : void 0;
|
|
3161
|
+
const now = new Date();
|
|
3162
|
+
const chains = source().chains.map((chain) => ({
|
|
3163
|
+
id: chain.id,
|
|
3164
|
+
label: chain.label,
|
|
3165
|
+
entries: chain.entries,
|
|
3166
|
+
slotLabel: resolveSlotState(chain, now, source().tz ?? "Asia/Shanghai").label,
|
|
3167
|
+
selected: chain.id === selectedChainId
|
|
3168
|
+
}));
|
|
3391
3169
|
return {
|
|
3392
3170
|
origin: agent.session.header?.origin ?? "root",
|
|
3393
3171
|
role,
|
|
3394
|
-
...
|
|
3395
|
-
|
|
3172
|
+
...roleChain === void 0 ? {} : { roleChain },
|
|
3173
|
+
chains,
|
|
3396
3174
|
switches: recentFallbacksSwitches(agent.session.snapshotEvents(), 5),
|
|
3397
3175
|
cooldown: state === void 0 ? [] : recovery === "half-open" ? [...state.cooldown.snapshot(), ...state.recovery.halfOpenEntries().map((entry) => ({
|
|
3398
3176
|
key: entry.key,
|
|
@@ -3406,25 +3184,24 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3406
3184
|
return {
|
|
3407
3185
|
active: isFallbackActive(config),
|
|
3408
3186
|
triggerCodes: config.triggerCodes,
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3187
|
+
chains: config.chains.map((chain) => ({
|
|
3188
|
+
id: chain.id,
|
|
3189
|
+
label: chain.label,
|
|
3190
|
+
entries: Array.isArray(chain.entries) ? chain.entries : [],
|
|
3191
|
+
timeSlots: (chain.timeSlots ?? []).map((row) => row.kind === "preset" ? {
|
|
3192
|
+
preset: row.preset,
|
|
3193
|
+
chainCount: (row.chain ?? []).length
|
|
3194
|
+
} : {
|
|
3195
|
+
start: row.start,
|
|
3196
|
+
end: row.end,
|
|
3197
|
+
days: row.days,
|
|
3198
|
+
chainCount: (row.chain ?? []).length
|
|
3199
|
+
})
|
|
3200
|
+
})),
|
|
3419
3201
|
tz: config.tz ?? "Asia/Shanghai",
|
|
3420
3202
|
roles: config.roles.list.map((role) => ({
|
|
3421
3203
|
id: role.id,
|
|
3422
|
-
|
|
3423
|
-
})),
|
|
3424
|
-
rules: config.roles.rules.map((rule) => ({
|
|
3425
|
-
provider: rule.provider ?? "",
|
|
3426
|
-
model: rule.model ?? "",
|
|
3427
|
-
role: rule.role
|
|
3204
|
+
chain: role.chain
|
|
3428
3205
|
})),
|
|
3429
3206
|
cooldownMs: config.cooldownMs,
|
|
3430
3207
|
revertPolicy: config.revertPolicy,
|
|
@@ -3455,4 +3232,4 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3455
3232
|
});
|
|
3456
3233
|
}
|
|
3457
3234
|
//#endregion
|
|
3458
|
-
export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, ROLE_PROJECTION_KEY, SelectorError, annotateCandidates, apply,
|
|
3235
|
+
export { CHAIN_ID_PATTERN, Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, ROLE_PROJECTION_KEY, SelectorError, annotateCandidates, apply, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, readRoleProjectionValue, resolveCandidate, resolveChain, resolveChainViews, selectCandidates, stateStore, validateFallbacksConfig };
|