dsh-model-switch 0.4.24 → 0.4.26

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/lib/compaction.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { i as sessionProviderOf, r as resolveCapabilityRoute } from "./session-route-BjNPlliu.js";
1
2
  //#region lib/types/compaction.js
2
3
  /** Official compaction row this module supersedes. */
3
4
  const OFFICIAL_COMPACTION_ROW = "compaction-basic";
@@ -81,6 +82,100 @@ function currentCompactionRoute(ctx) {
81
82
  return compactionRouteOf(settings);
82
83
  }
83
84
  /**
85
+ * Resolve the compaction summarization route for one session id.
86
+ *
87
+ * Looks up the session from `ctx.sessions` when available, derives its current
88
+ * provider (`pending` selection preferred over `lastUsed`, then logged routes),
89
+ * and resolves the provider group's compaction pair before falling back to the
90
+ * deployment-wide compaction pair.
91
+ * @param ctx - this row's context.
92
+ * @param sessionId - the session id carried on `GenerateOptions.sessionId`.
93
+ * @returns The derived compaction route, or undefined for the engine's own route.
94
+ */
95
+ function resolveSessionCompactionRoute(ctx, sessionId) {
96
+ if (typeof sessionId !== "string" || sessionId.trim() === "") return void 0;
97
+ let settings;
98
+ try {
99
+ settings = ctx.get("modelSwitch", false)?.currentSettings();
100
+ } catch {
101
+ settings = void 0;
102
+ }
103
+ if (!settings) return void 0;
104
+ let session;
105
+ try {
106
+ session = ctx.get("sessions", false)?.get?.(sessionId);
107
+ } catch {
108
+ session = void 0;
109
+ }
110
+ const provider = sessionProviderOf(ctx, session);
111
+ return resolveCapabilityRoute(settings, provider, "compaction");
112
+ }
113
+ /**
114
+ * Wrap the real llm service so `purpose === 'compaction'` calls resolve their
115
+ * summarization route per session while keeping `compaction/summary` truthful.
116
+ *
117
+ * `summarizeWithLlm` builds a mutable `options` literal with `purpose: 'compaction'`
118
+ * and `sessionId: agent.session.id`, calls `ctx.llm.stream(options)`, and then
119
+ * returns `{ provider: options.provider, model: options.model }` to `commitCompactionBody`.
120
+ * Mutating `options.provider` and `options.model` in place before delegating to
121
+ * `target.stream(options)` ensures the durable `compaction/summary` event
122
+ * records the exact provider/model that wrote the summary. All other members
123
+ * (such as `resolveModelInfo`) pass through bound to `real`.
124
+ * @param real - the host's `ctx.llm`.
125
+ * @param resolve - per-session route resolver; undefined keeps `options` untouched.
126
+ * @param onError - optional error sink when the resolver throws.
127
+ * @returns A transparent Proxy over `real`.
128
+ */
129
+ function compactionLlm(real, resolve, onError) {
130
+ return new Proxy(real, { get(target, property, _receiver) {
131
+ if (property === "stream") return (options) => {
132
+ if (options.purpose === "compaction" && typeof options.sessionId === "string" && options.sessionId.trim() !== "") {
133
+ let route;
134
+ try {
135
+ route = resolve(options.sessionId);
136
+ } catch (cause) {
137
+ onError?.(cause);
138
+ route = void 0;
139
+ }
140
+ if (route !== void 0) {
141
+ if (Object.isFrozen(options)) return target.stream({
142
+ ...options,
143
+ provider: route.provider,
144
+ model: route.model
145
+ });
146
+ options.provider = route.provider;
147
+ options.model = route.model;
148
+ }
149
+ }
150
+ return target.stream(options);
151
+ };
152
+ const value = Reflect.get(target, property, target);
153
+ return typeof value === "function" ? value.bind(target) : value;
154
+ } });
155
+ }
156
+ /**
157
+ * Build the child context the compaction engine mounts under.
158
+ *
159
+ * When `ctx.llm` is present, shadows it with {@link compactionLlm} on an
160
+ * extended context so the child engine fiber resolves the per-session wrapper
161
+ * while registering `ctx.compaction` on the shared context tree.
162
+ * @param ctx - this row's context.
163
+ * @returns A context shadowing `llm` when available, else `ctx`.
164
+ */
165
+ function compactionScope(ctx) {
166
+ let real;
167
+ try {
168
+ real = ctx.get("llm", false);
169
+ } catch {
170
+ real = void 0;
171
+ }
172
+ if (real === void 0) return ctx;
173
+ return ctx.extend({ llm: compactionLlm(real, (sessionId) => resolveSessionCompactionRoute(ctx, sessionId), (cause) => {
174
+ ctx.logger.error("model-switch: failed to resolve per-session compaction route; keeping the engine route");
175
+ ctx.logger.error(cause);
176
+ }) });
177
+ }
178
+ /**
84
179
  * Whether the compaction capability is actually served from this context.
85
180
  *
86
181
  * Awaiting a plugin fiber settles even when the plugin's injections were never
@@ -116,7 +211,7 @@ async function mountEngine(ctx, config, label) {
116
211
  }
117
212
  let fiber;
118
213
  try {
119
- fiber = ctx.plugin(loaded.default, config);
214
+ fiber = compactionScope(ctx).plugin(loaded.default, config);
120
215
  await fiber;
121
216
  } catch (cause) {
122
217
  if (fiber) try {
@@ -235,4 +330,4 @@ async function apply(ctx) {
235
330
  await scheduleReconcile(ctx, state, false);
236
331
  }
237
332
  //#endregion
238
- export { apply, compactionRouteOf, composeCompactionConfig, engineActive, forwardedConfig, inject, name, officialCompactionEntry };
333
+ export { apply, compactionLlm, compactionRouteOf, compactionScope, composeCompactionConfig, engineActive, forwardedConfig, inject, name, officialCompactionEntry, resolveSessionCompactionRoute };
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as normalizeProviderGroups } from "./provider-group-CsZi7H29.js";
1
+ import { n as normalizeProviderGroups } from "./provider-group-CwadImhH.js";
2
2
  import { ModelSwitchAdapterRegistry } from "./adapter-registry.js";
3
3
  import { createRequire } from "node:module";
4
4
  import z from "@deepseek-ai/schemastery";
@@ -10,9 +10,6 @@ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
10
10
  import { Config as Config$1, DEEPSEEK_DEFAULT_API_VERSION, DEEPSEEK_DEFAULT_BASE_URL, DEEPSEEK_DEFAULT_MAX_TOKENS, DEEPSEEK_DEFAULT_MAX_USES, DEEPSEEK_DEFAULT_MODEL, DEEPSEEK_PROVIDER_ID, DeepSeekSearchProvider, WEB_SEARCH_DEEPSEEK_SETTINGS_NAMESPACE } from "@deepseek-ai/dsh-web-search-deepseek";
11
11
  import { readFileSync } from "node:fs";
12
12
  import { dirname, join } from "node:path";
13
- import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
14
- import { boundContextSummary, createMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
15
- import { canonicalHeader } from "@deepseek-ai/dsh-session";
16
13
  //#region lib/types/host-settings.js
17
14
  /**
18
15
  * Annotated rather than inferred: the nested entry schema's inferred type
@@ -28,7 +25,6 @@ const Config = z.object({
28
25
  searchModel: z.string().volatile(),
29
26
  imageProvider: z.string().volatile(),
30
27
  imageModel: z.string().volatile(),
31
- compactOnSwitch: z.boolean().default(true).volatile(),
32
28
  autoReviewProvider: z.string().volatile(),
33
29
  autoReviewModel: z.string().volatile(),
34
30
  compactProvider: z.string().volatile(),
@@ -72,7 +68,6 @@ function readConfig(config) {
72
68
  ...searchModel === void 0 ? {} : { searchModel },
73
69
  ...imageProvider === void 0 ? {} : { imageProvider },
74
70
  ...imageModel === void 0 ? {} : { imageModel },
75
- compactOnSwitch: config.compactOnSwitch.get(),
76
71
  ...autoReviewProvider === void 0 || autoReviewProvider === "" ? {} : { autoReviewProvider },
77
72
  ...autoReviewModel === void 0 || autoReviewModel === "" ? {} : { autoReviewModel },
78
73
  ...compactProvider === void 0 || compactProvider === "" ? {} : { compactProvider },
@@ -617,300 +612,6 @@ function allowDshRuntime(logger, pluginName, candidates) {
617
612
  return shouldMountDshRuntime(logger, pluginName, version, new Set(Object.entries(compatibility?.dshReleases ?? {}).filter(([, status]) => status === "compatible" || status === "verified").map(([release]) => release)), compatibility?.blocklist);
618
613
  }
619
614
  //#endregion
620
- //#region lib/types/switch-compact.js
621
- /** Send-time context check and optional old-model compaction before a switched request. */
622
- /** Fraction of the target window reserved for output plus estimator error. */
623
- const OUTPUT_RESERVE = .2;
624
- /** Extra tokens on top of the ratio; the meter heuristic is not exact. */
625
- const ESTIMATE_MARGIN = 64;
626
- /**
627
- * Compare two fully specified model routes.
628
- * @param left - first route, if one was resolved.
629
- * @param right - second route, if one was resolved.
630
- * @returns Whether both routes identify the same provider and model.
631
- */
632
- function sameSwitchRoute(left, right) {
633
- return left !== void 0 && right !== void 0 && left.provider === right.provider && left.model === right.model;
634
- }
635
- /**
636
- * Decide whether a changed, complete route needs send-time context protection.
637
- * @param enabled - whether the user enabled send-time protection.
638
- * @param previous - route used by the preceding request.
639
- * @param selected - route assembled for the pending request.
640
- * @returns Whether the pending request changes its provider or model.
641
- */
642
- function shouldProtectSwitch(enabled, previous, selected) {
643
- if (!enabled) return false;
644
- if (previous === void 0 || selected === void 0) return false;
645
- return !sameSwitchRoute(previous, selected);
646
- }
647
- /**
648
- * Input budget for the target window. Reserves output (maxTokens or 20%) plus a
649
- * fixed estimator margin. `undefined` means the window cannot be used.
650
- * @param contextWindow - adapter-owned combined request/response capacity.
651
- * @param maxTokens - optional generation cap from the live agent options.
652
- * @returns Tokens available to the request input, or undefined for an invalid window.
653
- */
654
- function switchInputBudget(contextWindow, maxTokens) {
655
- if (!(contextWindow > 0) || !Number.isFinite(contextWindow)) return void 0;
656
- const reserve = Math.max(Math.ceil(contextWindow * OUTPUT_RESERVE), maxTokens ?? 0) + ESTIMATE_MARGIN;
657
- return Math.max(0, Math.floor(contextWindow - reserve));
658
- }
659
- /**
660
- * Determine whether an estimated input exceeds its target route budget.
661
- * @param tokens - estimated input tokens.
662
- * @param budget - input budget for the target window.
663
- * @returns Whether the budget is unusable or the estimate is too large.
664
- */
665
- function overSwitchBudget(tokens, budget) {
666
- return !(budget > 0) || tokens > budget;
667
- }
668
- /**
669
- * Build one transcript-visible Model Switch progress notice.
670
- * @param text - localized notice text shown in the transcript.
671
- * @param summary - bounded event summary for durable session metadata.
672
- * @returns A plugin-authored user message for the session log.
673
- */
674
- function switchNotice(text, summary) {
675
- return createUserMessage({
676
- content: [{
677
- type: "text",
678
- text
679
- }],
680
- source: {
681
- kind: "model-switch",
682
- form: "notice",
683
- summary: boundContextSummary(summary)
684
- }
685
- });
686
- }
687
- /**
688
- * Extract a complete route from partial request configuration.
689
- * @param value - candidate provider and model values.
690
- * @returns A complete route, or undefined when either identifier is absent.
691
- */
692
- function routeOf(value) {
693
- if (value === void 0 || value.provider === void 0 || value.provider.length === 0 || value.model === void 0 || value.model.length === 0) return void 0;
694
- return {
695
- provider: value.provider,
696
- model: value.model
697
- };
698
- }
699
- /**
700
- * Install send-time switch protection on the host context.
701
- * @param ctx - host context that owns agent-loop, llm, and tokenMeter.
702
- * @param settings - live Model Switch settings; `compactOnSwitch !== false` is on.
703
- * @param roleOf - provider-role lookup used to exclude native Agent runtimes.
704
- */
705
- function installSwitchCompaction(ctx, settings, roleOf = () => void 0) {
706
- const assemblies = /* @__PURE__ */ new WeakMap();
707
- let stopGate;
708
- ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
709
- const assembled = await next();
710
- if (context.agent !== void 0) {
711
- assemblies.set(context.agent, assembled);
712
- const previous = stopGate;
713
- let replacement;
714
- try {
715
- replacement = installGate();
716
- } catch (cause) {
717
- ctx.logger.warn("model-switch: could not refresh the switch-time compaction guard; the existing guard stays in place");
718
- ctx.logger.warn(cause);
719
- return assembled;
720
- }
721
- stopGate = replacement;
722
- previous?.();
723
- }
724
- return assembled;
725
- });
726
- function installGate() {
727
- return ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
728
- const decision = await next();
729
- if (decision.kind !== "enter" || signal.aborted) return decision;
730
- const enabled = settings().compactOnSwitch !== false;
731
- const previous = routeOf(agent.session.requestHeader()?.config);
732
- const assembly = assemblies.get(agent);
733
- const selected = routeOf(assembly === void 0 ? void 0 : {
734
- ...assembly.variables.provider === void 0 ? {} : { provider: assembly.variables.provider },
735
- ...assembly.variables.model === void 0 ? {} : { model: assembly.variables.model }
736
- });
737
- if (!shouldProtectSwitch(enabled, previous, selected) || previous === void 0 || selected === void 0) return decision;
738
- if (roleOf(previous.provider) === "agent" || roleOf(selected.provider) === "agent") return decision;
739
- return protectSwitch(ctx, agent, previous, selected, assembly, decision, messages, signal);
740
- }, { prepend: true });
741
- }
742
- }
743
- async function protectSwitch(ctx, agent, previous, selected, assembly, decision, claimed, signal) {
744
- const session = agent.session;
745
- const notice = (text, summary) => {
746
- session.append("user/message", switchNotice(text, summary), { surfaceOp: "append" });
747
- };
748
- const reject = (reason, summary, compacted) => {
749
- notice(compacted ? `无法继续本次模型切换:压缩已发生,但请求未继续(${reason})。` : `无法继续本次模型切换:${reason}`, summary);
750
- retainUserClaims(session, claimed);
751
- return { kind: "reject" };
752
- };
753
- notice("正在检查上下文…", "checking context");
754
- if (assembly === void 0) return reject("无法确定本次请求组装的目标模型。", "switch blocked: no assembled target", false);
755
- const llm = ctx.get("llm", false);
756
- if (llm?.resolveModelInfo === void 0) return reject("无法确定目标模型的上下文窗口。", "switch blocked: unknown window", false);
757
- let window;
758
- try {
759
- const value = (await llm.resolveModelInfo(selected.provider, selected.model, signal)).context?.contextWindow;
760
- window = typeof value === "number" && value > 0 ? value : void 0;
761
- } catch {
762
- window = void 0;
763
- }
764
- if (signal.aborted) return reject("已取消。", "switch blocked: cancelled", false);
765
- if (window === void 0) return reject("无法确定目标模型的上下文窗口。", "switch blocked: unknown window", false);
766
- const meter = ctx.get("tokenMeter", false);
767
- if (meter === void 0 || typeof meter.measure !== "function" || typeof meter.estimateMessage !== "function") return reject("无法估算待发上下文(缺少 token 计量)。", "switch blocked: no token meter", false);
768
- const header = targetHeader(selected, assembly);
769
- const previousHeader = session.requestHeader();
770
- let pressure;
771
- try {
772
- pressure = estimateSwitchPressure(meter, session, header, assembly, decision.messages, previousHeader);
773
- } catch (error) {
774
- return reject(`无法估算待发上下文(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: estimate failed", false);
775
- }
776
- const budget = agent.options.maxTokens === void 0 ? switchInputBudget(window) : switchInputBudget(window, agent.options.maxTokens);
777
- if (budget === void 0) return reject("无法确定目标模型的上下文窗口。", "switch blocked: unknown window", false);
778
- const passed = switchNotice("上下文容量检查通过,无需压缩。", "context check passed");
779
- const completed = switchNotice("压缩完成,复查通过。", "compacted");
780
- if (!overSwitchBudget(pressure + meter.estimateMessage(passed), budget)) {
781
- session.append("user/message", passed, { surfaceOp: "append" });
782
- return decision;
783
- }
784
- let fixedPressure;
785
- try {
786
- fixedPressure = estimateFixedInput(meter, session, header, assembly, decision.messages);
787
- } catch (error) {
788
- return reject(`无法估算待发上下文(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: estimate failed", false);
789
- }
790
- if (overSwitchBudget(fixedPressure + meter.estimateMessage(completed), budget)) return reject("新消息或固定请求内容已超过目标模型可用容量。", "switch blocked: fixed input oversized", false);
791
- notice("当前上下文超过目标模型可用容量,正在使用旧模型压缩…", "compacting with previous model");
792
- const compactionGeneration = session.surface.replaceGeneration;
793
- let compacted = false;
794
- try {
795
- const result = await compactWithPrevious(ctx, agent, previous, signal);
796
- compacted = result !== "none" || session.surface.replaceGeneration > compactionGeneration;
797
- if (result === "none") return reject("没有可压缩的历史。", "switch blocked: nothing to compact", compacted);
798
- } catch (error) {
799
- compacted = compacted || session.surface.replaceGeneration > compactionGeneration;
800
- if (signal.aborted) return reject("已取消。", "switch blocked: cancelled", compacted);
801
- return reject(`压缩失败(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: compact failed", compacted);
802
- }
803
- if (signal.aborted) return reject("已取消。", "switch blocked: cancelled", compacted);
804
- let again;
805
- try {
806
- again = estimateSwitchPressure(meter, session, header, assembly, decision.messages);
807
- } catch (error) {
808
- return reject(`压缩后无法复查(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: remeasure failed", compacted);
809
- }
810
- if (overSwitchBudget(again + meter.estimateMessage(completed), budget)) return reject("压缩后仍超过目标模型可用容量。", "switch blocked: still over budget", compacted);
811
- session.append("user/message", completed, { surfaceOp: "append" });
812
- return decision;
813
- }
814
- function targetHeader(selected, assembly) {
815
- return canonicalHeader({
816
- config: {
817
- provider: selected.provider,
818
- model: selected.model
819
- },
820
- ...assembly.tools.length > 0 ? { tools: [...assembly.tools] } : {}
821
- });
822
- }
823
- /**
824
- * Next-request input under the target route. Uses the committed surface priced
825
- * for the target (including image tokens), then adds still-pending enter
826
- * messages and the prospective system prompt while dropping already-priced
827
- * system nodes so they are not counted twice.
828
- *
829
- * The target envelope has no provider usage of its own, so the meter can only
830
- * price it with the chars-per-token heuristic. That heuristic is unconservative
831
- * for dense text (base64 payloads, minified sources, CJK). The previous
832
- * envelope holds the provider's reported usage for the same surface, so the
833
- * switch takes the larger of the two prices: the anchor keeps the check
834
- * truthful exactly where a switch makes the heuristic weakest.
835
- *
836
- * @param meter - replay token meter.
837
- * @param session - session being measured.
838
- * @param header - target request envelope.
839
- * @param assembly - prospective prompt assembly.
840
- * @param pending - messages the pre-step is about to enter.
841
- * @param previousHeader - envelope of the last request, when the session has one.
842
- * @returns Estimated next-request input tokens.
843
- */
844
- function estimateSwitchPressure(meter, session, header, assembly, pending, previousHeader) {
845
- const estimate = routePressure(meter, session, header, assembly, pending);
846
- if (previousHeader === void 0) return estimate;
847
- return Math.max(estimate, routePressure(meter, session, previousHeader, assembly, pending));
848
- }
849
- /**
850
- * Price one request envelope against the committed surface.
851
- * @param meter - replay token meter.
852
- * @param session - session being measured.
853
- * @param header - request envelope to price.
854
- * @param assembly - prospective prompt assembly.
855
- * @param pending - messages the pre-step is about to enter.
856
- * @returns Estimated input tokens for that envelope.
857
- */
858
- function routePressure(meter, session, header, assembly, pending) {
859
- const measured = meter.measure(session, header);
860
- let systemTokens = 0;
861
- for (const node of measured.nodes) if (session.eventAt(node.seq)?.type === "system/message") systemTokens += node.tokens;
862
- return measured.totalTokens - systemTokens + systemPromptTokens(meter, assembly) + pendingTokens(meter, pending);
863
- }
864
- function estimateFixedInput(meter, session, header, assembly, pending) {
865
- const measured = meter.measure(session, header);
866
- return measured.totalTokens - measured.surfaceTokens + systemPromptTokens(meter, assembly) + pendingTokens(meter, pending);
867
- }
868
- function pendingTokens(meter, messages) {
869
- let total = 0;
870
- for (const message of messages) {
871
- const tokens = meter.estimateMessage(message);
872
- if (!(tokens >= 0) || !Number.isFinite(tokens)) throw new Error("pending message token estimate is not a number");
873
- total += tokens;
874
- }
875
- return total;
876
- }
877
- function systemPromptTokens(meter, assembly) {
878
- const text = renderPrompt(assembly);
879
- if (text.length === 0) return 0;
880
- return meter.estimateMessage(createMessage({
881
- role: "system",
882
- content: [{
883
- type: "text",
884
- text
885
- }],
886
- source: { kind: "system-prompt" }
887
- }));
888
- }
889
- function retainUserClaims(session, claimed) {
890
- for (const message of claimed) {
891
- if (message.source.kind !== "user") continue;
892
- session.append("user/message", message, { surfaceOp: "append" });
893
- }
894
- }
895
- async function compactWithPrevious(ctx, agent, previous, signal) {
896
- const isolated = ctx.isolate("compaction");
897
- const Engine = (await import("@deepseek-ai/dsh-compaction-basic")).default;
898
- const fiber = isolated.plugin(Engine, {
899
- auto: false,
900
- summarizationProvider: previous.provider,
901
- summarizationModel: previous.model,
902
- compactionRetries: 0
903
- });
904
- try {
905
- await fiber;
906
- const engine = isolated.get("compaction", false);
907
- if (engine === void 0) throw new Error("private compaction engine is unavailable");
908
- return await engine.compactIfNeeded(agent, "context-overflow", signal) === null ? "none" : "compacted";
909
- } finally {
910
- await fiber.dispose();
911
- }
912
- }
913
- //#endregion
914
615
  //#region lib/types/host-runtime.js
915
616
  /** Host owner for Model Switch settings. */
916
617
  var ModelSwitchRuntime = class extends Service {
@@ -933,7 +634,6 @@ var ModelSwitchRuntime = class extends Service {
933
634
  this.source = () => readConfig(entry);
934
635
  ctx.inject(["settings"], (scope) => scope.effect(() => scope.settings.configure({ auto: false }, ctx.fiber), "Model Switch: disable automatic settings form"));
935
636
  if (!allowDshRuntime(ctx.logger, "dsh-model-switch", ["@deepseek-ai/dsh-agent"])) return;
936
- installSwitchCompaction(ctx, () => this.source(), (provider) => this.adapters.get(provider)?.role);
937
637
  installDeepSeekSearchAdapter(ctx);
938
638
  installModelSwitchSearchProvider(ctx, this);
939
639
  installCapabilitiesRpc(ctx, this.adapters, () => this.capabilities);
@@ -1,20 +1,14 @@
1
1
  //#region lib/types/provider-group.js
2
2
  /**
3
- * Provider configuration groups: per-provider presets for the session-naming
4
- * and subagent routes.
3
+ * Provider configuration groups: per-provider presets for the compaction,
4
+ * auto-review, session-naming, and subagent routes.
5
5
  *
6
6
  * A group is a DERIVED policy, not a write: `src/session-route.ts` resolves the
7
7
  * routes one session should use from its own provider plus this stored table, so
8
8
  * switching a session's provider never rewrites deployment-wide settings and two
9
9
  * sessions on different providers cannot overwrite each other. The field names
10
10
  * below are the flat settings field names a group pins, which keeps the stored
11
- * shape identical to the earlier write-through design.
12
- *
13
- * Legacy entries may still carry `compactProvider`/`compactModel`/
14
- * `autoReviewProvider`/`autoReviewModel`: normalization preserves them so an
15
- * older stored document is never silently rewritten into a different meaning,
16
- * while the consumption side ignores them (a group save from the current UI
17
- * writes '' for every unsupported field, which normalization drops).
11
+ * shape identical across releases.
18
12
  *
19
13
  * All functions here are pure and never throw on malformed storage.
20
14
  */
@@ -1,4 +1,4 @@
1
- import { t as groupOf } from "./provider-group-CsZi7H29.js";
1
+ import { t as groupOf } from "./provider-group-CwadImhH.js";
2
2
  //#region lib/types/session-route.js
3
3
  /**
4
4
  * Per-session capability route derivation.
@@ -0,0 +1,172 @@
1
+ import { t as groupOf } from "./provider-group-CwadImhH.js";
2
+ //#region lib/types/session-route.js
3
+ /**
4
+ * Per-session capability route derivation.
5
+ *
6
+ * Model Switch stores one deployment-wide route per capability (the flat
7
+ * settings fields) plus, per provider, a saved provider group. Nothing is
8
+ * written when a session changes provider: this module DERIVES the route a
9
+ * capability should use for one session from (a) the session's own provider and
10
+ * (b) the stored group table. Two sessions on different providers therefore
11
+ * resolve independently, and no session's actions rewrite another session's
12
+ * route.
13
+ *
14
+ * The session's provider is read from durable, official state in this order:
15
+ * the `modelSelection` session projection (`pending` before `lastUsed`, so a
16
+ * selection the user just made counts), then the latest logged request-context
17
+ * route, then the latest `request/header` route. Every read is optional: a
18
+ * session with no observable provider, an unregistered projection, or a
19
+ * profile without the projection package all resolve to the deployment default,
20
+ * which is exactly the pre-derivation behavior.
21
+ *
22
+ * All functions are pure except the readers, which never throw and never write.
23
+ */
24
+ /** Flat deployment-default fields per capability, in group-storage field names. */
25
+ const CAPABILITY_FIELDS = Object.freeze({
26
+ compaction: {
27
+ provider: "compactProvider",
28
+ model: "compactModel"
29
+ },
30
+ autoReview: {
31
+ provider: "autoReviewProvider",
32
+ model: "autoReviewModel"
33
+ },
34
+ title: {
35
+ provider: "titleProvider",
36
+ model: "titleModel"
37
+ },
38
+ subagent: {
39
+ provider: "subagentProvider",
40
+ model: "subagentModel"
41
+ }
42
+ });
43
+ /** A non-blank string; blank values behave exactly like an absent field. */
44
+ function present(value) {
45
+ return typeof value === "string" && value.trim() !== "";
46
+ }
47
+ /** Read one capability's pair from a flat route record; undefined unless both halves are present. */
48
+ function pairOf(source, fields) {
49
+ if (source === void 0) return void 0;
50
+ const record = source;
51
+ const provider = record[fields.provider];
52
+ const model = record[fields.model];
53
+ if (!present(provider) || !present(model)) return void 0;
54
+ return {
55
+ provider,
56
+ model
57
+ };
58
+ }
59
+ /**
60
+ * The deployment-wide route for one capability: the flat settings pair.
61
+ * @param settings - live Model Switch settings; undefined means none are reachable.
62
+ * @param capability - capability whose flat pair is read.
63
+ * @returns The complete pair, or undefined when the capability stays at its official/derived default.
64
+ */
65
+ function deploymentRoute(settings, capability) {
66
+ return pairOf(settings, CAPABILITY_FIELDS[capability]);
67
+ }
68
+ /**
69
+ * The route a subagent spawn should use for one parent-session provider.
70
+ *
71
+ * A saved group for that provider pins the spawn; otherwise the flat Subagent
72
+ * card pins it, but only in `fixed` mode — `follow-main` IS the official inherit
73
+ * path, expressed by leaving the request untouched rather than by naming it.
74
+ * @param settings - live Model Switch settings.
75
+ * @param provider - the delegating session's provider, when one is observable.
76
+ * @returns The complete route, or undefined for official inherit.
77
+ */
78
+ function subagentRouteOf(settings, provider) {
79
+ const group = groupRoute(settings, provider, "subagent");
80
+ if (group !== void 0) {
81
+ const effort = groupOf(settings?.providerGroups, provider ?? "")?.subagentReasoningEffort;
82
+ return present(effort) ? {
83
+ ...group,
84
+ reasoningEffort: effort
85
+ } : group;
86
+ }
87
+ if (settings?.subagentMode !== "fixed") return void 0;
88
+ const fixed = deploymentRoute(settings, "subagent");
89
+ if (fixed === void 0) return void 0;
90
+ return present(settings.subagentReasoningEffort) ? {
91
+ ...fixed,
92
+ reasoningEffort: settings.subagentReasoningEffort
93
+ } : fixed;
94
+ }
95
+ /**
96
+ * One provider's saved route for one capability.
97
+ * @param settings - live Model Switch settings carrying the group table.
98
+ * @param provider - the session's provider; blank or unknown resolves to undefined.
99
+ * @param capability - capability whose group pin is read.
100
+ * @returns The complete group pair, or undefined when the group pins nothing here.
101
+ */
102
+ function groupRoute(settings, provider, capability) {
103
+ if (!present(provider)) return void 0;
104
+ return pairOf(groupOf(settings?.providerGroups, provider), CAPABILITY_FIELDS[capability]);
105
+ }
106
+ /**
107
+ * The route one capability should use for one session.
108
+ *
109
+ * A saved provider group for the session's provider wins; otherwise the flat
110
+ * deployment default applies. An unknown session provider skips every group, so
111
+ * a session Model Switch cannot attribute keeps the deployment default.
112
+ * @param settings - live Model Switch settings.
113
+ * @param provider - the session's provider, when one is observable.
114
+ * @param capability - capability being resolved.
115
+ * @returns The complete pair, or undefined for the capability's official default.
116
+ */
117
+ function resolveCapabilityRoute(settings, provider, capability) {
118
+ return groupRoute(settings, provider, capability) ?? deploymentRoute(settings, capability);
119
+ }
120
+ /**
121
+ * Read the provider from the durable model-selection projection.
122
+ * @param ctx - context that may expose `sessionProjections`.
123
+ * @param session - the session whose selection is read.
124
+ * @returns The pending provider, else the last-used provider, else undefined.
125
+ */
126
+ function projectionProvider(ctx, session) {
127
+ let state;
128
+ try {
129
+ state = ctx.get("sessionProjections", false)?.stateOf(session, "modelSelection");
130
+ } catch {
131
+ state = void 0;
132
+ }
133
+ if (state === void 0) return void 0;
134
+ if (present(state.pending?.provider)) return state.pending.provider;
135
+ if (present(state.lastUsed?.provider)) return state.lastUsed.provider;
136
+ }
137
+ /**
138
+ * The provider one session is using, from official durable state.
139
+ *
140
+ * Preferred source is the session's own model selection (it covers a switch the
141
+ * client has sent but no request has consumed yet); the logged request-context
142
+ * and request-header routes cover profiles without the projection package and
143
+ * replayed sessions that never emitted a selection.
144
+ * @param ctx - context used for the optional projection read.
145
+ * @param session - the session to resolve; undefined resolves to undefined.
146
+ * @returns The provider, or undefined when nothing durable names one.
147
+ */
148
+ function sessionProviderOf(ctx, session) {
149
+ if (session === void 0) return void 0;
150
+ const selected = projectionProvider(ctx, session);
151
+ if (selected !== void 0) return selected;
152
+ try {
153
+ const context = session.requestContext()?.provider;
154
+ if (present(context)) return context;
155
+ const header = session.requestHeader()?.config.provider;
156
+ if (present(header)) return header;
157
+ } catch {
158
+ return;
159
+ }
160
+ }
161
+ /**
162
+ * The subagent route one exact session's spawns should use.
163
+ * @param ctx - context used for the optional session reads.
164
+ * @param session - the delegating session; undefined keeps official inherit.
165
+ * @param settings - live Model Switch settings.
166
+ * @returns The complete route, or undefined for official inherit.
167
+ */
168
+ function sessionSubagentRouteOf(ctx, session, settings) {
169
+ return subagentRouteOf(settings, sessionProviderOf(ctx, session));
170
+ }
171
+ //#endregion
172
+ export { sessionSubagentRouteOf as a, sessionProviderOf as i, groupRoute as n, resolveCapabilityRoute as r, deploymentRoute as t };