dsh-llm-fallbacks 0.3.5 → 0.4.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 +23 -1
- package/README.zh-CN.md +23 -1
- 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 +170 -39
- package/dist/client/locales.d.ts +14 -0
- package/dist/commands.d.ts +1 -1
- package/dist/gateway.d.ts +79 -3
- package/dist/index.d.ts +24 -4
- package/dist/index.js +343 -41
- package/dist/override.d.ts +39 -0
- package/dist/route-allowlist.d.ts +43 -0
- package/dist/state.d.ts +40 -0
- 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.d.ts
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* cooldown + failure bookkeeping, then return
|
|
17
17
|
* `{ kind: 'retry' }` (own recovery, no `next()`).
|
|
18
18
|
* - `agent/request` waterfall: apply a pending switch after `await next()`
|
|
19
|
-
* (provider/model override
|
|
20
|
-
*
|
|
19
|
+
* (provider/model override; the inherited `reasoningEffort` follows the
|
|
20
|
+
* upstream routeChanged rule — dropped on a route change unless explicitly
|
|
21
|
+
* named, preserved on a same-route override; spec D3); a
|
|
21
22
|
* root-origin `FallbacksChain/Auto` seed then overrides to the
|
|
22
23
|
* effective chain's first exact head (select-is-primary, plan
|
|
23
24
|
* fallbacks-virtual-chain Task 2); then the always-mode cap check (count
|
|
@@ -36,7 +37,7 @@ import { detectLegacyKeys, validateFallbacksConfig, type FallbacksConfig } from
|
|
|
36
37
|
import { Config } from './schema.ts';
|
|
37
38
|
import { resolveChain } from './chains.ts';
|
|
38
39
|
import { resolveRole } from './roles.ts';
|
|
39
|
-
import { FallbackStateStore } from './state.ts';
|
|
40
|
+
import { FallbackStateStore, type BlockedSwitchAttempt, type EffectiveChainHead } from './state.ts';
|
|
40
41
|
import { type EffectiveRolesReadback, type SeedDeclaration, type SeedDeclareOutcome, type SeedRevertOutcome } from './seeds.ts';
|
|
41
42
|
/** The plugin row id mounted by the profile bundle patch. */
|
|
42
43
|
export declare const name = "llm-fallbacks";
|
|
@@ -82,7 +83,9 @@ export { Config };
|
|
|
82
83
|
/** The plugin's composition config — the `fallbacks` settings schema (spec §4). */
|
|
83
84
|
export type Config = FallbacksConfig;
|
|
84
85
|
export type { FallbackSwitchReason, FallbacksSwitchEventData } from './events.ts';
|
|
85
|
-
export type { AgentFallbackState, FallbackStateStore, PendingSwitch, StepFailures } from './state.ts';
|
|
86
|
+
export type { AgentFallbackState, BlockedSwitchAttempt, FallbackStateStore, PendingSwitch, StepFailures } from './state.ts';
|
|
87
|
+
/** @internal T5 test-seam types (mirrors `BlockedSwitchAttempt`; not public API). */
|
|
88
|
+
export type { ChainHeadSource, EffectiveChainHead } from './state.ts';
|
|
86
89
|
export { resolveRole, type AgentLike, type Origin, } from './roles.ts';
|
|
87
90
|
export { annotateCandidates, createCandidateFilter, hasWildcardEntry, resolveCandidate, resolveChain, resolveChainViews, selectCandidates, type AnnotatedCandidate, type CandidateFilterOptions, type CandidateSkipReason, type FailingModel, } from './chains.ts';
|
|
88
91
|
export { defaultFallbacksConfig, detectLegacyKeys, INHERIT_ROLE_ID, ROLE_ID_PATTERN, validateFallbacksConfig, type FallbacksConfig, type FallbacksConfigLogger, type FallbacksRole, type FallbacksRoles, type FallbacksRoleRule, type FallbackStrategy, type RevertPolicy, type RecoveryPolicy, } from './config.ts';
|
|
@@ -96,6 +99,23 @@ export { presetRoles } from './presets.ts';
|
|
|
96
99
|
* request must not grow the store) without reaching into the closure.
|
|
97
100
|
*/
|
|
98
101
|
export declare function stateStore(ctx: Context): FallbackStateStore | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* @internal Test seam (mirrors `stateStore`): the per-agent blocked-switch
|
|
104
|
+
* attempts recorded by the T3 allowlist gate for the plugin applied to `ctx`
|
|
105
|
+
* (spec D1; in-memory only, issue #52). Not part of the plugin's public
|
|
106
|
+
* surface; lets tests (and the T5 gateway projection) read blocked attempts
|
|
107
|
+
* without reaching into the closure. `undefined` when no plugin is applied.
|
|
108
|
+
*/
|
|
109
|
+
export declare function blockedAttempts(ctx: Context): ReadonlyMap<string, BlockedSwitchAttempt> | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* @internal Test seam (mirrors `blockedAttempts`): the per-agent effective
|
|
112
|
+
* chain heads recorded by the T2 inject path (source `authorized` |
|
|
113
|
+
* `injected`) for the plugin applied to `ctx` (spec D4 / T2 M3). Not part
|
|
114
|
+
* of the plugin's public surface; lets tests and the T5 gateway projection
|
|
115
|
+
* read the head without reaching into the closure. `undefined` when no
|
|
116
|
+
* plugin is applied.
|
|
117
|
+
*/
|
|
118
|
+
export declare function chainHeads(ctx: Context): ReadonlyMap<string, EffectiveChainHead> | undefined;
|
|
99
119
|
/**
|
|
100
120
|
* Count durable `llm/retry` events for the current (turn, step, provider) in
|
|
101
121
|
* **always mode** (ADR-2; spec §2 clause 5). Normal-mode retries belong to
|
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";
|
|
@@ -695,6 +694,98 @@ function firstExactCandidate(all, wildcard) {
|
|
|
695
694
|
for (let index = 0; index < all.length; index += 1) if (!wildcard[index]) return all[index];
|
|
696
695
|
}
|
|
697
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
|
|
698
789
|
//#region .build/host/cooldown.js
|
|
699
790
|
var CooldownStore = class {
|
|
700
791
|
entries = new Map();
|
|
@@ -839,8 +930,29 @@ var FallbackStateStore = class {
|
|
|
839
930
|
}
|
|
840
931
|
};
|
|
841
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
|
|
842
954
|
//#region .build/host/gateway.js
|
|
843
|
-
const FALLBACKS_SETTINGS_NAMESPACE =
|
|
955
|
+
const FALLBACKS_SETTINGS_NAMESPACE = "fallbacks";
|
|
844
956
|
const CONFIG_KEYS = {
|
|
845
957
|
enabled: true,
|
|
846
958
|
triggerCodes: true,
|
|
@@ -874,10 +986,12 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
874
986
|
bridge;
|
|
875
987
|
seeds;
|
|
876
988
|
settings;
|
|
877
|
-
|
|
989
|
+
subagentPolicy;
|
|
990
|
+
constructor(ctx, bridge, seeds, subagentPolicy) {
|
|
878
991
|
super(ctx, "fallbacks");
|
|
879
992
|
this.bridge = bridge;
|
|
880
993
|
this.seeds = seeds;
|
|
994
|
+
this.subagentPolicy = subagentPolicy;
|
|
881
995
|
ctx.inject(["settings"], (sctx) => {
|
|
882
996
|
this.settings = sctx.settings;
|
|
883
997
|
return () => {
|
|
@@ -923,11 +1037,14 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
923
1037
|
}
|
|
924
1038
|
readResult() {
|
|
925
1039
|
const source = this.bridge.source();
|
|
926
|
-
|
|
1040
|
+
const result = {
|
|
927
1041
|
config: this.readConfig(source),
|
|
928
1042
|
legacyKeys: detectLegacyKeys(source),
|
|
929
1043
|
seeds: this.seeds.wireStatus(this.seedsIo())
|
|
930
1044
|
};
|
|
1045
|
+
const projected = projectSubagentPolicy(this.subagentPolicy);
|
|
1046
|
+
if (projected !== void 0) result.subagentPolicy = projected;
|
|
1047
|
+
return result;
|
|
931
1048
|
}
|
|
932
1049
|
seedsIo() {
|
|
933
1050
|
const settings = this.settings;
|
|
@@ -940,6 +1057,41 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
940
1057
|
};
|
|
941
1058
|
}
|
|
942
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
|
+
}
|
|
943
1095
|
function normalizeRoles(value) {
|
|
944
1096
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
945
1097
|
const roles = {};
|
|
@@ -2040,6 +2192,15 @@ var FallbacksChainAdapter = class extends LlmAdapter {
|
|
|
2040
2192
|
name: model
|
|
2041
2193
|
};
|
|
2042
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
|
+
}
|
|
2043
2204
|
stream(options) {
|
|
2044
2205
|
const config = this.readConfig();
|
|
2045
2206
|
const now = new Date();
|
|
@@ -2101,6 +2262,19 @@ const stateStores = new WeakMap();
|
|
|
2101
2262
|
function stateStore(ctx) {
|
|
2102
2263
|
return stateStores.get(ctx);
|
|
2103
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
|
+
}
|
|
2104
2278
|
async function makeModelExists(ctx, providers) {
|
|
2105
2279
|
const llm = ctx.get("llm");
|
|
2106
2280
|
if (llm === void 0 || typeof llm.listModels !== "function") return () => true;
|
|
@@ -2117,7 +2291,7 @@ async function makeModelExists(ctx, providers) {
|
|
|
2117
2291
|
}
|
|
2118
2292
|
function countRetryEvents(session, turn, step, provider) {
|
|
2119
2293
|
let count = 0;
|
|
2120
|
-
const events = session.
|
|
2294
|
+
const events = session.snapshotEvents();
|
|
2121
2295
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
2122
2296
|
const event = events[index];
|
|
2123
2297
|
const data = event.data;
|
|
@@ -2137,12 +2311,49 @@ function slotWinnerKey(winner) {
|
|
|
2137
2311
|
if (winner === "all-day") return "all-day";
|
|
2138
2312
|
return winner.kind === "preset" ? `preset:${winner.preset}` : `custom:${winner.start}-${winner.end}`;
|
|
2139
2313
|
}
|
|
2140
|
-
function overrideConfig(seed, to) {
|
|
2141
|
-
|
|
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;
|
|
2142
2349
|
return {
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2350
|
+
requestHeader: agent.session?.requestHeader()?.config,
|
|
2351
|
+
events: agent.session?.snapshotEvents(),
|
|
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)
|
|
2146
2357
|
};
|
|
2147
2358
|
}
|
|
2148
2359
|
function apply(ctx, config = defaultFallbacksConfig) {
|
|
@@ -2191,21 +2402,40 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2191
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);
|
|
2192
2403
|
let roleIds = new Map(entry.roles.list.map((role) => [role.id.trim(), role.id]));
|
|
2193
2404
|
let hasChains = entry.rootChain.length > 0 || entry.roles.list.some((role) => (role.chain?.length ?? 0) > 0);
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
+
});
|
|
2205
2418
|
});
|
|
2206
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);
|
|
2207
2424
|
try {
|
|
2208
|
-
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
|
+
});
|
|
2209
2439
|
} catch (error) {
|
|
2210
2440
|
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
2211
2441
|
ctx.logger("llm-fallbacks").debug("fallbacks gateway already registered — no gateway on this fiber (multi-fiber dedupe)");
|
|
@@ -2223,6 +2453,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2223
2453
|
stateStores.set(ctx, states);
|
|
2224
2454
|
const dispatchInjected = new Set();
|
|
2225
2455
|
const slotWinners = new Map();
|
|
2456
|
+
const lastKnownPolicySettings = new Map();
|
|
2226
2457
|
async function decide(agent, turn, step, current, reason, state) {
|
|
2227
2458
|
const config = source();
|
|
2228
2459
|
if (state !== void 0) {
|
|
@@ -2241,8 +2472,40 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2241
2472
|
cooldown,
|
|
2242
2473
|
failed
|
|
2243
2474
|
}), modelExists);
|
|
2244
|
-
const
|
|
2245
|
-
|
|
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.snapshotEvents()), 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;
|
|
2246
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);
|
|
2247
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, {
|
|
2248
2511
|
current,
|
|
@@ -2329,21 +2592,54 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2329
2592
|
if (config.enabled && hasChains && agent.session?.header?.origin === "subagent" && !dispatchInjected.has(agent.id)) {
|
|
2330
2593
|
dispatchInjected.add(agent.id);
|
|
2331
2594
|
try {
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2595
|
+
const settingsRead = readGuardedPolicySettings(ctx, agent.id, lastKnownPolicySettings, logger.warn);
|
|
2596
|
+
const policy = settingsRead.ok ? effectivePolicy(readSessionPolicyEvent(agent.session.snapshotEvents()), 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
|
+
}
|
|
2347
2643
|
}
|
|
2348
2644
|
}
|
|
2349
2645
|
} catch (error) {
|
|
@@ -2377,6 +2673,9 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2377
2673
|
states.delete(agent.id);
|
|
2378
2674
|
dispatchInjected.delete(agent.id);
|
|
2379
2675
|
slotWinners.delete(agent.id);
|
|
2676
|
+
blockedAttemptMap.delete(agent.id);
|
|
2677
|
+
chainHeadMap.delete(agent.id);
|
|
2678
|
+
lastKnownPolicySettings.delete(agent.id);
|
|
2380
2679
|
});
|
|
2381
2680
|
ctx.on("session/event", (session, event) => {
|
|
2382
2681
|
if (event.type !== "assistant/message") return;
|
|
@@ -2392,6 +2691,9 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2392
2691
|
states.clear();
|
|
2393
2692
|
dispatchInjected.clear();
|
|
2394
2693
|
slotWinners.clear();
|
|
2694
|
+
blockedAttemptMap.clear();
|
|
2695
|
+
chainHeadMap.clear();
|
|
2696
|
+
lastKnownPolicySettings.clear();
|
|
2395
2697
|
}, "llm-fallbacks: clear per-agent state");
|
|
2396
2698
|
const fallbacksCommandController = {
|
|
2397
2699
|
getSnapshot(agent) {
|
|
@@ -2405,7 +2707,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2405
2707
|
role,
|
|
2406
2708
|
...resolveChainForDiagnostic(config.roles.list, config.rootChain, role, logger.warn),
|
|
2407
2709
|
slot: resolveSlotState(config, new Date(), config.tz ?? "Asia/Shanghai"),
|
|
2408
|
-
switches: recentFallbacksSwitches(agent.session.
|
|
2710
|
+
switches: recentFallbacksSwitches(agent.session.snapshotEvents(), 5),
|
|
2409
2711
|
cooldown: state === void 0 ? [] : recovery === "half-open" ? [...state.cooldown.snapshot(), ...state.recovery.halfOpenEntries().map((entry) => ({
|
|
2410
2712
|
key: entry.key,
|
|
2411
2713
|
untilEpochMs: entry.untilEpochMs,
|
|
@@ -2469,4 +2771,4 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2469
2771
|
});
|
|
2470
2772
|
}
|
|
2471
2773
|
//#endregion
|
|
2472
|
-
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 };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouteChanged effort rule for config overrides (plan dsh-012-subagent-routing
|
|
3
|
+
* T4; spec D3).
|
|
4
|
+
*
|
|
5
|
+
* Pure, host-free module: plain seed config plus a target route in, the
|
|
6
|
+
* overridden config out. The effort handling mirrors the upstream
|
|
7
|
+
* `resolveChildAgentOptions` rule (deepseek-harness
|
|
8
|
+
* `subagent/src/child-agent.ts:117`): a changed provider+model route drops the
|
|
9
|
+
* seed's route-owned `reasoningEffort` so the selected model resolves its own
|
|
10
|
+
* default, unless an effort is explicitly named for the override; an
|
|
11
|
+
* unchanged route keeps the seed effort. Policy-independent by design — the
|
|
12
|
+
* same rule applies on every override path whether the subagent routing
|
|
13
|
+
* policy is on or off (spec D3). Never throws.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-llm-fallbacks/override
|
|
16
|
+
*/
|
|
17
|
+
import type { LlmCallConfig } from '@deepseek-ai/dsh-llm';
|
|
18
|
+
/** Reasoning-effort value carried by `LlmCallConfig.reasoningEffort`. */
|
|
19
|
+
export type LlmReasoningEffort = NonNullable<LlmCallConfig['reasoningEffort']>;
|
|
20
|
+
/**
|
|
21
|
+
* Override a request config with a target route under the upstream
|
|
22
|
+
* routeChanged effort rule (spec D3):
|
|
23
|
+
*
|
|
24
|
+
* - An explicitly named effort always survives — it wins over the seed effort
|
|
25
|
+
* on either route (upstream semantics: the request overrides the parent).
|
|
26
|
+
* - Same provider+model route → the seed `reasoningEffort` is preserved.
|
|
27
|
+
* - Route changed without an explicit effort → the seed effort is dropped
|
|
28
|
+
* (never carry a stale effort into a different provider/model route).
|
|
29
|
+
*
|
|
30
|
+
* @param seed - the caller's current request config.
|
|
31
|
+
* @param to - the effective target route.
|
|
32
|
+
* @param explicitEffort - an effort explicitly named for this override.
|
|
33
|
+
* @returns a fresh config routed to `to` with the effort rule applied; never
|
|
34
|
+
* mutates `seed`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function overrideConfigWithRouteRule(seed: LlmCallConfig, to: {
|
|
37
|
+
provider: string;
|
|
38
|
+
model: string;
|
|
39
|
+
}, explicitEffort?: LlmReasoningEffort): LlmCallConfig;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route identity and allowlist intersection on resolved candidates (plan
|
|
3
|
+
* dsh-012-subagent-routing T2; spec D1 ∩ D2).
|
|
4
|
+
*
|
|
5
|
+
* Pure, host-free module: plain route data in, results out. Route identity is
|
|
6
|
+
* the EXACT provider string plus the EXACT model string — no aliasing, no
|
|
7
|
+
* case-folding, no catalog-id substitution (spec Terms). Intersections run on
|
|
8
|
+
* RESOLVED candidates (concrete provider+model after the chain candidate
|
|
9
|
+
* pipeline); this module never resolves chains or expands wildcards — the
|
|
10
|
+
* caller passes already-resolved candidates in.
|
|
11
|
+
*
|
|
12
|
+
* Fail-closed direction: an empty allowlist contains nothing and an empty
|
|
13
|
+
* intersection yields `undefined` — the caller must skip the
|
|
14
|
+
* plugin-originated route rather than fall back to an unproven one (spec D1).
|
|
15
|
+
* Never throws.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-llm-fallbacks/route-allowlist
|
|
18
|
+
*/
|
|
19
|
+
import type { PolicyRoute } from './subagent-policy.ts';
|
|
20
|
+
/** One resolved chain candidate: a selector anchored to a concrete model id. */
|
|
21
|
+
export type ResolvedCandidate = {
|
|
22
|
+
readonly provider: string;
|
|
23
|
+
readonly model?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Normalize resolved chain candidates into detached plain routes. Candidates
|
|
27
|
+
* without a concrete model id are dropped (they are not routes); every
|
|
28
|
+
* surviving entry is a fresh object, so the result never aliases the
|
|
29
|
+
* caller's candidate list.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolvedRoutes(candidates: readonly ResolvedCandidate[]): PolicyRoute[];
|
|
32
|
+
/**
|
|
33
|
+
* Whether the route is on the allowlist under exact route identity: both the
|
|
34
|
+
* provider and the model strings must equal an allowed route exactly.
|
|
35
|
+
*/
|
|
36
|
+
export declare function routeOnAllowlist(route: PolicyRoute, allowedModels: readonly PolicyRoute[]): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The first resolved candidate that is on the allowlist, preserving the
|
|
39
|
+
* candidate order (the chain's first in-allowlist entry wins), or `undefined`
|
|
40
|
+
* when the intersection is empty — the caller then skips the inject / switch
|
|
41
|
+
* and the host seed stands (spec D1 ∩ D2).
|
|
42
|
+
*/
|
|
43
|
+
export declare function firstAllowedCandidate(resolved: readonly PolicyRoute[], allowedModels: readonly PolicyRoute[]): PolicyRoute | undefined;
|