dsh-model-switch 0.4.21 → 0.4.22
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/CHANGELOG.md +8 -0
- package/PRODUCT.md +1 -1
- package/README.md +4 -0
- package/README.zh.md +4 -0
- package/lib/client.js +601 -1
- package/lib/index.js +90 -2
- package/lib/types/client/ModelSwitchSettings.d.ts +3 -0
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client-contract.d.ts +3 -0
- package/lib/types/host-runtime.d.ts +2 -30
- package/lib/types/host-settings.d.ts +11 -29
- package/lib/types/provider-group.d.ts +78 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## v0.4.22
|
|
2
|
+
|
|
3
|
+
One addition: per-provider capability groups — save the compaction, subagent, and review routes together for a provider, and apply that group automatically when the main model's provider changes.
|
|
4
|
+
|
|
5
|
+
- **Provider groups.** New settings card (供应商配置组 / Provider groups) stores one route group per provider in the existing shared `model-switch` namespace (`providerGroups`, an entry array normalized to a provider map) covering exactly the three session-level capability routes — compaction, subagent, and auto review; search and image stay out of scope. Applying a group is a **full replacement** through one ordered write sequence: the pinned pairs are set, pairs the group does not pin are unset as a pair (never a half pair), and `subagentMode` is written last (`fixed` when the subagent pair is pinned — with its effort only when the effort is non-blank, blank meaning the provider default — else `follow-main`), so every unpinned capability reverts to its own default. Two plugin-owned switch surfaces trigger the apply: the composer picker's `directoryFace.select()` and the Main card's `saveMain()`, both gated on an actual provider change; a same-provider switch, a provider without a stored group, or any other switch surface (TUI) writes nothing. The apply rides the public ConfigForm `set`/`unset` onto the existing flat settings fields, so the consumers (`compaction.ts`, `subagent-runtime.ts`, `auto-review.ts`) are untouched, and the effect is profile-level like the fields it writes. Failures never fail the switch or the Main save: the picker logs a warning and still commits, while the Main card surfaces "Main model saved, but applying the provider group failed …". In the editor, a provider's draft seeds from the active routes the first time it is configured, each capability switch seeds from Main when turned on and stores the capability default when off, applying a group overwrites the three standalone cards (including manual edits, stated in the card copy), and **Delete group** removes the provider's entry (clearing the stored field when the last group goes, no-op when nothing is stored).
|
|
6
|
+
- **Storage and typing.** `providerGroups` is a `z.array(entry).default([]).volatile()` field on the exported `Config` (annotated explicitly so declaration emit stays inside this package), coerced per entry with `''` defaults; `normalizeProviderGroups` never throws, accepts both the stored entry array and a hand-written provider→routes record, drops blank fields, unknown fields, and blank/duplicate entries, and treats a literal `__proto__` provider id as data. `readConfig` omits the field when no group exists, keeping the exact empty-config shape unchanged.
|
|
7
|
+
- Verification: `pnpm run check` green (`tsc` + `tsdown`, 52 test files / 386 tests, pack gate); reverse verification — removing the picker's apply wiring turns exactly the adopt test red, green again after restore; lab 3082 smoke: two stored groups (one pinning compaction onto the keyless `deepseek-official` route, one onto a working route) apply on a picker provider switch (all three cards show the group values, switching back restores the previous routes), no-group providers leave the routes untouched, and `/compact` under the keyless pin fails with that route's `MISSING_CREDENTIAL` while the working group summarizes normally.
|
|
8
|
+
|
|
1
9
|
## v0.4.21
|
|
2
10
|
|
|
3
11
|
Two product changes and one investigation: plan-exit auto-compaction is removed entirely; official compaction (`/compact` and official automatic compaction) gains one configurable summarization model; and the "endless automatic compaction" reports are investigated and attributed to the official engine.
|
package/PRODUCT.md
CHANGED
|
@@ -20,7 +20,7 @@ Official `agent-presets` stay in the process. A nested `ctx.plugin(AgentPresets)
|
|
|
20
20
|
|
|
21
21
|
## Settings and lifecycle
|
|
22
22
|
|
|
23
|
-
The settings surface aggregates public Settings namespaces, reads and mutates with optimistic revisions, and saves rows independently. Changing a Main or fixed Subagent model applies only the target catalog row's default effort, so an effort never leaks across providers or models. The Subagent card is a RouteCard with an instant on/off for the Default Subagent route; turning it off keeps the last provider/model on disk. Switch compaction is a B-row in the Send protection group (发送保护), default on, written immediately. The Auto review card is a RouteCard with an instant switch defaulting **off = reviews follow the session model**; turning it on seeds the pinned reviewer route from Main and stays editable. The Compaction card mirrors it: default **off = official default = compaction follows the conversation target model**; turning it on seeds the summarization route from Main and stays editable, and the pinned pair drives `/compact` and official automatic compaction alike. Every registration, watcher, and dynamic tool generation has clean disposal. All visible copy lives in zh/en locale dictionaries.
|
|
23
|
+
The settings surface aggregates public Settings namespaces, reads and mutates with optimistic revisions, and saves rows independently. Changing a Main or fixed Subagent model applies only the target catalog row's default effort, so an effort never leaks across providers or models. The Subagent card is a RouteCard with an instant on/off for the Default Subagent route; turning it off keeps the last provider/model on disk. Switch compaction is a B-row in the Send protection group (发送保护), default on, written immediately. The Auto review card is a RouteCard with an instant switch defaulting **off = reviews follow the session model**; turning it on seeds the pinned reviewer route from Main and stays editable. The Compaction card mirrors it: default **off = official default = compaction follows the conversation target model**; turning it on seeds the summarization route from Main and stays editable, and the pinned pair drives `/compact` and official automatic compaction alike. Provider groups store one compaction/subagent/review route group per provider in the shared `model-switch` namespace and apply it as a full replacement whenever the main model's provider changes through either plugin-owned switch surface (the composer picker or a Main card save); the apply rewrites only the existing flat fields (an unpinned capability reverts to its own default, `subagentMode` last), a same-provider switch or a provider without a stored group writes nothing, the effect is profile-wide rather than per session, and a failed apply never fails the switch or the Main save. Search and image routes are out of scope for groups. Every registration, watcher, and dynamic tool generation has clean disposal. All visible copy lives in zh/en locale dictionaries.
|
|
24
24
|
|
|
25
25
|
## Exclusions
|
|
26
26
|
|
package/README.md
CHANGED
|
@@ -101,6 +101,10 @@ How the route reaches the engine depends on where the deployment runs its agent
|
|
|
101
101
|
|
|
102
102
|
Known issue (upstream; recorded in `docs/seam-gap.md`): while the working set stays above the official engine's pressure threshold, automatic compaction can fire on every step and keep re-attempting ranges whose summaries fail the engine's own "summary must be smaller" guard — a visible loop of automatic compactions that this plugin does not control (verified in a session A/B with the plugin enabled and disabled). Mitigations on the forwarded row config: `auto: false` (keeps `/compact`), a higher `thresholdRatio`, a smaller mounted tool schema, or a pinned compaction model whose summaries are leaner.
|
|
103
103
|
|
|
104
|
+
## Provider groups
|
|
105
|
+
|
|
106
|
+
The "Provider groups" card stores one route group per provider — the compaction, subagent, and review routes saved together — and applies it automatically whenever the main model's **provider** changes, whether through the composer picker or by saving a different provider on the Main card. Applying a group is one full replacement: all three routes are written into the cards above in a single decision, overwriting their current values **including manual edits**, while anything the group does not pin reverts to that capability's default (compaction and review follow the conversation model, subagent follows main). Switching to a provider without a stored group leaves the current routes untouched, and a switch within one provider never rewrites them. The stored effect is profile-wide (the shared settings namespace), not per session, and only these two switch surfaces trigger it — a TUI or other switch leaves the routes alone. In the editor, a provider's draft seeds from the active routes the first time you configure it, each capability row's off state means "use the capability default" and is stored as such, and **Delete group** removes the provider's group entirely (clearing the stored field when the last group goes). Search and image routes are out of scope. A failed apply never fails the switch or the Main save: the picker logs a warning and commits anyway, and the Main card reports "Main model saved, but applying the provider group failed …" while the routes stay as they were.
|
|
107
|
+
|
|
104
108
|
## What Model Switch does not change
|
|
105
109
|
|
|
106
110
|
- `web_fetch` and its configured provider
|
package/README.zh.md
CHANGED
|
@@ -99,6 +99,10 @@ Plan Review 拥有独立于 Main 的执行模型草稿。**确认执行**会先
|
|
|
99
99
|
|
|
100
100
|
已知问题(上游侧,见 `docs/seam-gap.md`):当工作集持续超过官方引擎的压力阈值时,自动压缩可能每步触发,并反复重试摘要过不了官方「摘要必须更小」守卫的小片段——表现为"不停自动压缩",本插件不控制该行为(已在同一会话做过插件开/关 A/B 对照)。可用的缓解(都在转发行配置上):`auto: false`(保留 `/compact`)、调高 `thresholdRatio`、减少挂载的工具 schema,或固定一个摘要更精炼的压缩模型。
|
|
101
101
|
|
|
102
|
+
## 供应商配置组(Provider groups)
|
|
103
|
+
|
|
104
|
+
「供应商配置组」卡片为每个供应商保存一组路由——压缩、子代理、审查三条路由一起存;当**主模型的供应商**发生变化时自动应用,无论变化来自输入框选择器选模型、还是在 Main 卡片上保存成别的供应商。应用是一次完整替换:三条路由一次性写入上方对应卡片,覆盖卡片当前值**(包括手动修改)**;组里没有固定的项回到该能力的默认值(压缩/审查跟随会话模型、子代理跟随主模型)。切换到没有配置组的供应商时现有路由保持不变,同一供应商内部切换不会重写路由。存储是 profile 级的(共享设置命名空间),不按会话生效,且只由这两个切换面触发——TUI 等其他切换不会动路由。编辑器里首次配置某供应商时从当前生效路由种子化;每行能力开关的关闭态表示"用该能力的默认值"并按默认存储;「删除该组」彻底移除该组(删掉最后一组时同时清掉存储字段)。搜索与图片路由不在本功能范围内。应用失败不会让切换或主模型保存失败:选择器侧记录警告并照常完成切换,Main 卡片侧提示"主模型已保存,但供应商组应用失败 …",路由保持原样。
|
|
105
|
+
|
|
102
106
|
## Model Switch 不会改变什么
|
|
103
107
|
|
|
104
108
|
- `web_fetch` 及其配置的 provider
|
package/lib/client.js
CHANGED
|
@@ -80,6 +80,174 @@ window.__ModuleLoader__.load({
|
|
|
80
80
|
return route !== void 0 && (route.provider ?? "").trim() !== "" && (route.model ?? "").trim() !== "";
|
|
81
81
|
}
|
|
82
82
|
//#endregion
|
|
83
|
+
//#region src/provider-group.ts
|
|
84
|
+
const ROUTE_FIELDS = Object.freeze([
|
|
85
|
+
"compactProvider",
|
|
86
|
+
"compactModel",
|
|
87
|
+
"autoReviewProvider",
|
|
88
|
+
"autoReviewModel",
|
|
89
|
+
"subagentProvider",
|
|
90
|
+
"subagentModel",
|
|
91
|
+
"subagentReasoningEffort"
|
|
92
|
+
]);
|
|
93
|
+
/** Own data property, so a literal '__proto__' provider id can never touch a prototype. */
|
|
94
|
+
function setKey(target, key, value) {
|
|
95
|
+
Object.defineProperty(target, key, {
|
|
96
|
+
value,
|
|
97
|
+
enumerable: true,
|
|
98
|
+
writable: true,
|
|
99
|
+
configurable: true
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** A non-blank string. Blank values behave exactly like an absent field. */
|
|
103
|
+
function present(value) {
|
|
104
|
+
return typeof value === "string" && value.trim() !== "";
|
|
105
|
+
}
|
|
106
|
+
/** Normalize one raw entry to its owning provider and route view; undefined for non-object junk. */
|
|
107
|
+
function normalizeEntry(value) {
|
|
108
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
109
|
+
const source = value;
|
|
110
|
+
const group = {};
|
|
111
|
+
for (const field of ROUTE_FIELDS) {
|
|
112
|
+
const item = source[field];
|
|
113
|
+
if (present(item)) group[field] = item;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
provider: present(source.provider) ? source.provider : "",
|
|
117
|
+
group
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Strip a raw stored value to well-formed groups; never throws. Both storage
|
|
122
|
+
* shapes are accepted: the stored entry array (keys live in each entry's
|
|
123
|
+
* `provider` field) and a hand-written provider → routes map (keys are the
|
|
124
|
+
* providers). All-empty entries stay: they pin a provider to every default.
|
|
125
|
+
*/
|
|
126
|
+
function normalizeProviderGroups(raw) {
|
|
127
|
+
const groups = {};
|
|
128
|
+
if (Array.isArray(raw)) {
|
|
129
|
+
for (const entry of raw) {
|
|
130
|
+
const view = normalizeEntry(entry);
|
|
131
|
+
if (view === void 0 || view.provider === "") continue;
|
|
132
|
+
setKey(groups, view.provider, view.group);
|
|
133
|
+
}
|
|
134
|
+
return groups;
|
|
135
|
+
}
|
|
136
|
+
if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) {
|
|
137
|
+
if (key.trim() === "") continue;
|
|
138
|
+
const view = normalizeEntry(value);
|
|
139
|
+
if (view === void 0) continue;
|
|
140
|
+
setKey(groups, key, view.group);
|
|
141
|
+
}
|
|
142
|
+
return groups;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The saved group for a provider.
|
|
146
|
+
* @returns the normalized group, or undefined when the provider has no group —
|
|
147
|
+
* which is what leaves the live routes untouched during a switch.
|
|
148
|
+
*/
|
|
149
|
+
function groupOf(groups, provider) {
|
|
150
|
+
if (groups === void 0 || provider.trim() === "") return void 0;
|
|
151
|
+
if (!Object.prototype.hasOwnProperty.call(groups, provider)) return void 0;
|
|
152
|
+
return groups[provider];
|
|
153
|
+
}
|
|
154
|
+
/** A complete pinned pair; either half missing leaves the capability at its default. */
|
|
155
|
+
function pair(group, providerField, modelField) {
|
|
156
|
+
const provider = group[providerField];
|
|
157
|
+
const model = group[modelField];
|
|
158
|
+
return present(provider) && present(model) ? {
|
|
159
|
+
provider,
|
|
160
|
+
model
|
|
161
|
+
} : void 0;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Ordered writes applying one group with full-replacement semantics: pinned
|
|
165
|
+
* capabilities land as pairs, everything else reverts to its default state
|
|
166
|
+
* (compaction/review follow the session model; Subagent follows official
|
|
167
|
+
* inherit). The mode write comes last so a fixed route never publishes with
|
|
168
|
+
* the previous provider's fields still half-written.
|
|
169
|
+
*/
|
|
170
|
+
function providerGroupWrites(group) {
|
|
171
|
+
const compact = pair(group, "compactProvider", "compactModel");
|
|
172
|
+
const review = pair(group, "autoReviewProvider", "autoReviewModel");
|
|
173
|
+
const subagent = pair(group, "subagentProvider", "subagentModel");
|
|
174
|
+
const effort = subagent !== void 0 && present(group.subagentReasoningEffort) ? group.subagentReasoningEffort : void 0;
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
field: "compactProvider",
|
|
178
|
+
value: compact?.provider
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
field: "compactModel",
|
|
182
|
+
value: compact?.model
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
field: "autoReviewProvider",
|
|
186
|
+
value: review?.provider
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
field: "autoReviewModel",
|
|
190
|
+
value: review?.model
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
field: "subagentProvider",
|
|
194
|
+
value: subagent?.provider
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
field: "subagentModel",
|
|
198
|
+
value: subagent?.model
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
field: "subagentReasoningEffort",
|
|
202
|
+
value: effort
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
field: "subagentMode",
|
|
206
|
+
mode: subagent !== void 0 ? "fixed" : "follow-main"
|
|
207
|
+
}
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
/** Storage entry for one provider's group; absent or blank route fields become ''. */
|
|
211
|
+
function entryOf(provider, group) {
|
|
212
|
+
return {
|
|
213
|
+
provider,
|
|
214
|
+
compactProvider: group.compactProvider ?? "",
|
|
215
|
+
compactModel: group.compactModel ?? "",
|
|
216
|
+
autoReviewProvider: group.autoReviewProvider ?? "",
|
|
217
|
+
autoReviewModel: group.autoReviewModel ?? "",
|
|
218
|
+
subagentProvider: group.subagentProvider ?? "",
|
|
219
|
+
subagentModel: group.subagentModel ?? "",
|
|
220
|
+
subagentReasoningEffort: group.subagentReasoningEffort ?? ""
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The stored entry list with one provider's group replaced (an entry) or
|
|
225
|
+
* removed (undefined). Every entry is re-normalized and de-duplicated so a
|
|
226
|
+
* save can never write junk back; the input is never mutated. A blank
|
|
227
|
+
* provider only normalizes.
|
|
228
|
+
*/
|
|
229
|
+
function withProviderGroup(storage, provider, group) {
|
|
230
|
+
const groups = { ...normalizeProviderGroups(storage) };
|
|
231
|
+
if (provider.trim() !== "") {
|
|
232
|
+
if (group === void 0) delete groups[provider];
|
|
233
|
+
else setKey(groups, provider, group);
|
|
234
|
+
}
|
|
235
|
+
return Object.entries(groups).map(([key, value]) => entryOf(key, value));
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Apply a saved group's routes to the live flat settings.
|
|
239
|
+
* @returns true when a group existed and every write settled; false when the
|
|
240
|
+
* provider has no group (nothing was written). A rejected write propagates.
|
|
241
|
+
*/
|
|
242
|
+
async function applyProviderGroupTo(sink, groups, provider) {
|
|
243
|
+
const group = groupOf(groups, provider);
|
|
244
|
+
if (group === void 0) return false;
|
|
245
|
+
for (const write of providerGroupWrites(group)) if (write.field === "subagentMode") await sink.set("subagentMode", write.mode);
|
|
246
|
+
else if (write.value === void 0) await sink.unset(write.field);
|
|
247
|
+
else await sink.set(write.field, write.value);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
83
251
|
//#region src/client/derived-config-form.ts
|
|
84
252
|
function deriveConfigForm(source, project, fields) {
|
|
85
253
|
let sourceSnapshot;
|
|
@@ -628,7 +796,40 @@ window.__ModuleLoader__.load({
|
|
|
628
796
|
function compact(...values) {
|
|
629
797
|
return values.filter((value) => value !== void 0 && value !== "").join(" · ");
|
|
630
798
|
}
|
|
799
|
+
/** A complete pinned pair; either half blank leaves the capability at its default. */
|
|
800
|
+
function draftPair(provider, model) {
|
|
801
|
+
return provider !== void 0 && provider.trim() !== "" && model !== void 0 && model.trim() !== "" ? {
|
|
802
|
+
provider,
|
|
803
|
+
model
|
|
804
|
+
} : void 0;
|
|
805
|
+
}
|
|
631
806
|
function RouteIcon({ kind }) {
|
|
807
|
+
if (kind === "providerGroups") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
808
|
+
width: "18",
|
|
809
|
+
height: "18",
|
|
810
|
+
viewBox: "0 0 18 18",
|
|
811
|
+
fill: "none",
|
|
812
|
+
"aria-hidden": "true",
|
|
813
|
+
children: [
|
|
814
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
815
|
+
d: "M3 5.5h12M3 12.5h12",
|
|
816
|
+
stroke: "currentColor",
|
|
817
|
+
strokeLinecap: "round"
|
|
818
|
+
}),
|
|
819
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
820
|
+
cx: "6.5",
|
|
821
|
+
cy: "5.5",
|
|
822
|
+
r: "2",
|
|
823
|
+
stroke: "currentColor"
|
|
824
|
+
}),
|
|
825
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
826
|
+
cx: "11.5",
|
|
827
|
+
cy: "12.5",
|
|
828
|
+
r: "2",
|
|
829
|
+
stroke: "currentColor"
|
|
830
|
+
})
|
|
831
|
+
]
|
|
832
|
+
});
|
|
632
833
|
if (kind === "compaction") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
633
834
|
width: "18",
|
|
634
835
|
height: "18",
|
|
@@ -840,7 +1041,7 @@ window.__ModuleLoader__.load({
|
|
|
840
1041
|
})]
|
|
841
1042
|
});
|
|
842
1043
|
}
|
|
843
|
-
function Actions({ t, busy, disabled, message, onCancel, onSave }) {
|
|
1044
|
+
function Actions({ t, busy, disabled, message, extra, onCancel, onSave }) {
|
|
844
1045
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
845
1046
|
className: ModelSwitchSettings_module_css_default.cardFooter,
|
|
846
1047
|
children: [
|
|
@@ -848,6 +1049,7 @@ window.__ModuleLoader__.load({
|
|
|
848
1049
|
className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
|
|
849
1050
|
children: message
|
|
850
1051
|
}),
|
|
1052
|
+
extra,
|
|
851
1053
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
852
1054
|
type: "button",
|
|
853
1055
|
className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
|
|
@@ -926,6 +1128,8 @@ window.__ModuleLoader__.load({
|
|
|
926
1128
|
const [message, setMessage] = (0, react.useState)();
|
|
927
1129
|
const [compactBusy, setCompactBusy] = (0, react.useState)(false);
|
|
928
1130
|
const [compactError, setCompactError] = (0, react.useState)();
|
|
1131
|
+
const [groupProvider, setGroupProvider] = (0, react.useState)("");
|
|
1132
|
+
const [groupDraft, setGroupDraft] = (0, react.useState)(void 0);
|
|
929
1133
|
const loadSearchCapabilities = props.loadCapabilities;
|
|
930
1134
|
const [searchSnapshot, setSearchSnapshot] = (0, react.useState)(void 0);
|
|
931
1135
|
const [searchError, setSearchError] = (0, react.useState)(void 0);
|
|
@@ -964,6 +1168,60 @@ window.__ModuleLoader__.load({
|
|
|
964
1168
|
if (timer !== void 0) clearTimeout(timer);
|
|
965
1169
|
};
|
|
966
1170
|
}, [loadSearchCapabilities]);
|
|
1171
|
+
const providerGroups = normalizeProviderGroups(switchSettings.value?.providerGroups);
|
|
1172
|
+
/** Seed an unsaved group from the live routes, mirroring how the switches seed from Main. */
|
|
1173
|
+
const seedGroupFromActive = () => {
|
|
1174
|
+
const compact = compaction.value !== void 0 && routePairConfigured(compaction.value) ? {
|
|
1175
|
+
provider: compaction.value.provider ?? "",
|
|
1176
|
+
model: compaction.value.model ?? ""
|
|
1177
|
+
} : void 0;
|
|
1178
|
+
const review = autoReview.value !== void 0 && routePairConfigured(autoReview.value) ? {
|
|
1179
|
+
provider: autoReview.value.provider ?? "",
|
|
1180
|
+
model: autoReview.value.model ?? ""
|
|
1181
|
+
} : void 0;
|
|
1182
|
+
const fixedSubagent = subagent.value !== void 0 && subagent.value.mode === "fixed" && routePairConfigured(subagent.value) ? {
|
|
1183
|
+
provider: subagent.value.provider ?? "",
|
|
1184
|
+
model: subagent.value.model ?? "",
|
|
1185
|
+
reasoningEffort: subagent.value.reasoningEffort
|
|
1186
|
+
} : void 0;
|
|
1187
|
+
return {
|
|
1188
|
+
...compact === void 0 ? {} : {
|
|
1189
|
+
compactProvider: compact.provider,
|
|
1190
|
+
compactModel: compact.model
|
|
1191
|
+
},
|
|
1192
|
+
...review === void 0 ? {} : {
|
|
1193
|
+
autoReviewProvider: review.provider,
|
|
1194
|
+
autoReviewModel: review.model
|
|
1195
|
+
},
|
|
1196
|
+
...fixedSubagent === void 0 ? {} : {
|
|
1197
|
+
subagentProvider: fixedSubagent.provider,
|
|
1198
|
+
subagentModel: fixedSubagent.model,
|
|
1199
|
+
...fixedSubagent.reasoningEffort === void 0 ? {} : { subagentReasoningEffort: fixedSubagent.reasoningEffort }
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
};
|
|
1203
|
+
/** Reload the editor draft: the saved group when one exists, else a seed from the live routes. */
|
|
1204
|
+
const resyncGroupDraft = () => {
|
|
1205
|
+
if (groupProvider === "") {
|
|
1206
|
+
setGroupDraft(void 0);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
setGroupDraft(groupOf(providerGroups, groupProvider) ?? seedGroupFromActive());
|
|
1210
|
+
};
|
|
1211
|
+
(0, react.useEffect)(() => {
|
|
1212
|
+
if (groupProvider !== "") return;
|
|
1213
|
+
const initial = controller.draft?.provider;
|
|
1214
|
+
if (initial !== void 0 && initial.trim() !== "") setGroupProvider(initial);
|
|
1215
|
+
}, [groupProvider, controller.draft]);
|
|
1216
|
+
(0, react.useEffect)(() => {
|
|
1217
|
+
resyncGroupDraft();
|
|
1218
|
+
}, [
|
|
1219
|
+
groupProvider,
|
|
1220
|
+
switchSettings.value,
|
|
1221
|
+
subagent.value,
|
|
1222
|
+
autoReview.value,
|
|
1223
|
+
compaction.value
|
|
1224
|
+
]);
|
|
967
1225
|
if ([
|
|
968
1226
|
controller.main,
|
|
969
1227
|
controller.subagent,
|
|
@@ -1182,6 +1440,83 @@ window.__ModuleLoader__.load({
|
|
|
1182
1440
|
const capabilityDisabled = (route, snapshot, next) => snapshot.status !== "ready" || !snapshot.writable || next === void 0 || busy === route || (next.provider ?? "").trim() === "" || (next.model ?? "").trim() === "";
|
|
1183
1441
|
const autoReviewDisabled = autoReview.status !== "ready" || !autoReview.writable || autoReviewDraft === void 0 || busy === "autoReview" || !autoReviewOn || (autoReviewDraft.provider ?? "").trim() === "" || (autoReviewDraft.model ?? "").trim() === "";
|
|
1184
1442
|
const compactionDisabled = compaction.status !== "ready" || !compaction.writable || compactionDraft === void 0 || busy === "compaction" || !compactionOn || (compactionDraft.provider ?? "").trim() === "" || (compactionDraft.model ?? "").trim() === "";
|
|
1443
|
+
const configuredGroupCount = Object.keys(providerGroups).length;
|
|
1444
|
+
const groupControlsDisabled = busy === "providerGroups" || switchSettings.status !== "ready" || !switchSettings.writable;
|
|
1445
|
+
const groupProviderChoices = deriveRouteChoices(groups, groupProvider === "" ? void 0 : { provider: groupProvider }).providers.map((choice) => groupOf(providerGroups, choice.id) === void 0 ? choice : {
|
|
1446
|
+
...choice,
|
|
1447
|
+
name: choice.name + " ✓"
|
|
1448
|
+
});
|
|
1449
|
+
const groupCompaction = groupDraft === void 0 ? void 0 : draftPair(groupDraft.compactProvider, groupDraft.compactModel);
|
|
1450
|
+
const groupReview = groupDraft === void 0 ? void 0 : draftPair(groupDraft.autoReviewProvider, groupDraft.autoReviewModel);
|
|
1451
|
+
const groupSubagent = groupDraft === void 0 ? void 0 : draftPair(groupDraft.subagentProvider, groupDraft.subagentModel);
|
|
1452
|
+
const groupCompactionChoices = deriveRouteChoices(groups, groupCompaction);
|
|
1453
|
+
const groupReviewChoices = deriveRouteChoices(groups, groupReview);
|
|
1454
|
+
const groupSubagentChoices = deriveRouteChoices(groups, groupSubagent);
|
|
1455
|
+
const groupSubMatched = matchCatalogModel(groups.find((group) => group.id === (groupDraft?.subagentProvider ?? ""))?.models ?? [], groupDraft?.subagentModel ?? "");
|
|
1456
|
+
const groupSubEfforts = (() => {
|
|
1457
|
+
const efforts = (groupSubMatched?.model.reasoning?.efforts ?? []).map((effort) => ({
|
|
1458
|
+
id: effort.id,
|
|
1459
|
+
name: effort.name
|
|
1460
|
+
}));
|
|
1461
|
+
const current = groupDraft?.subagentReasoningEffort;
|
|
1462
|
+
if (current !== void 0 && current !== "" && !efforts.some((option) => option.id === current)) efforts.push({
|
|
1463
|
+
id: current,
|
|
1464
|
+
name: current
|
|
1465
|
+
});
|
|
1466
|
+
return efforts;
|
|
1467
|
+
})();
|
|
1468
|
+
/** Enable a capability row by pinning it to the Main route, like the switches above seed from Main. */
|
|
1469
|
+
const enableGroupCapability = (kind) => {
|
|
1470
|
+
setGroupDraft((draft) => {
|
|
1471
|
+
const main = controller.draft;
|
|
1472
|
+
if (draft === void 0 || main === void 0) return draft;
|
|
1473
|
+
if (kind === "compaction") return {
|
|
1474
|
+
...draft,
|
|
1475
|
+
compactProvider: main.provider,
|
|
1476
|
+
compactModel: main.model
|
|
1477
|
+
};
|
|
1478
|
+
if (kind === "autoReview") return {
|
|
1479
|
+
...draft,
|
|
1480
|
+
autoReviewProvider: main.provider,
|
|
1481
|
+
autoReviewModel: main.model
|
|
1482
|
+
};
|
|
1483
|
+
return {
|
|
1484
|
+
...draft,
|
|
1485
|
+
subagentProvider: main.provider,
|
|
1486
|
+
subagentModel: main.model,
|
|
1487
|
+
...main.reasoningEffort === void 0 ? {} : { subagentReasoningEffort: main.reasoningEffort }
|
|
1488
|
+
};
|
|
1489
|
+
});
|
|
1490
|
+
};
|
|
1491
|
+
const disableGroupCapability = (kind) => {
|
|
1492
|
+
setGroupDraft((draft) => {
|
|
1493
|
+
if (draft === void 0) return draft;
|
|
1494
|
+
if (kind === "compaction") return {
|
|
1495
|
+
...draft,
|
|
1496
|
+
compactProvider: "",
|
|
1497
|
+
compactModel: ""
|
|
1498
|
+
};
|
|
1499
|
+
if (kind === "autoReview") return {
|
|
1500
|
+
...draft,
|
|
1501
|
+
autoReviewProvider: "",
|
|
1502
|
+
autoReviewModel: ""
|
|
1503
|
+
};
|
|
1504
|
+
return {
|
|
1505
|
+
...draft,
|
|
1506
|
+
subagentProvider: "",
|
|
1507
|
+
subagentModel: "",
|
|
1508
|
+
subagentReasoningEffort: ""
|
|
1509
|
+
};
|
|
1510
|
+
});
|
|
1511
|
+
};
|
|
1512
|
+
const saveProviderGroupCard = () => {
|
|
1513
|
+
if (groupProvider === "" || groupDraft === void 0) return;
|
|
1514
|
+
run("providerGroups", () => props.saveProviderGroup(groupProvider, groupDraft));
|
|
1515
|
+
};
|
|
1516
|
+
const deleteProviderGroupCard = () => {
|
|
1517
|
+
if (groupProvider === "") return;
|
|
1518
|
+
run("providerGroups", () => props.saveProviderGroup(groupProvider, void 0));
|
|
1519
|
+
};
|
|
1185
1520
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
1186
1521
|
className: ModelSwitchSettings_module_css_default.section,
|
|
1187
1522
|
children: [
|
|
@@ -1681,6 +2016,229 @@ window.__ModuleLoader__.load({
|
|
|
1681
2016
|
] })
|
|
1682
2017
|
})
|
|
1683
2018
|
]
|
|
2019
|
+
}),
|
|
2020
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2021
|
+
className: ModelSwitchSettings_module_css_default.group,
|
|
2022
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
2023
|
+
className: ModelSwitchSettings_module_css_default.groupLabel,
|
|
2024
|
+
children: props.t("providerGroups")
|
|
2025
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(RouteCard, {
|
|
2026
|
+
title: props.t("providerGroupsCard"),
|
|
2027
|
+
summary: props.t("providerGroupsSummary"),
|
|
2028
|
+
hint: props.t("providerGroupsHelp"),
|
|
2029
|
+
icon: "providerGroups",
|
|
2030
|
+
open: open === "providerGroups",
|
|
2031
|
+
onToggle: () => {
|
|
2032
|
+
toggle("providerGroups");
|
|
2033
|
+
},
|
|
2034
|
+
...configuredGroupCount > 0 ? { badge: String(configuredGroupCount) } : {},
|
|
2035
|
+
children: [
|
|
2036
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2037
|
+
className: ModelSwitchSettings_module_css_default.hint,
|
|
2038
|
+
children: props.t("providerGroupsBodyHelp")
|
|
2039
|
+
}),
|
|
2040
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2041
|
+
className: ModelSwitchSettings_module_css_default.formGrid,
|
|
2042
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2043
|
+
label: props.t("provider"),
|
|
2044
|
+
value: groupProvider,
|
|
2045
|
+
disabled: groupControlsDisabled,
|
|
2046
|
+
choices: groupProviderChoices,
|
|
2047
|
+
onChange: setGroupProvider
|
|
2048
|
+
})
|
|
2049
|
+
}),
|
|
2050
|
+
groupProvider === "" || groupDraft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2051
|
+
className: ModelSwitchSettings_module_css_default.hint,
|
|
2052
|
+
children: props.t("providerGroupPick")
|
|
2053
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2054
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2055
|
+
className: ModelSwitchSettings_module_css_default.rowCard,
|
|
2056
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2057
|
+
className: ModelSwitchSettings_module_css_default.rowCopy,
|
|
2058
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2059
|
+
className: ModelSwitchSettings_module_css_default.rowTitle,
|
|
2060
|
+
children: props.t("compaction")
|
|
2061
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2062
|
+
className: ModelSwitchSettings_module_css_default.hint,
|
|
2063
|
+
children: groupCompaction === void 0 ? props.t("compactionFollow") : routeName(groups, groupCompaction)
|
|
2064
|
+
})]
|
|
2065
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
|
|
2066
|
+
checked: groupCompaction !== void 0,
|
|
2067
|
+
disabled: groupControlsDisabled,
|
|
2068
|
+
label: props.t("compaction"),
|
|
2069
|
+
onChange: (value) => {
|
|
2070
|
+
if (value) enableGroupCapability("compaction");
|
|
2071
|
+
else disableGroupCapability("compaction");
|
|
2072
|
+
}
|
|
2073
|
+
})]
|
|
2074
|
+
}),
|
|
2075
|
+
groupCompaction === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2076
|
+
className: ModelSwitchSettings_module_css_default.formGrid,
|
|
2077
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2078
|
+
label: props.t("provider"),
|
|
2079
|
+
value: groupCompaction.provider,
|
|
2080
|
+
disabled: groupControlsDisabled,
|
|
2081
|
+
choices: groupCompactionChoices.providers,
|
|
2082
|
+
onChange: (provider) => {
|
|
2083
|
+
const first = groups.find((item) => item.id === provider)?.models[0];
|
|
2084
|
+
setGroupDraft((draft) => draft === void 0 ? draft : {
|
|
2085
|
+
...draft,
|
|
2086
|
+
compactProvider: provider,
|
|
2087
|
+
compactModel: first?.id ?? ""
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2091
|
+
label: props.t("model"),
|
|
2092
|
+
value: groupCompaction.model,
|
|
2093
|
+
disabled: groupControlsDisabled,
|
|
2094
|
+
choices: groupCompactionChoices.models,
|
|
2095
|
+
onChange: (model) => {
|
|
2096
|
+
setGroupDraft((draft) => draft === void 0 ? draft : {
|
|
2097
|
+
...draft,
|
|
2098
|
+
compactModel: model
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
})]
|
|
2102
|
+
}),
|
|
2103
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2104
|
+
className: ModelSwitchSettings_module_css_default.rowCard,
|
|
2105
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2106
|
+
className: ModelSwitchSettings_module_css_default.rowCopy,
|
|
2107
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2108
|
+
className: ModelSwitchSettings_module_css_default.rowTitle,
|
|
2109
|
+
children: props.t("autoReview")
|
|
2110
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2111
|
+
className: ModelSwitchSettings_module_css_default.hint,
|
|
2112
|
+
children: groupReview === void 0 ? props.t("autoReviewFollow") : routeName(groups, groupReview)
|
|
2113
|
+
})]
|
|
2114
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
|
|
2115
|
+
checked: groupReview !== void 0,
|
|
2116
|
+
disabled: groupControlsDisabled,
|
|
2117
|
+
label: props.t("autoReview"),
|
|
2118
|
+
onChange: (value) => {
|
|
2119
|
+
if (value) enableGroupCapability("autoReview");
|
|
2120
|
+
else disableGroupCapability("autoReview");
|
|
2121
|
+
}
|
|
2122
|
+
})]
|
|
2123
|
+
}),
|
|
2124
|
+
groupReview === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2125
|
+
className: ModelSwitchSettings_module_css_default.formGrid,
|
|
2126
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2127
|
+
label: props.t("provider"),
|
|
2128
|
+
value: groupReview.provider,
|
|
2129
|
+
disabled: groupControlsDisabled,
|
|
2130
|
+
choices: groupReviewChoices.providers,
|
|
2131
|
+
onChange: (provider) => {
|
|
2132
|
+
const first = groups.find((item) => item.id === provider)?.models[0];
|
|
2133
|
+
setGroupDraft((draft) => draft === void 0 ? draft : {
|
|
2134
|
+
...draft,
|
|
2135
|
+
autoReviewProvider: provider,
|
|
2136
|
+
autoReviewModel: first?.id ?? ""
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2140
|
+
label: props.t("model"),
|
|
2141
|
+
value: groupReview.model,
|
|
2142
|
+
disabled: groupControlsDisabled,
|
|
2143
|
+
choices: groupReviewChoices.models,
|
|
2144
|
+
onChange: (model) => {
|
|
2145
|
+
setGroupDraft((draft) => draft === void 0 ? draft : {
|
|
2146
|
+
...draft,
|
|
2147
|
+
autoReviewModel: model
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
})]
|
|
2151
|
+
}),
|
|
2152
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2153
|
+
className: ModelSwitchSettings_module_css_default.rowCard,
|
|
2154
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2155
|
+
className: ModelSwitchSettings_module_css_default.rowCopy,
|
|
2156
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2157
|
+
className: ModelSwitchSettings_module_css_default.rowTitle,
|
|
2158
|
+
children: props.t("subagent")
|
|
2159
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2160
|
+
className: ModelSwitchSettings_module_css_default.hint,
|
|
2161
|
+
children: groupSubagent === void 0 ? props.t("subagentOff") : routeName(groups, groupSubagent)
|
|
2162
|
+
})]
|
|
2163
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
|
|
2164
|
+
checked: groupSubagent !== void 0,
|
|
2165
|
+
disabled: groupControlsDisabled,
|
|
2166
|
+
label: props.t("subagent"),
|
|
2167
|
+
onChange: (value) => {
|
|
2168
|
+
if (value) enableGroupCapability("subagent");
|
|
2169
|
+
else disableGroupCapability("subagent");
|
|
2170
|
+
}
|
|
2171
|
+
})]
|
|
2172
|
+
}),
|
|
2173
|
+
groupSubagent === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2174
|
+
className: ModelSwitchSettings_module_css_default.formGrid,
|
|
2175
|
+
children: [
|
|
2176
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2177
|
+
label: props.t("provider"),
|
|
2178
|
+
value: groupSubagent.provider,
|
|
2179
|
+
disabled: groupControlsDisabled,
|
|
2180
|
+
choices: groupSubagentChoices.providers,
|
|
2181
|
+
onChange: (provider) => {
|
|
2182
|
+
const first = groups.find((item) => item.id === provider)?.models[0];
|
|
2183
|
+
const routed = selectRouteModel(groups, provider, first?.id ?? groupSubagent.model);
|
|
2184
|
+
setGroupDraft((draft) => draft === void 0 ? draft : {
|
|
2185
|
+
...draft,
|
|
2186
|
+
subagentProvider: routed.provider,
|
|
2187
|
+
subagentModel: routed.model,
|
|
2188
|
+
subagentReasoningEffort: routed.reasoningEffort ?? ""
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
}),
|
|
2192
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2193
|
+
label: props.t("model"),
|
|
2194
|
+
value: groupSubagent.model,
|
|
2195
|
+
disabled: groupControlsDisabled,
|
|
2196
|
+
choices: groupSubagentChoices.models,
|
|
2197
|
+
onChange: (model) => {
|
|
2198
|
+
const routed = selectRouteModel(groups, groupSubagent.provider, model);
|
|
2199
|
+
setGroupDraft((draft) => draft === void 0 ? draft : {
|
|
2200
|
+
...draft,
|
|
2201
|
+
subagentProvider: routed.provider,
|
|
2202
|
+
subagentModel: routed.model,
|
|
2203
|
+
subagentReasoningEffort: routed.reasoningEffort ?? ""
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
}),
|
|
2207
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2208
|
+
label: props.t("effort"),
|
|
2209
|
+
value: groupDraft.subagentReasoningEffort ?? "",
|
|
2210
|
+
disabled: groupControlsDisabled,
|
|
2211
|
+
choices: groupSubEfforts,
|
|
2212
|
+
onChange: (effort) => {
|
|
2213
|
+
setGroupDraft((draft) => draft === void 0 ? draft : {
|
|
2214
|
+
...draft,
|
|
2215
|
+
subagentReasoningEffort: effort
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
})
|
|
2219
|
+
]
|
|
2220
|
+
}),
|
|
2221
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
|
|
2222
|
+
t: props.t,
|
|
2223
|
+
busy: busy === "providerGroups",
|
|
2224
|
+
disabled: groupControlsDisabled || groupProvider === "",
|
|
2225
|
+
...message?.route === "providerGroups" ? { message: message.text } : {},
|
|
2226
|
+
extra: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2227
|
+
type: "button",
|
|
2228
|
+
className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
|
|
2229
|
+
disabled: groupControlsDisabled || groupOf(providerGroups, groupProvider) === void 0,
|
|
2230
|
+
onClick: deleteProviderGroupCard,
|
|
2231
|
+
children: props.t("providerGroupDelete")
|
|
2232
|
+
}),
|
|
2233
|
+
onCancel: () => {
|
|
2234
|
+
resyncGroupDraft();
|
|
2235
|
+
setMessage(void 0);
|
|
2236
|
+
},
|
|
2237
|
+
onSave: saveProviderGroupCard
|
|
2238
|
+
})
|
|
2239
|
+
] })
|
|
2240
|
+
]
|
|
2241
|
+
})]
|
|
1684
2242
|
})
|
|
1685
2243
|
]
|
|
1686
2244
|
});
|
|
@@ -1731,6 +2289,14 @@ window.__ModuleLoader__.load({
|
|
|
1731
2289
|
compactionFollow: "跟随会话模型",
|
|
1732
2290
|
compactionHelp: "为官方压缩(/compact 与官方自动压缩)指定摘要模型;关闭(两个字段都留空)时使用官方默认,即当前会话的目标模型。",
|
|
1733
2291
|
compactionRouteHelp: "摘要请求固定使用这里的 Provider/模型,与会话目标模型无关;保存后对下一次压缩立即生效。保存时若恰有压缩进行中,可能被打断并在下次重试。",
|
|
2292
|
+
providerGroups: "供应商配置组",
|
|
2293
|
+
providerGroupsCard: "按供应商预设路由",
|
|
2294
|
+
providerGroupsSummary: "切换主模型供应商时自动应用对应组",
|
|
2295
|
+
providerGroupsHelp: "为压缩、子代理、审查按供应商分别保存路由。",
|
|
2296
|
+
providerGroupsBodyHelp: "每个供应商保存一组压缩、子代理、审查路由;主模型切换到该供应商时,这三条路由自动写入上方对应卡片。切换到未配置的供应商时现有路由保持不变;应用会覆盖三张卡片的当前值(含手动修改)。",
|
|
2297
|
+
providerGroupPick: "选择供应商以编辑其配置组。",
|
|
2298
|
+
providerGroupDelete: "删除该组",
|
|
2299
|
+
providerGroupApplyFailed: "主模型已保存,但供应商组应用失败:",
|
|
1734
2300
|
"reason.central-subagent-routing": "Alpha.4 没有全局子代理启动路由接口。",
|
|
1735
2301
|
"reason.packaged-preset-roots": "Alpha.4 不支持插件提供额外 preset root。",
|
|
1736
2302
|
"reason.tool-owner-suppression": "Alpha.4 没有工具所有者或来源抑制接口。",
|
|
@@ -1782,6 +2348,14 @@ window.__ModuleLoader__.load({
|
|
|
1782
2348
|
compactionFollow: "Follows the conversation model",
|
|
1783
2349
|
compactionHelp: "Pick the summarization model for official compaction (/compact and official automatic compaction). When off (both fields empty), the official default applies: the conversation target model.",
|
|
1784
2350
|
compactionRouteHelp: "Summarization requests use exactly this provider/model regardless of the conversation target; saved changes apply to the very next compaction. Saving while a compaction is running may interrupt it; it retries next time.",
|
|
2351
|
+
providerGroups: "Provider groups",
|
|
2352
|
+
providerGroupsCard: "Per-provider route presets",
|
|
2353
|
+
providerGroupsSummary: "Applied automatically when the main model provider switches",
|
|
2354
|
+
providerGroupsHelp: "Save compaction, subagent, and review routes per provider.",
|
|
2355
|
+
providerGroupsBodyHelp: "Each provider stores one compaction, subagent, and review route group. Switching the main model to that provider writes these three routes into the cards above; switching to a provider without a group leaves the current routes untouched. Applying a group overwrites those three cards, including manual edits.",
|
|
2356
|
+
providerGroupPick: "Pick a provider to edit its group.",
|
|
2357
|
+
providerGroupDelete: "Delete group",
|
|
2358
|
+
providerGroupApplyFailed: "Main model saved, but applying the provider group failed: ",
|
|
1785
2359
|
"reason.central-subagent-routing": "Alpha.4 exposes no global Subagent start-routing seam.",
|
|
1786
2360
|
"reason.packaged-preset-roots": "Alpha.4 exposes no plugin-owned preset root.",
|
|
1787
2361
|
"reason.tool-owner-suppression": "Alpha.4 exposes no tool owner or provenance suppression seam.",
|
|
@@ -4230,6 +4804,7 @@ window.__ModuleLoader__.load({
|
|
|
4230
4804
|
const models = scope.modelDirectories;
|
|
4231
4805
|
const sessions = scope.sessions;
|
|
4232
4806
|
const mainDefaults = scope.configForms.get(MAIN_DEFAULT_CONFIG_ID);
|
|
4807
|
+
const ownedForm = scope.configForms.get(MODEL_SWITCH_CONFIG_ID);
|
|
4233
4808
|
let directoryService;
|
|
4234
4809
|
const orderStore = providerOrderStore(scope.configForms.get(PROVIDERS_CONFIG_ID));
|
|
4235
4810
|
scope.inject(["providerDirectory"], (directoryScope) => {
|
|
@@ -4303,6 +4878,11 @@ window.__ModuleLoader__.load({
|
|
|
4303
4878
|
return uncommitted;
|
|
4304
4879
|
}
|
|
4305
4880
|
if (!result.ok) return uncommitted;
|
|
4881
|
+
if (selection.provider !== currentProvider) try {
|
|
4882
|
+
await applyProviderGroupTo(ownedForm, normalizeProviderGroups(ownedForm.getSnapshot().value?.providerGroups), selection.provider);
|
|
4883
|
+
} catch (error) {
|
|
4884
|
+
console.warn("dsh-model-switch: provider group apply failed", error);
|
|
4885
|
+
}
|
|
4306
4886
|
return {
|
|
4307
4887
|
committed: true,
|
|
4308
4888
|
mainDefaultRestored: (await restoreMainDefault(mainDefaults, defaultBeforeSwitch)).restored
|
|
@@ -4415,7 +4995,12 @@ window.__ModuleLoader__.load({
|
|
|
4415
4995
|
const image = deriveConfigForm(owned, deriveImageSettings, IMAGE_SETTINGS_FIELDS);
|
|
4416
4996
|
const autoReview = deriveConfigForm(owned, deriveAutoReviewSettings, AUTO_REVIEW_SETTINGS_FIELDS);
|
|
4417
4997
|
const compaction = deriveConfigForm(owned, deriveCompactionSettings, COMPACTION_SETTINGS_FIELDS);
|
|
4998
|
+
/** Adopt the target provider's saved group; a provider without a group leaves the routes untouched. */
|
|
4999
|
+
const applyProviderGroupFor = async (provider) => {
|
|
5000
|
+
await applyProviderGroupTo(owned, normalizeProviderGroups(owned.getSnapshot().value?.providerGroups), provider);
|
|
5001
|
+
};
|
|
4418
5002
|
const saveMain = async (next, expectedRevision) => {
|
|
5003
|
+
const previousProvider = mainForm.getSnapshot().value?.provider;
|
|
4419
5004
|
if (!await mainForm.mutate([
|
|
4420
5005
|
{
|
|
4421
5006
|
op: "set",
|
|
@@ -4441,6 +5026,11 @@ window.__ModuleLoader__.load({
|
|
|
4441
5026
|
}
|
|
4442
5027
|
const revision = mainForm.getSnapshot().revision;
|
|
4443
5028
|
if (revision === void 0) throw new Error("settings unavailable after save");
|
|
5029
|
+
if (next.provider.trim() !== "" && next.provider !== previousProvider) try {
|
|
5030
|
+
await applyProviderGroupFor(next.provider);
|
|
5031
|
+
} catch (error) {
|
|
5032
|
+
throw new Error(t("providerGroupApplyFailed") + (error instanceof Error ? error.message : t("requestFailed")));
|
|
5033
|
+
}
|
|
4444
5034
|
return revision;
|
|
4445
5035
|
};
|
|
4446
5036
|
const connectionRpc = (() => {
|
|
@@ -4493,6 +5083,16 @@ window.__ModuleLoader__.load({
|
|
|
4493
5083
|
setCompactOnSwitch: async (value) => {
|
|
4494
5084
|
if (!await owned.set("compactOnSwitch", value)) throw new Error("settings-rejected");
|
|
4495
5085
|
},
|
|
5086
|
+
saveProviderGroup: async (provider, group) => {
|
|
5087
|
+
const current = owned.getSnapshot().value?.providerGroups;
|
|
5088
|
+
const next = withProviderGroup(current, provider, group);
|
|
5089
|
+
if (next.length === 0) {
|
|
5090
|
+
if (current === void 0) return;
|
|
5091
|
+
if (!await owned.unset("providerGroups")) throw new Error("settings-rejected");
|
|
5092
|
+
return;
|
|
5093
|
+
}
|
|
5094
|
+
if (!await owned.set("providerGroups", next)) throw new Error("settings-rejected");
|
|
5095
|
+
},
|
|
4496
5096
|
providerRoleOf,
|
|
4497
5097
|
loadCatalog: async () => {
|
|
4498
5098
|
const response = await remote.session.modelCatalog();
|
package/lib/index.js
CHANGED
|
@@ -12,7 +12,83 @@ import { dirname, join } from "node:path";
|
|
|
12
12
|
import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
|
|
13
13
|
import { boundContextSummary, createMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
14
14
|
import { canonicalHeader } from "@deepseek-ai/dsh-session";
|
|
15
|
+
//#region lib/types/provider-group.js
|
|
16
|
+
/**
|
|
17
|
+
* Provider configuration groups: per-provider presets for the compaction,
|
|
18
|
+
* subagent, and auto-review routes.
|
|
19
|
+
*
|
|
20
|
+
* A group stores the exact flat settings fields it will write, so applying a
|
|
21
|
+
* group is a plain settings-layer write: every capability consumer keeps
|
|
22
|
+
* reading the live values unchanged. All functions here are pure and never
|
|
23
|
+
* throw on malformed storage.
|
|
24
|
+
*/
|
|
25
|
+
const ROUTE_FIELDS = Object.freeze([
|
|
26
|
+
"compactProvider",
|
|
27
|
+
"compactModel",
|
|
28
|
+
"autoReviewProvider",
|
|
29
|
+
"autoReviewModel",
|
|
30
|
+
"subagentProvider",
|
|
31
|
+
"subagentModel",
|
|
32
|
+
"subagentReasoningEffort"
|
|
33
|
+
]);
|
|
34
|
+
/** Own data property, so a literal '__proto__' provider id can never touch a prototype. */
|
|
35
|
+
function setKey(target, key, value) {
|
|
36
|
+
Object.defineProperty(target, key, {
|
|
37
|
+
value,
|
|
38
|
+
enumerable: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
configurable: true
|
|
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
|
+
/** Normalize one raw entry to its owning provider and route view; undefined for non-object junk. */
|
|
48
|
+
function normalizeEntry(value) {
|
|
49
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
50
|
+
const source = value;
|
|
51
|
+
const group = {};
|
|
52
|
+
for (const field of ROUTE_FIELDS) {
|
|
53
|
+
const item = source[field];
|
|
54
|
+
if (present(item)) group[field] = item;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
provider: present(source.provider) ? source.provider : "",
|
|
58
|
+
group
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Strip a raw stored value to well-formed groups; never throws. Both storage
|
|
63
|
+
* shapes are accepted: the stored entry array (keys live in each entry's
|
|
64
|
+
* `provider` field) and a hand-written provider → routes map (keys are the
|
|
65
|
+
* providers). All-empty entries stay: they pin a provider to every default.
|
|
66
|
+
*/
|
|
67
|
+
function normalizeProviderGroups(raw) {
|
|
68
|
+
const groups = {};
|
|
69
|
+
if (Array.isArray(raw)) {
|
|
70
|
+
for (const entry of raw) {
|
|
71
|
+
const view = normalizeEntry(entry);
|
|
72
|
+
if (view === void 0 || view.provider === "") continue;
|
|
73
|
+
setKey(groups, view.provider, view.group);
|
|
74
|
+
}
|
|
75
|
+
return groups;
|
|
76
|
+
}
|
|
77
|
+
if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) {
|
|
78
|
+
if (key.trim() === "") continue;
|
|
79
|
+
const view = normalizeEntry(value);
|
|
80
|
+
if (view === void 0) continue;
|
|
81
|
+
setKey(groups, key, view.group);
|
|
82
|
+
}
|
|
83
|
+
return groups;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
15
86
|
//#region lib/types/host-settings.js
|
|
87
|
+
/**
|
|
88
|
+
* Annotated rather than inferred: the nested entry schema's inferred type
|
|
89
|
+
* expands to cosmokit internals (`Dict`) that cannot be named in the emitted
|
|
90
|
+
* declaration, and the annotation pins the output shape to this interface.
|
|
91
|
+
*/
|
|
16
92
|
const Config = z.object({
|
|
17
93
|
subagentMode: z.union(["follow-main", "fixed"]).default("follow-main").volatile(),
|
|
18
94
|
subagentProvider: z.string().volatile(),
|
|
@@ -26,7 +102,17 @@ const Config = z.object({
|
|
|
26
102
|
autoReviewProvider: z.string().volatile(),
|
|
27
103
|
autoReviewModel: z.string().volatile(),
|
|
28
104
|
compactProvider: z.string().volatile(),
|
|
29
|
-
compactModel: z.string().volatile()
|
|
105
|
+
compactModel: z.string().volatile(),
|
|
106
|
+
providerGroups: z.array(z.object({
|
|
107
|
+
provider: z.string().default(""),
|
|
108
|
+
compactProvider: z.string().default(""),
|
|
109
|
+
compactModel: z.string().default(""),
|
|
110
|
+
autoReviewProvider: z.string().default(""),
|
|
111
|
+
autoReviewModel: z.string().default(""),
|
|
112
|
+
subagentProvider: z.string().default(""),
|
|
113
|
+
subagentModel: z.string().default(""),
|
|
114
|
+
subagentReasoningEffort: z.string().default("")
|
|
115
|
+
})).default([]).volatile()
|
|
30
116
|
});
|
|
31
117
|
function readConfig(config) {
|
|
32
118
|
const subagentProvider = config.subagentProvider.get();
|
|
@@ -40,6 +126,7 @@ function readConfig(config) {
|
|
|
40
126
|
const autoReviewModel = config.autoReviewModel.get();
|
|
41
127
|
const compactProvider = config.compactProvider.get();
|
|
42
128
|
const compactModel = config.compactModel.get();
|
|
129
|
+
const providerGroups = normalizeProviderGroups(config.providerGroups.get());
|
|
43
130
|
return {
|
|
44
131
|
subagentMode: config.subagentMode.get(),
|
|
45
132
|
...subagentProvider === void 0 ? {} : { subagentProvider },
|
|
@@ -53,7 +140,8 @@ function readConfig(config) {
|
|
|
53
140
|
...autoReviewProvider === void 0 || autoReviewProvider === "" ? {} : { autoReviewProvider },
|
|
54
141
|
...autoReviewModel === void 0 || autoReviewModel === "" ? {} : { autoReviewModel },
|
|
55
142
|
...compactProvider === void 0 || compactProvider === "" ? {} : { compactProvider },
|
|
56
|
-
...compactModel === void 0 || compactModel === "" ? {} : { compactModel }
|
|
143
|
+
...compactModel === void 0 || compactModel === "" ? {} : { compactModel },
|
|
144
|
+
...Object.keys(providerGroups).length === 0 ? {} : { providerGroups }
|
|
57
145
|
};
|
|
58
146
|
}
|
|
59
147
|
//#endregion
|
|
@@ -3,6 +3,7 @@ import type { ModelProviderGroup } from '@deepseek-ai/dsh-api-session-controller
|
|
|
3
3
|
import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
4
4
|
import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
5
|
import { type CapabilityRouteView, type MainSettingsView, type ModelSwitchSettingsView, type SubagentSettingsView } from '../client-contract.js';
|
|
6
|
+
import { type ProviderGroupConfig } from '../provider-group.js';
|
|
6
7
|
import type { RuntimeCapabilities } from '../runtime-capabilities.js';
|
|
7
8
|
import { type CapabilitiesSnapshot } from './search-capabilities.js';
|
|
8
9
|
import type { ModelSwitchLocaleKey } from './locales.js';
|
|
@@ -23,6 +24,8 @@ export interface ModelSwitchSettingsFace {
|
|
|
23
24
|
setSubagent: (field: 'mode' | 'provider' | 'model' | 'reasoningEffort', value: string | undefined) => Promise<void>;
|
|
24
25
|
setCapability: (route: 'search' | 'image' | 'autoReview' | 'compaction', field: 'provider' | 'model', value: string | undefined) => Promise<void>;
|
|
25
26
|
setCompactOnSwitch: (value: boolean) => Promise<void>;
|
|
27
|
+
/** Replace or remove (undefined) one provider's saved capability group. */
|
|
28
|
+
saveProviderGroup: (provider: string, group: ProviderGroupConfig | undefined) => Promise<void>;
|
|
26
29
|
loadCatalog: () => Promise<readonly ModelProviderGroup[]>;
|
|
27
30
|
/** Host capabilities long-poll; absent in legacy faces, which keep the static-capabilities path. */
|
|
28
31
|
loadCapabilities?: (revision?: number, signal?: AbortSignal) => Promise<CapabilitiesSnapshot>;
|
|
@@ -42,6 +42,14 @@ export declare const zh: {
|
|
|
42
42
|
readonly compactionFollow: "跟随会话模型";
|
|
43
43
|
readonly compactionHelp: "为官方压缩(/compact 与官方自动压缩)指定摘要模型;关闭(两个字段都留空)时使用官方默认,即当前会话的目标模型。";
|
|
44
44
|
readonly compactionRouteHelp: "摘要请求固定使用这里的 Provider/模型,与会话目标模型无关;保存后对下一次压缩立即生效。保存时若恰有压缩进行中,可能被打断并在下次重试。";
|
|
45
|
+
readonly providerGroups: "供应商配置组";
|
|
46
|
+
readonly providerGroupsCard: "按供应商预设路由";
|
|
47
|
+
readonly providerGroupsSummary: "切换主模型供应商时自动应用对应组";
|
|
48
|
+
readonly providerGroupsHelp: "为压缩、子代理、审查按供应商分别保存路由。";
|
|
49
|
+
readonly providerGroupsBodyHelp: "每个供应商保存一组压缩、子代理、审查路由;主模型切换到该供应商时,这三条路由自动写入上方对应卡片。切换到未配置的供应商时现有路由保持不变;应用会覆盖三张卡片的当前值(含手动修改)。";
|
|
50
|
+
readonly providerGroupPick: "选择供应商以编辑其配置组。";
|
|
51
|
+
readonly providerGroupDelete: "删除该组";
|
|
52
|
+
readonly providerGroupApplyFailed: "主模型已保存,但供应商组应用失败:";
|
|
45
53
|
readonly 'reason.central-subagent-routing': "Alpha.4 没有全局子代理启动路由接口。";
|
|
46
54
|
readonly 'reason.packaged-preset-roots': "Alpha.4 不支持插件提供额外 preset root。";
|
|
47
55
|
readonly 'reason.tool-owner-suppression': "Alpha.4 没有工具所有者或来源抑制接口。";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProviderGroupsStorage } from './provider-group.js';
|
|
1
2
|
export declare const MODEL_SWITCH_CONFIG_ID = "model-switch";
|
|
2
3
|
export declare const MAIN_DEFAULT_CONFIG_ID = "agent-default-model";
|
|
3
4
|
export declare const PROVIDERS_CONFIG_ID = "llm-providers-ui";
|
|
@@ -23,6 +24,8 @@ export interface ModelSwitchSettingsView {
|
|
|
23
24
|
autoReviewModel?: string;
|
|
24
25
|
compactProvider?: string;
|
|
25
26
|
compactModel?: string;
|
|
27
|
+
/** Raw saved per-provider groups; edited whole through the provider-groups card. */
|
|
28
|
+
providerGroups?: ProviderGroupsStorage;
|
|
26
29
|
}
|
|
27
30
|
export interface SubagentSettingsView {
|
|
28
31
|
mode: 'follow-main' | 'fixed';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { ModelSelection } from '@deepseek-ai/dsh-agent';
|
|
3
3
|
import type { AgentDefaultModelConfig } from '@deepseek-ai/dsh-agent-default-model';
|
|
4
|
-
import { type Config as ModelSwitchSettingsConfig, type ModelSwitchSettings } from './host-settings.js';
|
|
4
|
+
import { Config, type Config as ModelSwitchSettingsConfig, type ModelSwitchSettings } from './host-settings.js';
|
|
5
5
|
import { ModelSwitchAdapterRegistry } from './adapter-registry.js';
|
|
6
6
|
declare module '@deepseek-ai/cordis' {
|
|
7
7
|
interface Context {
|
|
@@ -15,35 +15,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
15
15
|
/** Host owner for Model Switch settings and the released Main-default adapter. */
|
|
16
16
|
export declare class ModelSwitchRuntime extends Service {
|
|
17
17
|
static inject: string[];
|
|
18
|
-
static Config: import("@deepseek-ai/schemastery").default<
|
|
19
|
-
subagentMode: import("@deepseek-ai/schemastery").default<"follow-main" | "fixed", "follow-main" | "fixed", "volatile-defined">;
|
|
20
|
-
subagentProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
21
|
-
subagentModel: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
22
|
-
subagentReasoningEffort: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
23
|
-
searchProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
24
|
-
searchModel: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
25
|
-
imageProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
26
|
-
imageModel: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
27
|
-
compactOnSwitch: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
28
|
-
autoReviewProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
29
|
-
autoReviewModel: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
30
|
-
compactProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
31
|
-
compactModel: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
32
|
-
}>>, Schemastery.ObjectT<NoInfer<{
|
|
33
|
-
subagentMode: import("@deepseek-ai/schemastery").default<"follow-main" | "fixed", "follow-main" | "fixed", "volatile-defined">;
|
|
34
|
-
subagentProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
35
|
-
subagentModel: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
36
|
-
subagentReasoningEffort: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
37
|
-
searchProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
38
|
-
searchModel: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
39
|
-
imageProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
40
|
-
imageModel: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
41
|
-
compactOnSwitch: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
42
|
-
autoReviewProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
43
|
-
autoReviewModel: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
44
|
-
compactProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
45
|
-
compactModel: import("@deepseek-ai/schemastery").default<string, string, "volatile">;
|
|
46
|
-
}>>, "plain">;
|
|
18
|
+
static Config: import("@deepseek-ai/schemastery").default<any, Config, "plain">;
|
|
47
19
|
get capabilities(): {
|
|
48
20
|
searchProviderAdapters: {
|
|
49
21
|
available: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Volatile } from '@deepseek-ai/cordis';
|
|
2
2
|
import z from '@deepseek-ai/schemastery';
|
|
3
|
+
import { type ProviderGroupEntry, type ProviderGroups } from './provider-group.js';
|
|
3
4
|
export type SubagentMode = 'follow-main' | 'fixed';
|
|
4
5
|
/** Resolved live values consumed by Model Switch. */
|
|
5
6
|
export interface ModelSwitchSettings {
|
|
@@ -20,6 +21,8 @@ export interface ModelSwitchSettings {
|
|
|
20
21
|
compactProvider?: string;
|
|
21
22
|
/** Summarization model for `/compact` and automatic compaction; set together with `compactProvider`. */
|
|
22
23
|
compactModel?: string;
|
|
24
|
+
/** Per-provider compaction/subagent/review presets, keyed by provider id; absent when no group is saved. */
|
|
25
|
+
providerGroups?: ProviderGroups;
|
|
23
26
|
}
|
|
24
27
|
/** Loader-owned references; every field is directly editable in the profile form. */
|
|
25
28
|
export interface Config {
|
|
@@ -36,34 +39,13 @@ export interface Config {
|
|
|
36
39
|
autoReviewModel: Volatile<string | undefined>;
|
|
37
40
|
compactProvider: Volatile<string | undefined>;
|
|
38
41
|
compactModel: Volatile<string | undefined>;
|
|
42
|
+
/** Saved per-provider capability groups as one entry per provider; entry route fields are '' when the capability stays at its default. */
|
|
43
|
+
providerGroups: Volatile<readonly ProviderGroupEntry[]>;
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
searchModel: z<string, string, "volatile">;
|
|
47
|
-
imageProvider: z<string, string, "volatile">;
|
|
48
|
-
imageModel: z<string, string, "volatile">;
|
|
49
|
-
compactOnSwitch: z<boolean, boolean, "volatile-defined">;
|
|
50
|
-
autoReviewProvider: z<string, string, "volatile">;
|
|
51
|
-
autoReviewModel: z<string, string, "volatile">;
|
|
52
|
-
compactProvider: z<string, string, "volatile">;
|
|
53
|
-
compactModel: z<string, string, "volatile">;
|
|
54
|
-
}>>, Schemastery.ObjectT<NoInfer<{
|
|
55
|
-
subagentMode: z<"follow-main" | "fixed", "follow-main" | "fixed", "volatile-defined">;
|
|
56
|
-
subagentProvider: z<string, string, "volatile">;
|
|
57
|
-
subagentModel: z<string, string, "volatile">;
|
|
58
|
-
subagentReasoningEffort: z<string, string, "volatile">;
|
|
59
|
-
searchProvider: z<string, string, "volatile">;
|
|
60
|
-
searchModel: z<string, string, "volatile">;
|
|
61
|
-
imageProvider: z<string, string, "volatile">;
|
|
62
|
-
imageModel: z<string, string, "volatile">;
|
|
63
|
-
compactOnSwitch: z<boolean, boolean, "volatile-defined">;
|
|
64
|
-
autoReviewProvider: z<string, string, "volatile">;
|
|
65
|
-
autoReviewModel: z<string, string, "volatile">;
|
|
66
|
-
compactProvider: z<string, string, "volatile">;
|
|
67
|
-
compactModel: z<string, string, "volatile">;
|
|
68
|
-
}>>, "plain">;
|
|
45
|
+
/**
|
|
46
|
+
* Annotated rather than inferred: the nested entry schema's inferred type
|
|
47
|
+
* expands to cosmokit internals (`Dict`) that cannot be named in the emitted
|
|
48
|
+
* declaration, and the annotation pins the output shape to this interface.
|
|
49
|
+
*/
|
|
50
|
+
export declare const Config: z<any, Config, 'plain'>;
|
|
69
51
|
export declare function readConfig(config: Config): ModelSwitchSettings;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider configuration groups: per-provider presets for the compaction,
|
|
3
|
+
* subagent, and auto-review routes.
|
|
4
|
+
*
|
|
5
|
+
* A group stores the exact flat settings fields it will write, so applying a
|
|
6
|
+
* group is a plain settings-layer write: every capability consumer keeps
|
|
7
|
+
* reading the live values unchanged. All functions here are pure and never
|
|
8
|
+
* throw on malformed storage.
|
|
9
|
+
*/
|
|
10
|
+
import type { SubagentMode } from './host-settings.js';
|
|
11
|
+
/** Flat settings fields a provider group can pin — exactly what an application writes. */
|
|
12
|
+
export type ProviderGroupRouteField = 'compactProvider' | 'compactModel' | 'autoReviewProvider' | 'autoReviewModel' | 'subagentProvider' | 'subagentModel' | 'subagentReasoningEffort';
|
|
13
|
+
/** Stored entry: the owning provider plus every route field; '' means the capability stays at its default. */
|
|
14
|
+
export interface ProviderGroupEntry {
|
|
15
|
+
provider: string;
|
|
16
|
+
compactProvider: string;
|
|
17
|
+
compactModel: string;
|
|
18
|
+
autoReviewProvider: string;
|
|
19
|
+
autoReviewModel: string;
|
|
20
|
+
subagentProvider: string;
|
|
21
|
+
subagentModel: string;
|
|
22
|
+
subagentReasoningEffort: string;
|
|
23
|
+
}
|
|
24
|
+
/** Normalized view of one provider's group; absent fields follow their default state. */
|
|
25
|
+
export type ProviderGroupConfig = Partial<Record<ProviderGroupRouteField, string>>;
|
|
26
|
+
/** Normalized provider → group map. */
|
|
27
|
+
export type ProviderGroups = Readonly<Record<string, ProviderGroupConfig>>;
|
|
28
|
+
/** Storage shape of the volatile `providerGroups` config field: one entry per provider. */
|
|
29
|
+
export type ProviderGroupsStorage = readonly ProviderGroupEntry[];
|
|
30
|
+
/**
|
|
31
|
+
* Strip a raw stored value to well-formed groups; never throws. Both storage
|
|
32
|
+
* shapes are accepted: the stored entry array (keys live in each entry's
|
|
33
|
+
* `provider` field) and a hand-written provider → routes map (keys are the
|
|
34
|
+
* providers). All-empty entries stay: they pin a provider to every default.
|
|
35
|
+
*/
|
|
36
|
+
export declare function normalizeProviderGroups(raw: unknown): ProviderGroups;
|
|
37
|
+
/**
|
|
38
|
+
* The saved group for a provider.
|
|
39
|
+
* @returns the normalized group, or undefined when the provider has no group —
|
|
40
|
+
* which is what leaves the live routes untouched during a switch.
|
|
41
|
+
*/
|
|
42
|
+
export declare function groupOf(groups: ProviderGroups | undefined, provider: string): ProviderGroupConfig | undefined;
|
|
43
|
+
/** One application write; `value` undefined means "unset this field". */
|
|
44
|
+
export type ProviderGroupWrite = {
|
|
45
|
+
readonly field: 'subagentMode';
|
|
46
|
+
readonly mode: SubagentMode;
|
|
47
|
+
} | {
|
|
48
|
+
readonly field: ProviderGroupRouteField;
|
|
49
|
+
readonly value: string | undefined;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Ordered writes applying one group with full-replacement semantics: pinned
|
|
53
|
+
* capabilities land as pairs, everything else reverts to its default state
|
|
54
|
+
* (compaction/review follow the session model; Subagent follows official
|
|
55
|
+
* inherit). The mode write comes last so a fixed route never publishes with
|
|
56
|
+
* the previous provider's fields still half-written.
|
|
57
|
+
*/
|
|
58
|
+
export declare function providerGroupWrites(group: ProviderGroupConfig): ProviderGroupWrite[];
|
|
59
|
+
/** Storage entry for one provider's group; absent or blank route fields become ''. */
|
|
60
|
+
export declare function entryOf(provider: string, group: ProviderGroupConfig): ProviderGroupEntry;
|
|
61
|
+
/**
|
|
62
|
+
* The stored entry list with one provider's group replaced (an entry) or
|
|
63
|
+
* removed (undefined). Every entry is re-normalized and de-duplicated so a
|
|
64
|
+
* save can never write junk back; the input is never mutated. A blank
|
|
65
|
+
* provider only normalizes.
|
|
66
|
+
*/
|
|
67
|
+
export declare function withProviderGroup(storage: unknown, provider: string, group: ProviderGroupConfig | undefined): ProviderGroupsStorage;
|
|
68
|
+
/** Minimal writable settings surface an application needs; satisfied by the owned ConfigForm. */
|
|
69
|
+
export interface ProviderGroupSink {
|
|
70
|
+
set(field: 'subagentMode' | ProviderGroupRouteField, value: string): Promise<unknown>;
|
|
71
|
+
unset(field: ProviderGroupRouteField): Promise<unknown>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Apply a saved group's routes to the live flat settings.
|
|
75
|
+
* @returns true when a group existed and every write settled; false when the
|
|
76
|
+
* provider has no group (nothing was written). A rejected write propagates.
|
|
77
|
+
*/
|
|
78
|
+
export declare function applyProviderGroupTo(sink: ProviderGroupSink, groups: ProviderGroups | undefined, provider: string): Promise<boolean>;
|