dsh-llm-fallbacks 0.3.4 → 0.4.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/README.zh-CN.md +26 -2
- package/dist/authorized-route.d.ts +68 -0
- package/dist/client/ConversationFallbackSwitch.d.ts +18 -14
- package/dist/client/fallbacks-store.d.ts +120 -18
- package/dist/client/index.d.ts +9 -8
- package/dist/client/index.js +176 -41
- package/dist/client/locales.d.ts +14 -0
- package/dist/commands.d.ts +4 -0
- package/dist/config.d.ts +14 -0
- package/dist/cooldown.d.ts +9 -0
- package/dist/gateway.d.ts +79 -3
- package/dist/index.d.ts +25 -5
- package/dist/index.js +470 -50
- package/dist/override.d.ts +39 -0
- package/dist/recovery.d.ts +92 -0
- package/dist/route-allowlist.d.ts +43 -0
- package/dist/state.d.ts +67 -2
- package/dist/subagent-policy.d.ts +123 -0
- package/dist/virtual-adapter.d.ts +49 -0
- package/package.json +25 -21
- package/scripts/build-client.ts +10 -6
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
2
|
import z from "@deepseek-ai/schemastery";
|
|
4
3
|
import { LlmAdapter, LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
4
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
@@ -259,6 +258,65 @@ function resolveSlotState(config, now, tz) {
|
|
|
259
258
|
label: "all-day"
|
|
260
259
|
};
|
|
261
260
|
}
|
|
261
|
+
const ESCALATION_CAP_MS = 36e5;
|
|
262
|
+
function escalatedCooldownMs(ms, n) {
|
|
263
|
+
return Math.max(ms, Math.min(ESCALATION_CAP_MS, ms * 2 ** (n - 1)));
|
|
264
|
+
}
|
|
265
|
+
var RecoveryStore = class {
|
|
266
|
+
entries = new Map();
|
|
267
|
+
recordFailure(key) {
|
|
268
|
+
const entry = this.entries.get(key);
|
|
269
|
+
if (entry === void 0) {
|
|
270
|
+
this.entries.set(key, {
|
|
271
|
+
consecutiveFailures: 1,
|
|
272
|
+
halfOpen: false,
|
|
273
|
+
probeLogged: false
|
|
274
|
+
});
|
|
275
|
+
return 1;
|
|
276
|
+
}
|
|
277
|
+
entry.consecutiveFailures += 1;
|
|
278
|
+
entry.halfOpen = false;
|
|
279
|
+
entry.probeLogged = false;
|
|
280
|
+
return entry.consecutiveFailures;
|
|
281
|
+
}
|
|
282
|
+
markHalfOpen(key, until) {
|
|
283
|
+
const entry = this.entries.get(key);
|
|
284
|
+
if (entry === void 0) {
|
|
285
|
+
this.entries.set(key, {
|
|
286
|
+
consecutiveFailures: 0,
|
|
287
|
+
halfOpen: true,
|
|
288
|
+
probeLogged: false,
|
|
289
|
+
expiredUntil: until
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
entry.halfOpen = true;
|
|
294
|
+
entry.probeLogged = false;
|
|
295
|
+
entry.expiredUntil = until;
|
|
296
|
+
}
|
|
297
|
+
tryMarkProbeLogged(key) {
|
|
298
|
+
const entry = this.entries.get(key);
|
|
299
|
+
if (entry === void 0 || !entry.halfOpen || entry.probeLogged) return false;
|
|
300
|
+
entry.probeLogged = true;
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
isHalfOpen(key) {
|
|
304
|
+
return this.entries.get(key)?.halfOpen === true;
|
|
305
|
+
}
|
|
306
|
+
close(key) {
|
|
307
|
+
const entry = this.entries.get(key);
|
|
308
|
+
if (entry === void 0 || !entry.halfOpen) return;
|
|
309
|
+
this.entries.delete(key);
|
|
310
|
+
}
|
|
311
|
+
halfOpenEntries() {
|
|
312
|
+
const out = [];
|
|
313
|
+
for (const [key, entry] of this.entries) if (entry.halfOpen && entry.expiredUntil !== void 0) out.push({
|
|
314
|
+
key,
|
|
315
|
+
untilEpochMs: entry.expiredUntil
|
|
316
|
+
});
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
262
320
|
//#endregion
|
|
263
321
|
//#region .build/host/config.js
|
|
264
322
|
const defaultFallbacksConfig = {
|
|
@@ -280,7 +338,8 @@ const defaultFallbacksConfig = {
|
|
|
280
338
|
presets: "bundled",
|
|
281
339
|
roleAutoMatch: true,
|
|
282
340
|
timeSlots: [],
|
|
283
|
-
tz: "Asia/Shanghai"
|
|
341
|
+
tz: "Asia/Shanghai",
|
|
342
|
+
recovery: "timer"
|
|
284
343
|
};
|
|
285
344
|
const INHERIT_ROLE_ID = "inherit";
|
|
286
345
|
const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
@@ -325,6 +384,7 @@ function validateFallbacksConfig(config, logger) {
|
|
|
325
384
|
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`);
|
|
326
385
|
}
|
|
327
386
|
}
|
|
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`);
|
|
328
388
|
}
|
|
329
389
|
function detectLegacyKeys(source) {
|
|
330
390
|
const keys = [];
|
|
@@ -394,7 +454,8 @@ const Config = z.object({
|
|
|
394
454
|
days: z.array(z.number()),
|
|
395
455
|
chain: z.array(z.string())
|
|
396
456
|
})).default([]),
|
|
397
|
-
tz: z.string().default("Asia/Shanghai")
|
|
457
|
+
tz: z.string().default("Asia/Shanghai"),
|
|
458
|
+
recovery: z.union([z.const("timer"), z.const("half-open")]).default("timer")
|
|
398
459
|
});
|
|
399
460
|
function buildAutomatchPrompt(roles, agent) {
|
|
400
461
|
const lines = [
|
|
@@ -633,6 +694,98 @@ function firstExactCandidate(all, wildcard) {
|
|
|
633
694
|
for (let index = 0; index < all.length; index += 1) if (!wildcard[index]) return all[index];
|
|
634
695
|
}
|
|
635
696
|
//#endregion
|
|
697
|
+
//#region .build/host/authorized-route.js
|
|
698
|
+
function completeRoute(source) {
|
|
699
|
+
if (typeof source !== "object" || source === null || typeof source.provider !== "string" || source.provider.length === 0 || typeof source.model !== "string" || source.model.length === 0) return;
|
|
700
|
+
return {
|
|
701
|
+
provider: source.provider,
|
|
702
|
+
model: source.model
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function lastModelSelection(entries) {
|
|
706
|
+
if (entries === void 0) return void 0;
|
|
707
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
708
|
+
const candidate = entries[index];
|
|
709
|
+
if (candidate?.type !== "model/selection") continue;
|
|
710
|
+
const route = completeRoute(candidate.data);
|
|
711
|
+
if (route !== void 0) return route;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function detectAuthorizedRoute(session) {
|
|
715
|
+
const { events, requestHeader, options, inherited } = session;
|
|
716
|
+
const selected = lastModelSelection(events);
|
|
717
|
+
if (selected !== void 0) return selected;
|
|
718
|
+
const route = completeRoute(requestHeader) ?? completeRoute(options);
|
|
719
|
+
if (route === void 0) return void 0;
|
|
720
|
+
const baseline = completeRoute(inherited);
|
|
721
|
+
if (baseline !== void 0 && route.provider === baseline.provider && route.model === baseline.model) return;
|
|
722
|
+
return route;
|
|
723
|
+
}
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region .build/host/route-allowlist.js
|
|
726
|
+
function resolvedRoutes(candidates) {
|
|
727
|
+
const routes = [];
|
|
728
|
+
for (const candidate of candidates) {
|
|
729
|
+
if (candidate.model === void 0) continue;
|
|
730
|
+
routes.push({
|
|
731
|
+
provider: candidate.provider,
|
|
732
|
+
model: candidate.model
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
return routes;
|
|
736
|
+
}
|
|
737
|
+
function routeOnAllowlist(route, allowedModels) {
|
|
738
|
+
return allowedModels.some((allowed) => allowed.provider === route.provider && allowed.model === route.model);
|
|
739
|
+
}
|
|
740
|
+
function firstAllowedCandidate(resolved, allowedModels) {
|
|
741
|
+
return resolved.find((route) => routeOnAllowlist(route, allowedModels));
|
|
742
|
+
}
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region .build/host/subagent-policy.js
|
|
745
|
+
function parseRoutes(value) {
|
|
746
|
+
if (!Array.isArray(value)) return void 0;
|
|
747
|
+
const routes = [];
|
|
748
|
+
for (const candidate of value) {
|
|
749
|
+
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;
|
|
750
|
+
routes.push({
|
|
751
|
+
provider: candidate.provider,
|
|
752
|
+
model: candidate.model
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
return routes;
|
|
756
|
+
}
|
|
757
|
+
function readSessionPolicyEvent(events) {
|
|
758
|
+
const event = events.find((candidate) => candidate.type === "subagent/model-selection-policy");
|
|
759
|
+
if (event === void 0) return {
|
|
760
|
+
ok: false,
|
|
761
|
+
present: false
|
|
762
|
+
};
|
|
763
|
+
const data = event.data;
|
|
764
|
+
const routes = parseRoutes(typeof data === "object" && data !== null && "allowedModels" in data ? data.allowedModels : void 0);
|
|
765
|
+
if (routes === void 0 || routes.length === 0) return {
|
|
766
|
+
ok: false,
|
|
767
|
+
present: true
|
|
768
|
+
};
|
|
769
|
+
return {
|
|
770
|
+
ok: true,
|
|
771
|
+
allowedModels: routes
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function effectivePolicy(event, settings) {
|
|
775
|
+
if (event.ok) return {
|
|
776
|
+
state: "enabled",
|
|
777
|
+
allowedModels: event.allowedModels
|
|
778
|
+
};
|
|
779
|
+
if (event.present) return { state: "unprovable" };
|
|
780
|
+
if (settings === void 0 || settings.enabled !== true) return { state: "disabled" };
|
|
781
|
+
const routes = parseRoutes(settings.allowedModels);
|
|
782
|
+
if (routes === void 0 || routes.length === 0) return { state: "unprovable" };
|
|
783
|
+
return {
|
|
784
|
+
state: "enabled",
|
|
785
|
+
allowedModels: routes
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
636
789
|
//#region .build/host/cooldown.js
|
|
637
790
|
var CooldownStore = class {
|
|
638
791
|
entries = new Map();
|
|
@@ -651,6 +804,12 @@ var CooldownStore = class {
|
|
|
651
804
|
}
|
|
652
805
|
return true;
|
|
653
806
|
}
|
|
807
|
+
peek(key) {
|
|
808
|
+
return this.entries.get(key);
|
|
809
|
+
}
|
|
810
|
+
keys() {
|
|
811
|
+
return this.entries.keys();
|
|
812
|
+
}
|
|
654
813
|
snapshot(now = Date.now()) {
|
|
655
814
|
const active = [];
|
|
656
815
|
for (const [key, until] of this.entries) {
|
|
@@ -701,7 +860,8 @@ var FallbackStateStore = class {
|
|
|
701
860
|
failed: new StepFailureSet(),
|
|
702
861
|
switchCount: 0
|
|
703
862
|
},
|
|
704
|
-
cooldown: new CooldownStore()
|
|
863
|
+
cooldown: new CooldownStore(),
|
|
864
|
+
recovery: new RecoveryStore()
|
|
705
865
|
};
|
|
706
866
|
this.states.set(agentId, state);
|
|
707
867
|
}
|
|
@@ -752,13 +912,47 @@ var FallbackStateStore = class {
|
|
|
752
912
|
suppress(state, key, untilEpochMs) {
|
|
753
913
|
state.cooldown.suppress(key, untilEpochMs);
|
|
754
914
|
}
|
|
755
|
-
isSuppressed(state, key, now = Date.now()) {
|
|
756
|
-
return state.cooldown.isSuppressed(key, now);
|
|
915
|
+
isSuppressed(state, key, now = Date.now(), recovery = "timer") {
|
|
916
|
+
if (recovery !== "half-open") return state.cooldown.isSuppressed(key, now);
|
|
917
|
+
const until = state.cooldown.peek(key);
|
|
918
|
+
if (until === void 0) return false;
|
|
919
|
+
if (until > now) return true;
|
|
920
|
+
state.cooldown.isSuppressed(key, now);
|
|
921
|
+
state.recovery.markHalfOpen(key, until);
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
syncRecovery(state, now, mode) {
|
|
925
|
+
if (mode !== "half-open") return;
|
|
926
|
+
for (const key of state.cooldown.keys()) this.isSuppressed(state, key, now, mode);
|
|
927
|
+
}
|
|
928
|
+
observeSuccess(state, key) {
|
|
929
|
+
state.recovery.close(key);
|
|
757
930
|
}
|
|
758
931
|
};
|
|
759
932
|
//#endregion
|
|
933
|
+
//#region .build/host/override.js
|
|
934
|
+
function overrideConfigWithRouteRule(seed, to, explicitEffort) {
|
|
935
|
+
if (explicitEffort !== void 0) return {
|
|
936
|
+
...seed,
|
|
937
|
+
reasoningEffort: explicitEffort,
|
|
938
|
+
provider: to.provider,
|
|
939
|
+
model: to.model
|
|
940
|
+
};
|
|
941
|
+
if (!(to.provider !== seed.provider || to.model !== seed.model)) return {
|
|
942
|
+
...seed,
|
|
943
|
+
provider: to.provider,
|
|
944
|
+
model: to.model
|
|
945
|
+
};
|
|
946
|
+
const { reasoningEffort: _routeOwned, ...withoutRouteOwnedEffort } = seed;
|
|
947
|
+
return {
|
|
948
|
+
...withoutRouteOwnedEffort,
|
|
949
|
+
provider: to.provider,
|
|
950
|
+
model: to.model
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
//#endregion
|
|
760
954
|
//#region .build/host/gateway.js
|
|
761
|
-
const FALLBACKS_SETTINGS_NAMESPACE =
|
|
955
|
+
const FALLBACKS_SETTINGS_NAMESPACE = "fallbacks";
|
|
762
956
|
const CONFIG_KEYS = {
|
|
763
957
|
enabled: true,
|
|
764
958
|
triggerCodes: true,
|
|
@@ -771,7 +965,8 @@ const CONFIG_KEYS = {
|
|
|
771
965
|
presets: true,
|
|
772
966
|
roleAutoMatch: true,
|
|
773
967
|
timeSlots: true,
|
|
774
|
-
tz: true
|
|
968
|
+
tz: true,
|
|
969
|
+
recovery: true
|
|
775
970
|
};
|
|
776
971
|
const ROLES_KEYS = {
|
|
777
972
|
list: true,
|
|
@@ -791,10 +986,12 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
791
986
|
bridge;
|
|
792
987
|
seeds;
|
|
793
988
|
settings;
|
|
794
|
-
|
|
989
|
+
subagentPolicy;
|
|
990
|
+
constructor(ctx, bridge, seeds, subagentPolicy) {
|
|
795
991
|
super(ctx, "fallbacks");
|
|
796
992
|
this.bridge = bridge;
|
|
797
993
|
this.seeds = seeds;
|
|
994
|
+
this.subagentPolicy = subagentPolicy;
|
|
798
995
|
ctx.inject(["settings"], (sctx) => {
|
|
799
996
|
this.settings = sctx.settings;
|
|
800
997
|
return () => {
|
|
@@ -840,11 +1037,14 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
840
1037
|
}
|
|
841
1038
|
readResult() {
|
|
842
1039
|
const source = this.bridge.source();
|
|
843
|
-
|
|
1040
|
+
const result = {
|
|
844
1041
|
config: this.readConfig(source),
|
|
845
1042
|
legacyKeys: detectLegacyKeys(source),
|
|
846
1043
|
seeds: this.seeds.wireStatus(this.seedsIo())
|
|
847
1044
|
};
|
|
1045
|
+
const projected = projectSubagentPolicy(this.subagentPolicy);
|
|
1046
|
+
if (projected !== void 0) result.subagentPolicy = projected;
|
|
1047
|
+
return result;
|
|
848
1048
|
}
|
|
849
1049
|
seedsIo() {
|
|
850
1050
|
const settings = this.settings;
|
|
@@ -857,6 +1057,41 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
857
1057
|
};
|
|
858
1058
|
}
|
|
859
1059
|
};
|
|
1060
|
+
function projectSubagentPolicy(snapshot) {
|
|
1061
|
+
if (snapshot === void 0) return void 0;
|
|
1062
|
+
let captured;
|
|
1063
|
+
try {
|
|
1064
|
+
captured = snapshot();
|
|
1065
|
+
} catch {
|
|
1066
|
+
return { state: "unprovable" };
|
|
1067
|
+
}
|
|
1068
|
+
const { policy, head, blockedAttempt } = captured;
|
|
1069
|
+
if (policy.state === "disabled") return { state: "disabled" };
|
|
1070
|
+
if (policy.state === "unprovable") return { state: "unprovable" };
|
|
1071
|
+
const wire = {
|
|
1072
|
+
state: "enabled",
|
|
1073
|
+
allowedModels: policy.allowedModels.map((route) => ({
|
|
1074
|
+
provider: route.provider,
|
|
1075
|
+
model: route.model
|
|
1076
|
+
}))
|
|
1077
|
+
};
|
|
1078
|
+
if (head !== void 0) wire.head = {
|
|
1079
|
+
route: {
|
|
1080
|
+
provider: head.route.provider,
|
|
1081
|
+
model: head.route.model
|
|
1082
|
+
},
|
|
1083
|
+
source: head.source
|
|
1084
|
+
};
|
|
1085
|
+
if (blockedAttempt !== void 0) wire.blockedAttempt = {
|
|
1086
|
+
at: blockedAttempt.at,
|
|
1087
|
+
route: {
|
|
1088
|
+
provider: blockedAttempt.route.provider,
|
|
1089
|
+
model: blockedAttempt.route.model
|
|
1090
|
+
},
|
|
1091
|
+
reason: blockedAttempt.reason
|
|
1092
|
+
};
|
|
1093
|
+
return wire;
|
|
1094
|
+
}
|
|
860
1095
|
function normalizeRoles(value) {
|
|
861
1096
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
862
1097
|
const roles = {};
|
|
@@ -1005,6 +1240,7 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
1005
1240
|
cooldownNone: "无活跃冷却",
|
|
1006
1241
|
cooldownLine: "{key} 冷却至 {time}",
|
|
1007
1242
|
cooldownNever: "{key} 会话内不再回主",
|
|
1243
|
+
cooldownHalfOpen: "{key} half-open(等待恢复探针)",
|
|
1008
1244
|
reason: {
|
|
1009
1245
|
"trigger-code": "触发码",
|
|
1010
1246
|
"always-cap": "always 上限"
|
|
@@ -1059,6 +1295,7 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
1059
1295
|
cooldownNone: "none active",
|
|
1060
1296
|
cooldownLine: "{key} suppressed until {time}",
|
|
1061
1297
|
cooldownNever: "{key} not reverting this session",
|
|
1298
|
+
cooldownHalfOpen: "{key} half-open (awaiting recovery probe)",
|
|
1062
1299
|
reason: {
|
|
1063
1300
|
"trigger-code": "trigger-code",
|
|
1064
1301
|
"always-cap": "always-cap"
|
|
@@ -1151,6 +1388,7 @@ function formatSwitch(entry, t) {
|
|
|
1151
1388
|
return t.switchLine.replace("{from}", from).replace("{to}", to).replace("{role}", entry.role).replace("{reason}", t.reason[entry.reason] ?? entry.reason);
|
|
1152
1389
|
}
|
|
1153
1390
|
function formatCooldown(entry, t) {
|
|
1391
|
+
if (entry.halfOpen === true) return t.cooldownHalfOpen.replace("{key}", entry.key);
|
|
1154
1392
|
if (!Number.isFinite(entry.untilEpochMs)) return t.cooldownNever.replace("{key}", entry.key);
|
|
1155
1393
|
return t.cooldownLine.replace("{key}", entry.key).replace("{time}", new Date(entry.untilEpochMs).toISOString());
|
|
1156
1394
|
}
|
|
@@ -1954,6 +2192,15 @@ var FallbacksChainAdapter = class extends LlmAdapter {
|
|
|
1954
2192
|
name: model
|
|
1955
2193
|
};
|
|
1956
2194
|
}
|
|
2195
|
+
imageRequestPricing(_provider, _model) {
|
|
2196
|
+
try {
|
|
2197
|
+
const head = effectiveHeadOf(this.readConfig(), new Date());
|
|
2198
|
+
if (head === void 0 || head.provider === "FallbacksChain") return void 0;
|
|
2199
|
+
return this.getLlm()?.imageRequestPricing(head.provider, head.model);
|
|
2200
|
+
} catch {
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
1957
2204
|
stream(options) {
|
|
1958
2205
|
const config = this.readConfig();
|
|
1959
2206
|
const now = new Date();
|
|
@@ -2015,6 +2262,19 @@ const stateStores = new WeakMap();
|
|
|
2015
2262
|
function stateStore(ctx) {
|
|
2016
2263
|
return stateStores.get(ctx);
|
|
2017
2264
|
}
|
|
2265
|
+
const blockedAttemptStores = new WeakMap();
|
|
2266
|
+
function blockedAttempts(ctx) {
|
|
2267
|
+
return blockedAttemptStores.get(ctx);
|
|
2268
|
+
}
|
|
2269
|
+
const chainHeadStores = new WeakMap();
|
|
2270
|
+
function chainHeads(ctx) {
|
|
2271
|
+
return chainHeadStores.get(ctx);
|
|
2272
|
+
}
|
|
2273
|
+
function latestByAt(map) {
|
|
2274
|
+
let latest;
|
|
2275
|
+
for (const value of map.values()) if (latest === void 0 || value.at >= latest.at) latest = value;
|
|
2276
|
+
return latest;
|
|
2277
|
+
}
|
|
2018
2278
|
async function makeModelExists(ctx, providers) {
|
|
2019
2279
|
const llm = ctx.get("llm");
|
|
2020
2280
|
if (llm === void 0 || typeof llm.listModels !== "function") return () => true;
|
|
@@ -2051,12 +2311,49 @@ function slotWinnerKey(winner) {
|
|
|
2051
2311
|
if (winner === "all-day") return "all-day";
|
|
2052
2312
|
return winner.kind === "preset" ? `preset:${winner.preset}` : `custom:${winner.start}-${winner.end}`;
|
|
2053
2313
|
}
|
|
2054
|
-
function overrideConfig(seed, to) {
|
|
2055
|
-
|
|
2314
|
+
function overrideConfig(seed, to, explicitEffort) {
|
|
2315
|
+
return overrideConfigWithRouteRule(seed, to, explicitEffort);
|
|
2316
|
+
}
|
|
2317
|
+
function readSubagentSettings(ctx) {
|
|
2318
|
+
const service = ctx.get("subagentModelSelection");
|
|
2319
|
+
return service === void 0 ? void 0 : service.current();
|
|
2320
|
+
}
|
|
2321
|
+
function readGuardedPolicySettings(ctx, agentId, lastKnown, warn) {
|
|
2322
|
+
try {
|
|
2323
|
+
const service = ctx.get("subagentModelSelection");
|
|
2324
|
+
if (service === void 0) return {
|
|
2325
|
+
ok: true,
|
|
2326
|
+
settings: lastKnown.get(agentId)
|
|
2327
|
+
};
|
|
2328
|
+
const settings = service.current();
|
|
2329
|
+
lastKnown.set(agentId, settings);
|
|
2330
|
+
return {
|
|
2331
|
+
ok: true,
|
|
2332
|
+
settings
|
|
2333
|
+
};
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
warn("llm-fallbacks: agent \"%s\" subagent model-selection settings read failed — %s", agentId, error?.message ?? String(error));
|
|
2336
|
+
const known = lastKnown.get(agentId);
|
|
2337
|
+
return known === void 0 ? { ok: false } : {
|
|
2338
|
+
ok: true,
|
|
2339
|
+
settings: known
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
function authorizedRouteView(ctx, agent) {
|
|
2344
|
+
const header = agent.session?.header;
|
|
2345
|
+
const parentId = header?.origin === "subagent" ? header.parentSession : void 0;
|
|
2346
|
+
const parent = parentId === void 0 ? void 0 : ctx.get("agents")?.get(parentId);
|
|
2347
|
+
const parentHeader = parent?.session.requestHeader()?.config;
|
|
2348
|
+
const parentOptions = parent?.options;
|
|
2056
2349
|
return {
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2350
|
+
requestHeader: agent.session?.requestHeader()?.config,
|
|
2351
|
+
events: agent.session?.events,
|
|
2352
|
+
options: agent.options,
|
|
2353
|
+
inherited: parentHeader ?? (typeof parentOptions?.provider === "string" && typeof parentOptions?.model === "string" ? {
|
|
2354
|
+
provider: parentOptions.provider,
|
|
2355
|
+
model: parentOptions.model
|
|
2356
|
+
} : void 0)
|
|
2060
2357
|
};
|
|
2061
2358
|
}
|
|
2062
2359
|
function apply(ctx, config = defaultFallbacksConfig) {
|
|
@@ -2105,21 +2402,40 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2105
2402
|
if (legacyKeys.length > 0) logger.warn("llm-fallbacks: legacy config keys detected (chains/roles.default/undeclared role refs); see docs/configuration.md migration table — %o", legacyKeys);
|
|
2106
2403
|
let roleIds = new Map(entry.roles.list.map((role) => [role.id.trim(), role.id]));
|
|
2107
2404
|
let hasChains = entry.rootChain.length > 0 || entry.roles.list.some((role) => (role.chain?.length ?? 0) > 0);
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2405
|
+
ctx.inject(["settings"], (sctx) => {
|
|
2406
|
+
sctx.settings.installSection(ctx, FALLBACKS_SETTINGS_NAMESPACE, Config, entry, {
|
|
2407
|
+
setSource: (current) => {
|
|
2408
|
+
source = current;
|
|
2409
|
+
},
|
|
2410
|
+
onChange: () => {
|
|
2411
|
+
const current = source();
|
|
2412
|
+
roleIds = new Map(current.roles.list.map((role) => [role.id.trim(), role.id]));
|
|
2413
|
+
hasChains = current.rootChain.length > 0 || current.roles.list.some((role) => (role.chain?.length ?? 0) > 0);
|
|
2414
|
+
reconcileFallbacksAdapter();
|
|
2415
|
+
slotWinners.clear();
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2119
2418
|
});
|
|
2120
2419
|
const bridge = { source: () => source() };
|
|
2420
|
+
const chainHeadMap = new Map();
|
|
2421
|
+
chainHeadStores.set(ctx, chainHeadMap);
|
|
2422
|
+
const blockedAttemptMap = new Map();
|
|
2423
|
+
blockedAttemptStores.set(ctx, blockedAttemptMap);
|
|
2121
2424
|
try {
|
|
2122
|
-
new FallbacksConfigGateway(ctx, bridge, seeds)
|
|
2425
|
+
new FallbacksConfigGateway(ctx, bridge, seeds, () => {
|
|
2426
|
+
try {
|
|
2427
|
+
return {
|
|
2428
|
+
policy: effectivePolicy({
|
|
2429
|
+
ok: false,
|
|
2430
|
+
present: false
|
|
2431
|
+
}, readSubagentSettings(ctx)),
|
|
2432
|
+
head: latestByAt(chainHeadMap),
|
|
2433
|
+
blockedAttempt: latestByAt(blockedAttemptMap)
|
|
2434
|
+
};
|
|
2435
|
+
} catch {
|
|
2436
|
+
return { policy: { state: "unprovable" } };
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2123
2439
|
} catch (error) {
|
|
2124
2440
|
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
2125
2441
|
ctx.logger("llm-fallbacks").debug("fallbacks gateway already registered — no gateway on this fiber (multi-fiber dedupe)");
|
|
@@ -2137,6 +2453,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2137
2453
|
stateStores.set(ctx, states);
|
|
2138
2454
|
const dispatchInjected = new Set();
|
|
2139
2455
|
const slotWinners = new Map();
|
|
2456
|
+
const lastKnownPolicySettings = new Map();
|
|
2140
2457
|
async function decide(agent, turn, step, current, reason, state) {
|
|
2141
2458
|
const config = source();
|
|
2142
2459
|
if (state !== void 0) {
|
|
@@ -2148,15 +2465,48 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2148
2465
|
const { all, wildcard } = resolveChainViews(config.roles.list, rootTail, role, current.provider, current.model, logger.warn);
|
|
2149
2466
|
if (all.length === 0) return null;
|
|
2150
2467
|
const modelExists = hasWildcardEntry(config.roles.list, rootTail, role) ? await makeModelExists(ctx, [...new Set(all.map((candidate) => candidate.provider))]) : void 0;
|
|
2151
|
-
const cooldown = { isSuppressed: (key) => state !== void 0 && states.isSuppressed(state, key) };
|
|
2468
|
+
const cooldown = { isSuppressed: (key) => state !== void 0 && states.isSuppressed(state, key, Date.now(), source().recovery ?? "timer") };
|
|
2152
2469
|
const failed = { has: (key) => state !== void 0 && state.stepFailures.failed.has(key) };
|
|
2153
2470
|
const surviving = selectCandidates(all, wildcard, createCandidateFilter({
|
|
2154
2471
|
current,
|
|
2155
2472
|
cooldown,
|
|
2156
2473
|
failed
|
|
2157
2474
|
}), modelExists);
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2475
|
+
const first = surviving[0];
|
|
2476
|
+
let target;
|
|
2477
|
+
if (first !== void 0 && first.model !== void 0) target = {
|
|
2478
|
+
provider: first.provider,
|
|
2479
|
+
model: first.model
|
|
2480
|
+
};
|
|
2481
|
+
if (agent.session?.header?.origin === "subagent" && target !== void 0) {
|
|
2482
|
+
const settingsRead = readGuardedPolicySettings(ctx, agent.id, lastKnownPolicySettings, logger.warn);
|
|
2483
|
+
const policy = settingsRead.ok ? effectivePolicy(readSessionPolicyEvent(agent.session.events), settingsRead.settings) : { state: "unprovable" };
|
|
2484
|
+
if (policy.state === "unprovable") {
|
|
2485
|
+
logger.warn("llm-fallbacks: agent \"%s\" subagent model-selection policy is on but unreadable — skipping fallback switch", agent.id);
|
|
2486
|
+
return null;
|
|
2487
|
+
}
|
|
2488
|
+
if (policy.state === "enabled") {
|
|
2489
|
+
const allowed = firstAllowedCandidate(resolvedRoutes(surviving), policy.allowedModels);
|
|
2490
|
+
if (allowed === void 0) {
|
|
2491
|
+
logger.warn("llm-fallbacks: agent \"%s\" fallback switch %s/%s blocked: resolved candidates are all outside the subagent allowlist — no switch", agent.id, target.provider, target.model);
|
|
2492
|
+
blockedAttemptMap.set(agent.id, {
|
|
2493
|
+
at: Date.now(),
|
|
2494
|
+
route: {
|
|
2495
|
+
provider: target.provider,
|
|
2496
|
+
model: target.model
|
|
2497
|
+
},
|
|
2498
|
+
reason
|
|
2499
|
+
});
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2502
|
+
target = {
|
|
2503
|
+
provider: allowed.provider,
|
|
2504
|
+
model: allowed.model
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (target === void 0) return null;
|
|
2509
|
+
if ((source().recovery ?? "timer") === "half-open" && state !== void 0 && state.recovery.tryMarkProbeLogged(selectorKey(target.provider, target.model))) logger.info("llm-fallbacks: agent \"%s\" half-open probe %s/%s (role=%s, reason=%s)", agent.id, target.provider, target.model, role, reason);
|
|
2160
2510
|
logger.info("llm-fallbacks: agent \"%s\" fallback switch (降级切换) %s/%s -> %s/%s (role=%s, reason=%s, candidates=%o)", agent.id, current.provider, current.model, target.provider, target.model, role, reason, annotateCandidates(all, surviving, {
|
|
2161
2511
|
current,
|
|
2162
2512
|
cooldown,
|
|
@@ -2178,13 +2528,22 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2178
2528
|
function commit(state, pending, turn, step) {
|
|
2179
2529
|
const config = source();
|
|
2180
2530
|
const fromKey = selectorKey(pending.from.provider, pending.from.model);
|
|
2181
|
-
const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : Date.now() + config.cooldownMs;
|
|
2531
|
+
const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : config.recovery === "half-open" ? Date.now() + escalatedCooldownMs(config.cooldownMs, state.recovery.recordFailure(fromKey)) : Date.now() + config.cooldownMs;
|
|
2182
2532
|
states.syncStep(state, turn, step);
|
|
2183
2533
|
states.writePending(state, pending);
|
|
2184
2534
|
states.suppress(state, fromKey, until);
|
|
2185
2535
|
states.recordFailure(state, fromKey);
|
|
2186
2536
|
states.recordSwitch(state);
|
|
2187
2537
|
}
|
|
2538
|
+
function failHalfOpenProbe(agentId, current) {
|
|
2539
|
+
const key = selectorKey(current.provider, current.model);
|
|
2540
|
+
if ((source().recovery ?? "timer") !== "half-open") return;
|
|
2541
|
+
if (source().revertPolicy === "never") return;
|
|
2542
|
+
if (states.peek(agentId)?.recovery.isHalfOpen(key) !== true) return;
|
|
2543
|
+
const state = states.get(agentId);
|
|
2544
|
+
const n = state.recovery.recordFailure(key);
|
|
2545
|
+
states.suppress(state, key, Date.now() + escalatedCooldownMs(source().cooldownMs, n));
|
|
2546
|
+
}
|
|
2188
2547
|
ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure }, next) => {
|
|
2189
2548
|
const config = source();
|
|
2190
2549
|
if (!config.enabled || !config.triggerCodes.includes(failure.code)) return next();
|
|
@@ -2192,7 +2551,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2192
2551
|
if (!current.model) return next();
|
|
2193
2552
|
try {
|
|
2194
2553
|
const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id));
|
|
2195
|
-
if (pending === null)
|
|
2554
|
+
if (pending === null) {
|
|
2555
|
+
failHalfOpenProbe(agent.id, current);
|
|
2556
|
+
return next();
|
|
2557
|
+
}
|
|
2196
2558
|
commit(states.get(agent.id), pending, turn, step);
|
|
2197
2559
|
return { kind: "retry" };
|
|
2198
2560
|
} catch (error) {
|
|
@@ -2230,21 +2592,54 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2230
2592
|
if (config.enabled && hasChains && agent.session?.header?.origin === "subagent" && !dispatchInjected.has(agent.id)) {
|
|
2231
2593
|
dispatchInjected.add(agent.id);
|
|
2232
2594
|
try {
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2595
|
+
const settingsRead = readGuardedPolicySettings(ctx, agent.id, lastKnownPolicySettings, logger.warn);
|
|
2596
|
+
const policy = settingsRead.ok ? effectivePolicy(readSessionPolicyEvent(agent.session.events), settingsRead.settings) : { state: "unprovable" };
|
|
2597
|
+
if (policy.state === "unprovable") logger.warn("llm-fallbacks: agent \"%s\" subagent model-selection policy is on but unreadable — skipping role-inject", agent.id);
|
|
2598
|
+
else {
|
|
2599
|
+
const authorized = policy.state === "enabled" ? detectAuthorizedRoute(authorizedRouteView(ctx, agent)) : void 0;
|
|
2600
|
+
if (authorized !== void 0) {
|
|
2601
|
+
chainHeadMap.set(agent.id, {
|
|
2602
|
+
at: Date.now(),
|
|
2603
|
+
route: {
|
|
2604
|
+
provider: authorized.provider,
|
|
2605
|
+
model: authorized.model
|
|
2606
|
+
},
|
|
2607
|
+
source: "authorized"
|
|
2608
|
+
});
|
|
2609
|
+
logger.info("llm-fallbacks: agent \"%s\" authorized child route %s/%s is the chain head — skipping role-inject", agent.id, authorized.provider, authorized.model);
|
|
2610
|
+
} else {
|
|
2611
|
+
const role = await resolveRoleAtDispatch(agent, config.roles.rules, roleIds, {
|
|
2612
|
+
automatchEnabled: config.roleAutoMatch ?? true,
|
|
2613
|
+
automatch: (agent) => pickRoleByLlm(ctx, config.roles, agent, { warn: logger.warn }),
|
|
2614
|
+
warn: logger.warn
|
|
2615
|
+
});
|
|
2616
|
+
if (role !== "inherit") {
|
|
2617
|
+
const { all, wildcard } = resolveChainViews(config.roles.list, config.rootChain, role, seed.provider, seed.model, logger.warn);
|
|
2618
|
+
let to;
|
|
2619
|
+
if (policy.state === "enabled") {
|
|
2620
|
+
const routes = resolvedRoutes(all);
|
|
2621
|
+
to = firstAllowedCandidate(routes, policy.allowedModels);
|
|
2622
|
+
if (to === void 0 && routes.length > 0) logger.warn("llm-fallbacks: agent \"%s\" role-inject candidates are all outside the subagent allowlist — skipping inject (host seed stands)", agent.id);
|
|
2623
|
+
} else {
|
|
2624
|
+
const head = firstExactCandidate(all, wildcard);
|
|
2625
|
+
if (head !== void 0 && head.model !== void 0) to = {
|
|
2626
|
+
provider: head.provider,
|
|
2627
|
+
model: head.model
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
if (to !== void 0 && !(to.provider === seed.provider && to.model === seed.model)) {
|
|
2631
|
+
if (policy.state === "enabled") chainHeadMap.set(agent.id, {
|
|
2632
|
+
at: Date.now(),
|
|
2633
|
+
route: {
|
|
2634
|
+
provider: to.provider,
|
|
2635
|
+
model: to.model
|
|
2636
|
+
},
|
|
2637
|
+
source: "injected"
|
|
2638
|
+
});
|
|
2639
|
+
logger.info("llm-fallbacks: agent \"%s\" role-inject role=%s model=%s/%s", agent.id, role, to.provider, to.model);
|
|
2640
|
+
return overrideConfig(seed, to);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2248
2643
|
}
|
|
2249
2644
|
}
|
|
2250
2645
|
} catch (error) {
|
|
@@ -2262,7 +2657,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2262
2657
|
commit(commitState, pending, turn, step);
|
|
2263
2658
|
const appliedCap = states.applyPending(commitState, turn, step);
|
|
2264
2659
|
if (appliedCap !== void 0) return overrideConfig(seed, appliedCap.to);
|
|
2265
|
-
}
|
|
2660
|
+
} else failHalfOpenProbe(agent.id, {
|
|
2661
|
+
provider: seed.provider,
|
|
2662
|
+
model: seed.model
|
|
2663
|
+
});
|
|
2266
2664
|
}
|
|
2267
2665
|
return seed;
|
|
2268
2666
|
});
|
|
@@ -2275,24 +2673,46 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2275
2673
|
states.delete(agent.id);
|
|
2276
2674
|
dispatchInjected.delete(agent.id);
|
|
2277
2675
|
slotWinners.delete(agent.id);
|
|
2676
|
+
blockedAttemptMap.delete(agent.id);
|
|
2677
|
+
chainHeadMap.delete(agent.id);
|
|
2678
|
+
lastKnownPolicySettings.delete(agent.id);
|
|
2679
|
+
});
|
|
2680
|
+
ctx.on("session/event", (session, event) => {
|
|
2681
|
+
if (event.type !== "assistant/message") return;
|
|
2682
|
+
if ((source().recovery ?? "timer") !== "half-open") return;
|
|
2683
|
+
if (event.data.interrupted === true) return;
|
|
2684
|
+
const message = event.data.message;
|
|
2685
|
+
if (message.source.kind !== "model") return;
|
|
2686
|
+
const state = states.peek(session.id);
|
|
2687
|
+
if (state === void 0) return;
|
|
2688
|
+
states.observeSuccess(state, selectorKey(message.source.provider, message.source.model));
|
|
2278
2689
|
});
|
|
2279
2690
|
ctx.effect(() => () => {
|
|
2280
2691
|
states.clear();
|
|
2281
2692
|
dispatchInjected.clear();
|
|
2282
2693
|
slotWinners.clear();
|
|
2694
|
+
blockedAttemptMap.clear();
|
|
2695
|
+
chainHeadMap.clear();
|
|
2696
|
+
lastKnownPolicySettings.clear();
|
|
2283
2697
|
}, "llm-fallbacks: clear per-agent state");
|
|
2284
2698
|
const fallbacksCommandController = {
|
|
2285
2699
|
getSnapshot(agent) {
|
|
2286
2700
|
const config = source();
|
|
2287
2701
|
const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn);
|
|
2288
2702
|
const state = states.peek(agent.id);
|
|
2703
|
+
const recovery = config.recovery ?? "timer";
|
|
2704
|
+
if (state !== void 0 && recovery === "half-open") states.syncRecovery(state, Date.now(), recovery);
|
|
2289
2705
|
return {
|
|
2290
2706
|
origin: agent.session.header?.origin ?? "root",
|
|
2291
2707
|
role,
|
|
2292
2708
|
...resolveChainForDiagnostic(config.roles.list, config.rootChain, role, logger.warn),
|
|
2293
2709
|
slot: resolveSlotState(config, new Date(), config.tz ?? "Asia/Shanghai"),
|
|
2294
2710
|
switches: recentFallbacksSwitches(agent.session.events, 5),
|
|
2295
|
-
cooldown: state === void 0 ? [] : state.cooldown.snapshot()
|
|
2711
|
+
cooldown: state === void 0 ? [] : recovery === "half-open" ? [...state.cooldown.snapshot(), ...state.recovery.halfOpenEntries().map((entry) => ({
|
|
2712
|
+
key: entry.key,
|
|
2713
|
+
untilEpochMs: entry.untilEpochMs,
|
|
2714
|
+
halfOpen: true
|
|
2715
|
+
}))] : state.cooldown.snapshot()
|
|
2296
2716
|
};
|
|
2297
2717
|
},
|
|
2298
2718
|
getConfig() {
|
|
@@ -2351,4 +2771,4 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2351
2771
|
});
|
|
2352
2772
|
}
|
|
2353
2773
|
//#endregion
|
|
2354
|
-
export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };
|
|
2774
|
+
export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, blockedAttempts, chainHeads, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };
|