@wy35002/dsh-model-fallback-ui 0.1.2-alpha.3

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/client.js ADDED
@@ -0,0 +1,1089 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@wy35002/dsh-model-fallback-ui",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
11
+ //#region \0dsh-css:E:\ai\DeepSeekHarness\deepseek-harness\packages\client\ui-model-fallback\src\client\ModelFallbackSection.module.css.mjs
12
+ const css = ".FSqZvG_section{max-width:760px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:14px;display:flex}.FSqZvG_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}.FSqZvG_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.FSqZvG_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px;line-height:20px}.FSqZvG_help{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.FSqZvG_fieldset{border:none;flex-direction:column;gap:10px;margin:0;padding:0;display:flex}.FSqZvG_legend{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.FSqZvG_empty{border:1px dashed var(--dsw-alias-border-l3);color:var(--dsw-alias-label-tertiary);text-align:center;border-radius:10px;margin:0;padding:14px 12px;font-size:13px;line-height:20px}.FSqZvG_chain{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.FSqZvG_row{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;grid-template-columns:28px minmax(140px,1.1fr) minmax(180px,1.5fr) 132px auto;align-items:end;gap:8px;padding:10px 12px;display:grid}.FSqZvG_pos{background:var(--dsw-alias-bg-module-platform);width:28px;height:32px;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;font-family:var(--ds-font-family-code);border-radius:8px;justify-content:center;align-items:center;display:inline-flex}.FSqZvG_cell,.FSqZvG_cellLong{flex-direction:column;gap:4px;min-width:0;display:flex}.FSqZvG_label{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.FSqZvG_select,.FSqZvG_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;width:100%;padding:0 10px;font-size:13px;line-height:20px}.FSqZvG_select{appearance:none;cursor:pointer;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 10px center;background-repeat:no-repeat;background-size:12px 12px;max-width:220px;padding-right:30px}.FSqZvG_input:focus,.FSqZvG_select:focus{border-color:var(--dsw-alias-brand-primary);outline:none}.FSqZvG_input:disabled,.FSqZvG_select:disabled{opacity:.6;cursor:default}.FSqZvG_rowActions{align-items:center;gap:4px;display:inline-flex}.FSqZvG_iconBtn,.FSqZvG_iconBtnDanger{box-sizing:border-box;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;font-size:12px;display:inline-flex}.FSqZvG_iconBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.FSqZvG_iconBtnDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}.FSqZvG_iconBtn:disabled,.FSqZvG_iconBtnDanger:disabled{opacity:.4;cursor:default}.FSqZvG_addRow{margin-top:2px}.FSqZvG_addInline{align-items:center;gap:8px;display:flex}.FSqZvG_addInline .FSqZvG_select{max-width:260px}.FSqZvG_modelPickerBar{align-items:center;gap:8px;display:flex}.FSqZvG_policyRow{align-items:center;gap:12px;display:flex}.FSqZvG_policyQ{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}.FSqZvG_toggle{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:8px;font-size:13px;line-height:20px;display:inline-flex}.FSqZvG_toggle input{accent-color:var(--dsw-alias-brand-primary)}.FSqZvG_toggle:has(input:disabled){opacity:.6;cursor:default}.FSqZvG_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:12px;padding-top:4px;display:flex}.FSqZvG_footerActions{align-items:center;gap:8px;display:inline-flex}.FSqZvG_status{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.FSqZvG_status.FSqZvG_saved{color:var(--dsw-alias-state-success-primary)}.FSqZvG_status.FSqZvG_error{color:var(--dsw-alias-state-error-primary)}.FSqZvG_modeBar{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:baseline;gap:10px;padding:10px 14px;display:flex}.FSqZvG_modeLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.FSqZvG_modeValue{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}.FSqZvG_overrideList{flex-direction:column;gap:6px;display:flex}.FSqZvG_overrideRow{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;grid-template-columns:minmax(130px,.9fr) 1fr;align-items:center;gap:10px;padding:8px 10px;display:grid}.FSqZvG_overrideProvider{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;font-family:var(--ds-font-family-code);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.FSqZvG_overrideIds{flex-wrap:wrap;gap:6px;display:flex}.FSqZvG_overrideChip{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-module-platform);font-size:11px;line-height:16px;font-family:var(--ds-font-family-code);border-radius:6px;align-items:center;gap:4px;padding:2px 6px;display:inline-flex}.FSqZvG_iconBtnSmall{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:4px;padding:1px 3px;font-size:11px;line-height:1}.FSqZvG_iconBtnSmall:hover:not(:disabled){color:var(--dsw-alias-state-error-primary)}.FSqZvG_iconBtnSmall:disabled{opacity:.4;cursor:default}.FSqZvG_providerList{grid-template-columns:1fr 1fr;gap:6px 16px;display:grid}.FSqZvG_pickerPanel{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;flex-direction:column;gap:8px;margin-top:10px;padding:10px 12px;display:flex}.FSqZvG_pickerHelp{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.FSqZvG_pickerChips{flex-wrap:wrap;gap:6px;display:flex}.FSqZvG_pickerChip,.FSqZvG_pickerChipAdded{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-module-platform);cursor:pointer;border-radius:16px;padding:4px 10px;font-size:12px;line-height:18px}.FSqZvG_pickerChip:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}.FSqZvG_pickerChipAdded{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);cursor:default;opacity:.8}";
13
+ const tagId = "@wy35002/dsh-model-fallback-ui/ModelFallbackSection.module.css";
14
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
15
+ const tag = document.createElement("style");
16
+ tag.dataset.plugin = "@wy35002/dsh-model-fallback-ui";
17
+ tag.dataset.pluginCss = tagId;
18
+ tag.textContent = css;
19
+ document.head.appendChild(tag);
20
+ }
21
+ var ModelFallbackSection_module_css_default = {
22
+ "addInline": "FSqZvG_addInline",
23
+ "addRow": "FSqZvG_addRow",
24
+ "cell": "FSqZvG_cell",
25
+ "cellLong": "FSqZvG_cellLong",
26
+ "chain": "FSqZvG_chain",
27
+ "empty": "FSqZvG_empty",
28
+ "error": "FSqZvG_error",
29
+ "fieldset": "FSqZvG_fieldset",
30
+ "footer": "FSqZvG_footer",
31
+ "footerActions": "FSqZvG_footerActions",
32
+ "help": "FSqZvG_help",
33
+ "iconBtn": "FSqZvG_iconBtn",
34
+ "iconBtnDanger": "FSqZvG_iconBtnDanger",
35
+ "iconBtnSmall": "FSqZvG_iconBtnSmall",
36
+ "input": "FSqZvG_input",
37
+ "intro": "FSqZvG_intro",
38
+ "label": "FSqZvG_label",
39
+ "legend": "FSqZvG_legend",
40
+ "modeBar": "FSqZvG_modeBar",
41
+ "modeLabel": "FSqZvG_modeLabel",
42
+ "modeValue": "FSqZvG_modeValue",
43
+ "modelPickerBar": "FSqZvG_modelPickerBar",
44
+ "overrideChip": "FSqZvG_overrideChip",
45
+ "overrideIds": "FSqZvG_overrideIds",
46
+ "overrideList": "FSqZvG_overrideList",
47
+ "overrideProvider": "FSqZvG_overrideProvider",
48
+ "overrideRow": "FSqZvG_overrideRow",
49
+ "pickerChip": "FSqZvG_pickerChip",
50
+ "pickerChipAdded": "FSqZvG_pickerChipAdded",
51
+ "pickerChips": "FSqZvG_pickerChips",
52
+ "pickerHelp": "FSqZvG_pickerHelp",
53
+ "pickerPanel": "FSqZvG_pickerPanel",
54
+ "policyQ": "FSqZvG_policyQ",
55
+ "policyRow": "FSqZvG_policyRow",
56
+ "pos": "FSqZvG_pos",
57
+ "providerList": "FSqZvG_providerList",
58
+ "row": "FSqZvG_row",
59
+ "rowActions": "FSqZvG_rowActions",
60
+ "saved": "FSqZvG_saved",
61
+ "section": "FSqZvG_section",
62
+ "select": "FSqZvG_select",
63
+ "status": "FSqZvG_status",
64
+ "title": "FSqZvG_title",
65
+ "toggle": "FSqZvG_toggle"
66
+ };
67
+ //#endregion
68
+ //#region src/client/ModelFallbackSection.tsx
69
+ /**
70
+ * Model-fallback settings section. Two modes resolved from the `model-fallback`
71
+ * settings namespace:
72
+ *
73
+ * - Auto (default): the plugin auto-discovers free models and tries them first
74
+ * before any paid model. The panel exposes free-model overrides and a
75
+ * per-provider "trust whole provider (unknown = free)" list.
76
+ * - Manual: an explicit, ordered fallback chain (shown only when the chain is
77
+ * non-empty).
78
+ *
79
+ * Edits are local until Save writes through the settings transport with
80
+ * revision fencing; Revert reloads the current namespace.
81
+ */
82
+ function providerOptions(providers) {
83
+ return providers.map((p) => ({
84
+ value: p.id,
85
+ label: p.name && p.name !== p.id ? `${p.name} (${p.id})` : p.id
86
+ }));
87
+ }
88
+ const KINDS = [
89
+ {
90
+ value: "primary",
91
+ en: "kindPrimary"
92
+ },
93
+ {
94
+ value: "free",
95
+ en: "kindFree"
96
+ },
97
+ {
98
+ value: "paid",
99
+ en: "kindPaid"
100
+ }
101
+ ];
102
+ const POLICIES = [
103
+ {
104
+ value: "ask",
105
+ en: "paidPolicyAsk"
106
+ },
107
+ {
108
+ value: "auto",
109
+ en: "paidPolicyAuto"
110
+ },
111
+ {
112
+ value: "never",
113
+ en: "paidPolicyNever"
114
+ }
115
+ ];
116
+ function statusClass(kind) {
117
+ const base = ModelFallbackSection_module_css_default.status ?? "";
118
+ if (kind === "saved") return `${base} ${ModelFallbackSection_module_css_default.statusSaved ?? ""}`;
119
+ if (kind === "error") return `${base} ${ModelFallbackSection_module_css_default.statusError ?? ""}`;
120
+ return base;
121
+ }
122
+ function modeKind(chain) {
123
+ return chain.length > 0 ? "manual" : "auto";
124
+ }
125
+ function parseCodes(raw) {
126
+ return raw.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
127
+ }
128
+ function modelLabel(model) {
129
+ return model.name !== void 0 && model.name !== model.id ? `${model.name} (${model.id})` : model.id;
130
+ }
131
+ /** A reusable "load this provider's configured models, then pick one" control.
132
+ * The provider is decided by the caller; this component only loads + picks. */
133
+ function ModelPicker(props) {
134
+ const { provider, writable, onLoadModels, onPick, isPicked, t } = props;
135
+ const [loading, setLoading] = (0, react.useState)(false);
136
+ const [picked, setPicked] = (0, react.useState)(null);
137
+ const [loadError, setLoadError] = (0, react.useState)(null);
138
+ const load = async () => {
139
+ if (!provider) return;
140
+ setLoading(true);
141
+ setLoadError(null);
142
+ const models = await onLoadModels(provider);
143
+ setLoading(false);
144
+ setPicked(models);
145
+ if (models.length === 0) setLoadError(t("modelsEmpty"));
146
+ };
147
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
149
+ className: ModelFallbackSection_module_css_default.modelPickerBar,
150
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
151
+ variant: "outline",
152
+ size: "sm",
153
+ disabled: !writable || !provider || loading,
154
+ onClick: () => void load(),
155
+ children: loading ? t("loadingModels") : t("loadModels")
156
+ })
157
+ }),
158
+ loadError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
159
+ className: ModelFallbackSection_module_css_default.error,
160
+ children: loadError
161
+ }),
162
+ picked && picked.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
163
+ className: ModelFallbackSection_module_css_default.pickerPanel,
164
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
165
+ className: ModelFallbackSection_module_css_default.pickerHelp,
166
+ children: t("modelPickerHelp")
167
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
168
+ className: ModelFallbackSection_module_css_default.pickerChips,
169
+ children: picked.map((model) => {
170
+ const added = isPicked(model.id);
171
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
172
+ className: added ? ModelFallbackSection_module_css_default.pickerChipAdded : ModelFallbackSection_module_css_default.pickerChip,
173
+ disabled: !writable || added,
174
+ onClick: () => onPick(model.id),
175
+ title: modelLabel(model),
176
+ children: [modelLabel(model), added ? ` · ${t("modelAdded")}` : ""]
177
+ }, model.id);
178
+ })
179
+ })]
180
+ })
181
+ ] });
182
+ }
183
+ /** Free-model overrides: per-provider lists of ids the user marks free. */
184
+ function FreeOverrides(props) {
185
+ const { groups, providers, writable, onAdd, onRemove, onLoadModels, isAdded, t } = props;
186
+ const firstProvider = providers[0];
187
+ const [provider, setProvider] = (0, react.useState)(firstProvider ? firstProvider.value : "");
188
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
189
+ className: ModelFallbackSection_module_css_default.fieldset,
190
+ children: [
191
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
192
+ className: ModelFallbackSection_module_css_default.legend,
193
+ children: t("freeLabel")
194
+ }),
195
+ groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
196
+ className: ModelFallbackSection_module_css_default.empty,
197
+ children: t("freeEmpty")
198
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
199
+ className: ModelFallbackSection_module_css_default.overrideList,
200
+ children: groups.map((group, gIdx) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
201
+ className: ModelFallbackSection_module_css_default.overrideRow,
202
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
203
+ className: ModelFallbackSection_module_css_default.overrideProvider,
204
+ children: group.provider
205
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
206
+ className: ModelFallbackSection_module_css_default.overrideIds,
207
+ children: group.ids.map((id, mIdx) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
208
+ className: ModelFallbackSection_module_css_default.overrideChip,
209
+ children: [id, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
210
+ className: ModelFallbackSection_module_css_default.iconBtnSmall,
211
+ disabled: !writable,
212
+ onClick: () => onRemove(gIdx, mIdx),
213
+ "aria-label": t("removeEntry"),
214
+ title: t("removeEntry"),
215
+ children: "✕"
216
+ })]
217
+ }, mIdx))
218
+ })]
219
+ }, gIdx))
220
+ }),
221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
222
+ className: ModelFallbackSection_module_css_default.addInline,
223
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
224
+ className: ModelFallbackSection_module_css_default.select,
225
+ value: provider,
226
+ disabled: !writable || providers.length === 0,
227
+ onChange: (e) => setProvider(e.target.value),
228
+ "aria-label": t("freeProvider"),
229
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
230
+ value: "",
231
+ children: providers.length === 0 ? t("providerEmpty") : t("freeProvider")
232
+ }), providers.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
233
+ value: opt.value,
234
+ children: opt.label
235
+ }, opt.value))]
236
+ })
237
+ }),
238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
239
+ provider,
240
+ writable,
241
+ onLoadModels,
242
+ onPick: (id) => onAdd(provider, id),
243
+ isPicked: (id) => isAdded(provider, id),
244
+ t
245
+ }),
246
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
247
+ className: ModelFallbackSection_module_css_default.help,
248
+ children: t("freeHelp")
249
+ })
250
+ ]
251
+ });
252
+ }
253
+ /** Trust-whole-provider list: every unknown model of a checked provider is free. */
254
+ function TrustProviders(props) {
255
+ const { providers, trusted, writable, onToggle, t } = props;
256
+ if (providers.length === 0) return null;
257
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
258
+ className: ModelFallbackSection_module_css_default.fieldset,
259
+ children: [
260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
261
+ className: ModelFallbackSection_module_css_default.legend,
262
+ children: t("unknownFreeLabel")
263
+ }),
264
+ providers.length > 0 && trusted.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
265
+ className: ModelFallbackSection_module_css_default.empty,
266
+ children: t("unknownFreeEmpty")
267
+ }) : null,
268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
269
+ className: ModelFallbackSection_module_css_default.providerList,
270
+ children: providers.map((p) => {
271
+ const label = p.name && p.name !== p.id ? `${p.name} (${p.id})` : p.id;
272
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
273
+ className: ModelFallbackSection_module_css_default.toggle,
274
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
275
+ type: "checkbox",
276
+ checked: trusted.includes(p.id),
277
+ disabled: !writable,
278
+ onChange: () => onToggle(p.id)
279
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
280
+ }, p.id);
281
+ })
282
+ }),
283
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
284
+ className: ModelFallbackSection_module_css_default.help,
285
+ children: t("unknownFreeHelp")
286
+ })
287
+ ]
288
+ });
289
+ }
290
+ /** Manual-mode explicit chain editor. */
291
+ function ChainEditor(props) {
292
+ const { chain, providers, writable, onLoadModels, actions, t } = props;
293
+ const [modelsCache, setModelsCache] = (0, react.useState)({});
294
+ const [loading, setLoading] = (0, react.useState)({});
295
+ const loadProvider = (provider) => {
296
+ if (!provider || modelsCache[provider] !== void 0 || loading[provider]) return;
297
+ setLoading((prev) => ({
298
+ ...prev,
299
+ [provider]: true
300
+ }));
301
+ onLoadModels(provider).then((models) => {
302
+ setModelsCache((prev) => ({
303
+ ...prev,
304
+ [provider]: models
305
+ }));
306
+ setLoading((prev) => ({
307
+ ...prev,
308
+ [provider]: false
309
+ }));
310
+ });
311
+ };
312
+ (0, react.useEffect)(() => {
313
+ for (const entry of chain) loadProvider(entry.provider);
314
+ }, [
315
+ chain,
316
+ modelsCache,
317
+ loading,
318
+ onLoadModels
319
+ ]);
320
+ function updateEntry(index, patch) {
321
+ const entry = chain[index];
322
+ if (!entry) return;
323
+ actions.setEntry(index, {
324
+ ...entry,
325
+ ...patch
326
+ });
327
+ }
328
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
329
+ className: ModelFallbackSection_module_css_default.fieldset,
330
+ children: [
331
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
332
+ className: ModelFallbackSection_module_css_default.legend,
333
+ children: t("chainLabel")
334
+ }),
335
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
336
+ className: ModelFallbackSection_module_css_default.help,
337
+ children: t("chainHelp")
338
+ }),
339
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
340
+ className: ModelFallbackSection_module_css_default.chain,
341
+ children: chain.map((entry, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
342
+ className: ModelFallbackSection_module_css_default.row,
343
+ children: [
344
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
345
+ className: ModelFallbackSection_module_css_default.pos,
346
+ "aria-hidden": true,
347
+ children: index + 1
348
+ }),
349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
350
+ className: ModelFallbackSection_module_css_default.cell,
351
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
352
+ className: ModelFallbackSection_module_css_default.label,
353
+ children: t("provider")
354
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
355
+ className: ModelFallbackSection_module_css_default.select,
356
+ value: entry.provider,
357
+ disabled: !writable,
358
+ onChange: (e) => {
359
+ const value = e.target.value;
360
+ updateEntry(index, {
361
+ provider: value,
362
+ model: ""
363
+ });
364
+ loadProvider(value);
365
+ },
366
+ "aria-label": t("provider"),
367
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
368
+ value: "",
369
+ children: providers.length === 0 ? t("providerEmpty") : t("provider")
370
+ }), providers.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
371
+ value: opt.value,
372
+ children: opt.label
373
+ }, opt.value))]
374
+ })]
375
+ }),
376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
377
+ className: ModelFallbackSection_module_css_default.cellLong,
378
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
379
+ className: ModelFallbackSection_module_css_default.label,
380
+ children: t("model")
381
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
382
+ className: ModelFallbackSection_module_css_default.select,
383
+ value: entry.model,
384
+ disabled: !writable || !entry.provider || loading[entry.provider] === true,
385
+ onChange: (e) => updateEntry(index, { model: e.target.value }),
386
+ "aria-label": t("model"),
387
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
388
+ value: "",
389
+ children: t("modelPlaceholder")
390
+ }), (modelsCache[entry.provider] ?? []).map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
391
+ value: m.id,
392
+ children: modelLabel(m)
393
+ }, m.id))]
394
+ })]
395
+ }),
396
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
397
+ className: ModelFallbackSection_module_css_default.cell,
398
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
399
+ className: ModelFallbackSection_module_css_default.label,
400
+ children: t("kind")
401
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
402
+ className: ModelFallbackSection_module_css_default.select,
403
+ value: entry.kind,
404
+ disabled: !writable,
405
+ onChange: (e) => updateEntry(index, { kind: e.target.value }),
406
+ "aria-label": t("kind"),
407
+ children: KINDS.map((k) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
408
+ value: k.value,
409
+ children: t(k.en)
410
+ }, k.value))
411
+ })]
412
+ }),
413
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
414
+ className: ModelFallbackSection_module_css_default.rowActions,
415
+ children: [
416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
417
+ className: ModelFallbackSection_module_css_default.iconBtn,
418
+ disabled: !writable || index <= 0,
419
+ onClick: () => actions.moveEntry(index, index - 1),
420
+ "aria-label": t("moveUp"),
421
+ title: t("moveUp"),
422
+ children: "▲"
423
+ }),
424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
425
+ className: ModelFallbackSection_module_css_default.iconBtn,
426
+ disabled: !writable || index >= chain.length - 1,
427
+ onClick: () => actions.moveEntry(index, index + 1),
428
+ "aria-label": t("moveDown"),
429
+ title: t("moveDown"),
430
+ children: "▼"
431
+ }),
432
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
433
+ className: ModelFallbackSection_module_css_default.iconBtnDanger,
434
+ disabled: !writable,
435
+ onClick: () => actions.removeEntry(index),
436
+ "aria-label": t("removeEntry"),
437
+ title: t("removeEntry"),
438
+ children: "✕"
439
+ })
440
+ ]
441
+ })
442
+ ]
443
+ }, index))
444
+ }),
445
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddEntryButton, {
446
+ writable,
447
+ onAdd: actions.addEntry,
448
+ t
449
+ })
450
+ ]
451
+ });
452
+ }
453
+ function AddEntryButton(props) {
454
+ const { writable, onAdd, t } = props;
455
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
456
+ variant: "outline",
457
+ size: "sm",
458
+ disabled: !writable,
459
+ onClick: () => onAdd(),
460
+ children: t("addEntry")
461
+ });
462
+ }
463
+ function Loaded(props) {
464
+ const { useSnapshot, actions, t } = props.injected;
465
+ const state = useSnapshot((snapshot) => snapshot);
466
+ const providerOpts = providerOptions(state.providers);
467
+ const mode = modeKind(state.chain);
468
+ const statusText = () => {
469
+ if (state.saved) return t("saved");
470
+ if (state.saving) return t("saving");
471
+ if (state.saveError || state.error) return state.saveError ?? state.error ?? void 0;
472
+ };
473
+ const statusKind = state.saved ? "saved" : state.error || state.saveError ? "error" : void 0;
474
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
475
+ className: ModelFallbackSection_module_css_default.section,
476
+ children: [
477
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
478
+ className: ModelFallbackSection_module_css_default.title,
479
+ children: t("title")
480
+ }),
481
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
482
+ className: ModelFallbackSection_module_css_default.intro,
483
+ children: t("intro")
484
+ }),
485
+ state.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
486
+ className: ModelFallbackSection_module_css_default.error,
487
+ children: t("loadFailed")
488
+ }),
489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
490
+ className: ModelFallbackSection_module_css_default.modeBar,
491
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
492
+ className: ModelFallbackSection_module_css_default.modeLabel,
493
+ children: [t("modeLabel"), ":"]
494
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
495
+ className: ModelFallbackSection_module_css_default.modeValue,
496
+ children: mode === "auto" ? t("modeAuto") : t("modeManual")
497
+ })]
498
+ }),
499
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FreeOverrides, {
500
+ groups: state.freeModelIds,
501
+ providers: providerOpts,
502
+ writable: state.writable,
503
+ onAdd: actions.addFreeModel,
504
+ onRemove: actions.removeFreeModel,
505
+ onLoadModels: actions.fetchProviderModels,
506
+ isAdded: (provider, id) => state.freeModelIds.some((g) => g.provider === provider && g.ids.includes(id)),
507
+ t
508
+ }),
509
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrustProviders, {
510
+ providers: state.providers,
511
+ trusted: state.unknownFreeProviders,
512
+ writable: state.writable,
513
+ onToggle: actions.toggleUnknownFreeProvider,
514
+ t
515
+ }),
516
+ mode === "manual" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainEditor, {
517
+ chain: state.chain,
518
+ providers: providerOpts,
519
+ writable: state.writable,
520
+ onLoadModels: actions.fetchProviderModels,
521
+ actions,
522
+ t
523
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
524
+ className: ModelFallbackSection_module_css_default.fieldset,
525
+ children: [
526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
527
+ className: ModelFallbackSection_module_css_default.legend,
528
+ children: t("chainLabel")
529
+ }),
530
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
531
+ className: ModelFallbackSection_module_css_default.empty,
532
+ children: t("chainEmpty")
533
+ }),
534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddEntryButton, {
535
+ writable: state.writable,
536
+ onAdd: actions.addEntry,
537
+ t
538
+ })
539
+ ]
540
+ }),
541
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
542
+ className: ModelFallbackSection_module_css_default.fieldset,
543
+ children: [
544
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
545
+ className: ModelFallbackSection_module_css_default.legend,
546
+ children: t("policyLabel")
547
+ }),
548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
549
+ className: ModelFallbackSection_module_css_default.policyRow,
550
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
551
+ className: ModelFallbackSection_module_css_default.policyQ,
552
+ children: t("paidPolicy")
553
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
554
+ className: ModelFallbackSection_module_css_default.select,
555
+ value: state.policy.paidSwitchPolicy,
556
+ disabled: !state.writable,
557
+ onChange: (e) => actions.setPolicy({ paidSwitchPolicy: e.target.value }),
558
+ "aria-label": t("paidPolicy"),
559
+ children: POLICIES.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
560
+ value: p.value,
561
+ children: t(p.en)
562
+ }, p.value))
563
+ })]
564
+ }),
565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
566
+ className: ModelFallbackSection_module_css_default.toggle,
567
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
568
+ type: "checkbox",
569
+ checked: state.policy.skipProviderOnQuota,
570
+ disabled: !state.writable,
571
+ onChange: (e) => actions.setPolicy({ skipProviderOnQuota: e.target.checked })
572
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("skipQuota") })]
573
+ }),
574
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
575
+ className: ModelFallbackSection_module_css_default.cell,
576
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
577
+ className: ModelFallbackSection_module_css_default.label,
578
+ children: t("switchCodesLabel")
579
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
580
+ className: ModelFallbackSection_module_css_default.input,
581
+ type: "text",
582
+ value: state.switchableCodes.join(" "),
583
+ placeholder: "RATE_LIMIT QUOTA",
584
+ disabled: !state.writable,
585
+ onChange: (e) => actions.setSwitchableCodes(parseCodes(e.target.value)),
586
+ "aria-label": t("switchCodesLabel")
587
+ })]
588
+ }),
589
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
590
+ className: ModelFallbackSection_module_css_default.help,
591
+ children: t("switchCodesHelp")
592
+ })
593
+ ]
594
+ }),
595
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
596
+ className: ModelFallbackSection_module_css_default.help,
597
+ children: t("modelHelp")
598
+ }),
599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
600
+ className: ModelFallbackSection_module_css_default.footer,
601
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
602
+ className: statusClass(statusKind),
603
+ children: statusText()
604
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
605
+ className: ModelFallbackSection_module_css_default.footerActions,
606
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
607
+ variant: "outline",
608
+ disabled: !state.writable || state.saving,
609
+ onClick: () => actions.revert(),
610
+ children: t("revert")
611
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
612
+ disabled: !state.writable || state.saving,
613
+ onClick: () => void actions.save(),
614
+ children: state.saving ? t("saving") : t("save")
615
+ })]
616
+ })]
617
+ })
618
+ ]
619
+ });
620
+ }
621
+ function ModelFallbackSection(props) {
622
+ const useSnapshot = props.useSnapshot;
623
+ if (props.controller === void 0 || useSnapshot === void 0) return null;
624
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Loaded, {
625
+ injected: props,
626
+ useSnapshot
627
+ });
628
+ }
629
+ //#endregion
630
+ //#region src/client/store.ts
631
+ const NAMESPACE = "model-fallback";
632
+ /** Must match the host plugin's DEFAULT_SWITCHABLE_CODES. */
633
+ const DEFAULT_SWITCHABLE_CODES = Object.freeze(["RATE_LIMIT", "QUOTA"]);
634
+ function decodeNamespaceValue(value) {
635
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
636
+ const raw = value;
637
+ const out = {};
638
+ if (Array.isArray(raw.chain)) out.chain = raw.chain;
639
+ if (typeof raw.paidSwitchPolicy === "string") out.paidSwitchPolicy = raw.paidSwitchPolicy;
640
+ if (typeof raw.skipProviderOnQuota === "boolean") out.skipProviderOnQuota = raw.skipProviderOnQuota;
641
+ if (Array.isArray(raw.switchableCodes)) out.switchableCodes = raw.switchableCodes;
642
+ if (Array.isArray(raw.freeModelIds)) out.freeModelIds = raw.freeModelIds.filter((r) => typeof r === "object" && r !== null && "provider" in r && typeof r.provider === "string" && Array.isArray(r.ids));
643
+ if (Array.isArray(raw.paidModelIds)) out.paidModelIds = raw.paidModelIds.filter((r) => typeof r === "object" && r !== null && "provider" in r && typeof r.provider === "string" && Array.isArray(r.ids));
644
+ if (Array.isArray(raw.unknownFreeProviders)) out.unknownFreeProviders = raw.unknownFreeProviders;
645
+ return out;
646
+ }
647
+ function defaultPolicy() {
648
+ return {
649
+ paidSwitchPolicy: "ask",
650
+ skipProviderOnQuota: true
651
+ };
652
+ }
653
+ function findGroup(list, provider) {
654
+ return list.findIndex((g) => g.provider === provider);
655
+ }
656
+ function mergeGroup(list, provider, modelId, existing) {
657
+ const idx = findGroup(list, provider);
658
+ if (idx < 0) return [...list, {
659
+ provider,
660
+ ids: [modelId]
661
+ }];
662
+ const next = [...list];
663
+ next[idx] = {
664
+ provider,
665
+ ids: existing.length === 0 ? [modelId] : [...existing, modelId]
666
+ };
667
+ return next;
668
+ }
669
+ function removeIdFromGroup(list, groupIndex, modelIndex) {
670
+ const group = list[groupIndex];
671
+ if (group === void 0) return list;
672
+ const ids = [...group.ids];
673
+ ids.splice(modelIndex, 1);
674
+ if (ids.length === 0) {
675
+ const next = [...list];
676
+ next.splice(groupIndex, 1);
677
+ return next;
678
+ }
679
+ const next = [...list];
680
+ next[groupIndex] = {
681
+ ...group,
682
+ ids
683
+ };
684
+ return next;
685
+ }
686
+ function buildOps(chain, policy, switchableCodes, freeModelIds, paidModelIds, unknownFreeProviders) {
687
+ return [
688
+ {
689
+ op: "set",
690
+ path: ["chain"],
691
+ value: chain
692
+ },
693
+ {
694
+ op: "set",
695
+ path: ["paidSwitchPolicy"],
696
+ value: policy.paidSwitchPolicy
697
+ },
698
+ {
699
+ op: "set",
700
+ path: ["skipProviderOnQuota"],
701
+ value: policy.skipProviderOnQuota
702
+ },
703
+ {
704
+ op: "set",
705
+ path: ["switchableCodes"],
706
+ value: switchableCodes
707
+ },
708
+ {
709
+ op: "set",
710
+ path: ["freeModelIds"],
711
+ value: freeModelIds
712
+ },
713
+ {
714
+ op: "set",
715
+ path: ["paidModelIds"],
716
+ value: paidModelIds
717
+ },
718
+ {
719
+ op: "set",
720
+ path: ["unknownFreeProviders"],
721
+ value: unknownFreeProviders
722
+ }
723
+ ];
724
+ }
725
+ var ModelFallbackStore = class {
726
+ ctx;
727
+ scope;
728
+ store;
729
+ disposers = [];
730
+ constructor(ctx, scope) {
731
+ this.ctx = ctx;
732
+ this.scope = scope;
733
+ const init = {
734
+ status: "loading",
735
+ error: null,
736
+ writable: false,
737
+ revision: void 0,
738
+ chain: [],
739
+ policy: defaultPolicy(),
740
+ switchableCodes: [...DEFAULT_SWITCHABLE_CODES],
741
+ freeModelIds: [],
742
+ paidModelIds: [],
743
+ unknownFreeProviders: [],
744
+ providers: [],
745
+ saving: false,
746
+ saveError: null,
747
+ saved: null
748
+ };
749
+ this.store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(init);
750
+ this.disposers.push(this.scope.subscribe(() => this.refetchFromScope()));
751
+ this.load();
752
+ }
753
+ refetchFromScope() {
754
+ const snap = this.scope.getSnapshot();
755
+ if (snap.value === void 0) return;
756
+ const decoded = decodeNamespaceValue(snap.value);
757
+ if (decoded === void 0) return;
758
+ const policy = decoded.paidSwitchPolicy === "ask" || decoded.paidSwitchPolicy === "auto" || decoded.paidSwitchPolicy === "never" ? decoded.paidSwitchPolicy : "ask";
759
+ this.store.update((draft) => {
760
+ draft.revision = snap.revision;
761
+ draft.writable = snap.writable;
762
+ draft.chain = decoded.chain ?? [];
763
+ draft.policy = {
764
+ paidSwitchPolicy: policy,
765
+ skipProviderOnQuota: decoded.skipProviderOnQuota ?? true
766
+ };
767
+ const codes = decoded.switchableCodes ?? [];
768
+ draft.switchableCodes = codes.length > 0 ? codes : [...DEFAULT_SWITCHABLE_CODES];
769
+ draft.freeModelIds = decoded.freeModelIds ?? [];
770
+ draft.paidModelIds = decoded.paidModelIds ?? [];
771
+ draft.unknownFreeProviders = decoded.unknownFreeProviders ?? [];
772
+ });
773
+ }
774
+ async load() {
775
+ this.store.update((draft) => {
776
+ draft.status = "loading";
777
+ draft.error = null;
778
+ });
779
+ const providers = await this.fetchProviders();
780
+ this.store.update((draft) => {
781
+ draft.status = "ready";
782
+ draft.providers = providers;
783
+ if (draft.chain.length === 0 && draft.revision !== void 0) this.refetchFromScope();
784
+ });
785
+ }
786
+ async fetchProviders() {
787
+ try {
788
+ const result = await this.ctx.remote.llm.listProviders();
789
+ return result.ok ? result.value : [];
790
+ } catch {
791
+ return [];
792
+ }
793
+ }
794
+ actions = {
795
+ load: () => this.load(),
796
+ setChain: (chain) => {
797
+ this.store.update((draft) => {
798
+ draft.chain = [...chain];
799
+ });
800
+ },
801
+ setEntry: (index, entry) => {
802
+ this.store.update((draft) => {
803
+ if (index < 0 || index >= draft.chain.length) return;
804
+ draft.chain = draft.chain.map((e, i) => i === index ? { ...entry } : e);
805
+ });
806
+ },
807
+ addEntry: () => {
808
+ this.store.update((draft) => {
809
+ draft.chain = [...draft.chain, {
810
+ provider: "",
811
+ model: "",
812
+ kind: "free"
813
+ }];
814
+ });
815
+ },
816
+ removeEntry: (index) => {
817
+ this.store.update((draft) => {
818
+ draft.chain = draft.chain.filter((_e, i) => i !== index);
819
+ });
820
+ },
821
+ moveEntry: (from, to) => {
822
+ this.store.update((draft) => {
823
+ if (to < 0 || to >= draft.chain.length || from === to) return;
824
+ const next = [...draft.chain];
825
+ const entry = next.splice(from, 1)[0];
826
+ if (entry === void 0) return;
827
+ next.splice(to, 0, entry);
828
+ draft.chain = next;
829
+ });
830
+ },
831
+ setPolicy: (policy) => {
832
+ this.store.update((draft) => {
833
+ draft.policy = {
834
+ ...draft.policy,
835
+ ...policy
836
+ };
837
+ });
838
+ },
839
+ setSwitchableCodes: (codes) => {
840
+ this.store.update((draft) => {
841
+ draft.switchableCodes = [...codes];
842
+ });
843
+ },
844
+ addFreeModel: (provider, modelId) => {
845
+ this.store.update((draft) => {
846
+ if (!modelId.trim()) return;
847
+ const existing = findGroup(draft.freeModelIds, provider);
848
+ const group = draft.freeModelIds[existing];
849
+ if (group !== void 0 && group.ids.includes(modelId.trim())) return;
850
+ draft.freeModelIds = mergeGroup(draft.freeModelIds, provider, modelId.trim(), group?.ids ?? []);
851
+ });
852
+ },
853
+ removeFreeModel: (groupIndex, modelIndex) => {
854
+ this.store.update((draft) => {
855
+ draft.freeModelIds = removeIdFromGroup(draft.freeModelIds, groupIndex, modelIndex);
856
+ });
857
+ },
858
+ addPaidModel: (provider, modelId) => {
859
+ this.store.update((draft) => {
860
+ if (!modelId.trim()) return;
861
+ const existing = findGroup(draft.paidModelIds, provider);
862
+ const group = draft.paidModelIds[existing];
863
+ if (group !== void 0 && group.ids.includes(modelId.trim())) return;
864
+ draft.paidModelIds = mergeGroup(draft.paidModelIds, provider, modelId.trim(), group?.ids ?? []);
865
+ });
866
+ },
867
+ removePaidModel: (groupIndex, modelIndex) => {
868
+ this.store.update((draft) => {
869
+ draft.paidModelIds = removeIdFromGroup(draft.paidModelIds, groupIndex, modelIndex);
870
+ });
871
+ },
872
+ toggleUnknownFreeProvider: (provider) => {
873
+ this.store.update((draft) => {
874
+ if (draft.unknownFreeProviders.includes(provider)) draft.unknownFreeProviders = draft.unknownFreeProviders.filter((p) => p !== provider);
875
+ else draft.unknownFreeProviders = [...draft.unknownFreeProviders, provider];
876
+ });
877
+ },
878
+ fetchProviderModels: async (provider) => {
879
+ try {
880
+ const result = await this.ctx.remote.llm.listModels(provider);
881
+ return result.ok ? result.value : [];
882
+ } catch {
883
+ return [];
884
+ }
885
+ },
886
+ save: async () => {
887
+ const cur = this.store.getSnapshot();
888
+ if (!cur.writable) return "error";
889
+ this.store.update((draft) => {
890
+ draft.saving = true;
891
+ draft.saveError = null;
892
+ draft.saved = null;
893
+ });
894
+ try {
895
+ const result = await this.ctx.remote.settings.mutate(NAMESPACE, buildOps(cur.chain, cur.policy, cur.switchableCodes, cur.freeModelIds, cur.paidModelIds, cur.unknownFreeProviders), cur.revision);
896
+ if (result.ok) {
897
+ this.store.update((draft) => {
898
+ draft.saving = false;
899
+ draft.saved = true;
900
+ setTimeout(() => this.store.update((d) => {
901
+ d.saved = null;
902
+ }), 1800);
903
+ });
904
+ return "ok";
905
+ }
906
+ return result.error?.code === "settings/conflict" ? "conflict" : "error";
907
+ } catch {
908
+ return "error";
909
+ } finally {
910
+ this.store.update((draft) => {
911
+ draft.saving = false;
912
+ });
913
+ }
914
+ },
915
+ revert: () => {
916
+ this.load();
917
+ }
918
+ };
919
+ dispose() {
920
+ for (const d of this.disposers) d();
921
+ this.disposers = [];
922
+ }
923
+ };
924
+ //#endregion
925
+ //#region src/client/locales.ts
926
+ /** Copy dictionaries for the model-fallback settings section. */
927
+ const en = {
928
+ nav: "Model fallback",
929
+ title: "Model fallback",
930
+ intro: "When the current model is rate-limited or out of quota the plugin auto-switches: free models are tried first (discovered automatically), then any paid model only if your policy allows. Override which models count as free, or trust a whole provider.",
931
+ modeLabel: "Mode",
932
+ modeAuto: "Auto (discovered free pool, fail-closed)",
933
+ modeManual: "Manual (explicit chain)",
934
+ chainLabel: "Fallback chain",
935
+ chainEmpty: "No chain configured. Add entries below to switch to explicit chain mode; otherwise the plugin runs in auto mode.",
936
+ chainHelp: "Explicit, ordered chain. Only models you list here are tried, in this order.",
937
+ pos: "Order",
938
+ provider: "Provider",
939
+ model: "Model",
940
+ modelPlaceholder: "Select a model",
941
+ modelHelp: "Enter a model id. Use the Models settings page or the model-fallback-discover tool to see what each provider offers.",
942
+ kind: "Tier",
943
+ kindPrimary: "Primary",
944
+ kindFree: "Free",
945
+ kindPaid: "Paid",
946
+ addEntry: "Add entry",
947
+ removeEntry: "Remove",
948
+ moveUp: "Move up",
949
+ moveDown: "Move down",
950
+ freeLabel: "Free-model overrides",
951
+ freeEmpty: "No overrides. The plugin uses its built-in community free-model table.",
952
+ freeHelp: "Pick a provider, then load and click its configured models to mark them free beyond the built-in table.",
953
+ freeProvider: "Provider",
954
+ addFree: "Add",
955
+ loadModels: "Load models",
956
+ loadingModels: "Loading…",
957
+ modelsEmpty: "This provider reports no models.",
958
+ modelPickerHelp: "Click a model to add it as free.",
959
+ modelAdded: "Added",
960
+ unknownFreeLabel: "Trust whole provider (treat unknown models as free)",
961
+ unknownFreeEmpty: "None — unknown models are treated as paid (fail-closed).",
962
+ unknownFreeHelp: "Check a provider you trust entirely; its unrecognized models are counted as free.",
963
+ policyLabel: "Switching policy",
964
+ paidPolicy: "Switching into a paid model",
965
+ paidPolicyAsk: "Ask me each time",
966
+ paidPolicyAuto: "Switch automatically",
967
+ paidPolicyNever: "Never switch to paid",
968
+ switchCodesLabel: "Fail codes that trigger a switch",
969
+ switchCodesHelp: "Space-separated. RATE_LIMIT and QUOTA are the defaults.",
970
+ skipQuota: "On quota exhaustion, skip the rest of the same provider",
971
+ save: "Save",
972
+ saving: "Saving…",
973
+ saved: "Saved",
974
+ revert: "Revert",
975
+ readOnly: "The settings document is read-only in this deployment.",
976
+ loadFailed: "Could not load the fallback settings",
977
+ conflict: "The settings changed while you were editing. Reopen to see the current values.",
978
+ writeError: "Could not save the settings",
979
+ providerEmpty: "No registered providers"
980
+ };
981
+ const zh = {
982
+ nav: "模型回退",
983
+ title: "模型回退",
984
+ intro: "当前模型被限流或额度耗尽时,插件自动切换:先自动发现并尝试免费模型,再在策略允许时尝试付费模型。可覆盖哪些模型算免费,或直接信任整个提供方。",
985
+ modeLabel: "模式",
986
+ modeAuto: "自动(自动发现免费池,未知即失败关停)",
987
+ modeManual: "手动(显式链路)",
988
+ chainLabel: "回退链路",
989
+ chainEmpty: "尚未配置链路。在下方添加条目即可切换到手动链路模式;否则插件以自动模式运行。",
990
+ chainHelp: "显式、有序链路。仅尝试你在此列出的模型,且按此顺序。",
991
+ pos: "顺序",
992
+ provider: "提供方",
993
+ model: "模型",
994
+ modelPlaceholder: "选择模型",
995
+ modelHelp: "输入模型 ID。可用「模型」设置页或 model-fallback-discover 工具查看各提供方可用模型。",
996
+ kind: "档位",
997
+ kindPrimary: "主用",
998
+ kindFree: "免费",
999
+ kindPaid: "付费",
1000
+ addEntry: "添加条目",
1001
+ removeEntry: "删除",
1002
+ moveUp: "上移",
1003
+ moveDown: "下移",
1004
+ freeLabel: "免费模型覆盖",
1005
+ freeEmpty: "无覆盖。插件使用内置的社区免费模型表。",
1006
+ freeHelp: "选择一个提供方,然后加载并点选其已配置的模型,标记为内置表之外的免费模型。",
1007
+ freeProvider: "提供方",
1008
+ addFree: "添加",
1009
+ loadModels: "加载模型",
1010
+ loadingModels: "加载中…",
1011
+ modelsEmpty: "该提供方未报告任何模型。",
1012
+ modelPickerHelp: "点击一个模型以标记为免费。",
1013
+ modelAdded: "已添加",
1014
+ unknownFreeLabel: "信任整个提供方(未知模型视为免费)",
1015
+ unknownFreeEmpty: "无——未知模型一律视为付费(失败关停)。",
1016
+ unknownFreeHelp: "勾选你完全信任的提供方;其未识别的模型将被计入免费。",
1017
+ policyLabel: "切换策略",
1018
+ paidPolicy: "切换到付费模型时",
1019
+ paidPolicyAsk: "每次都问我",
1020
+ paidPolicyAuto: "自动切换",
1021
+ paidPolicyNever: "绝不切到付费",
1022
+ switchCodesLabel: "触发切换的失败码",
1023
+ switchCodesHelp: "空格分隔。默认 RATE_LIMIT 和 QUOTA。",
1024
+ skipQuota: "额度耗尽时跳过同一提供方其余模型",
1025
+ save: "保存",
1026
+ saving: "保存中…",
1027
+ saved: "已保存",
1028
+ revert: "放弃",
1029
+ readOnly: "当前部署的设置文档为只读。",
1030
+ loadFailed: "无法加载回退设置",
1031
+ conflict: "编辑期间设置已被修改,请重新打开查看当前值。",
1032
+ writeError: "保存设置失败",
1033
+ providerEmpty: "没有已注册的提供方"
1034
+ };
1035
+ //#endregion
1036
+ //#region src/client/index.ts
1037
+ const NS = "model.fallback";
1038
+ const SETTINGS_NAMESPACE = "model-fallback";
1039
+ function decodeModelFallback(value) {
1040
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1041
+ const raw = value;
1042
+ const out = {};
1043
+ if (Array.isArray(raw.chain)) out.chain = raw.chain;
1044
+ return out;
1045
+ }
1046
+ const inject = [
1047
+ "slots",
1048
+ "locale",
1049
+ "remote",
1050
+ "remote.llm",
1051
+ "remote.settings",
1052
+ "settingsScope"
1053
+ ];
1054
+ function apply(ctx) {
1055
+ ctx.effect(() => ctx.locale.register(NS, {
1056
+ zh,
1057
+ en
1058
+ }), "ui-model-fallback: copy dictionaries");
1059
+ const controller = new ModelFallbackStore(ctx, ctx.settingsScope.bind({
1060
+ namespace: SETTINGS_NAMESPACE,
1061
+ decode: decodeModelFallback
1062
+ }));
1063
+ const t = ctx.locale.bind(NS);
1064
+ const injected = () => ({
1065
+ controller,
1066
+ hooks: { snapshot: controller.store },
1067
+ actions: controller.actions,
1068
+ t
1069
+ });
1070
+ ctx.effect(() => () => {
1071
+ controller.dispose();
1072
+ }, "ui-model-fallback: store teardown");
1073
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1074
+ name: "settings.section",
1075
+ id: "model-fallback",
1076
+ order: 100,
1077
+ label: () => t("nav"),
1078
+ locale: NS,
1079
+ inject: injected
1080
+ }, ModelFallbackSection));
1081
+ }
1082
+ //#endregion
1083
+ exports.apply = apply;
1084
+ exports.inject = inject;
1085
+ return module.exports;
1086
+ }
1087
+ });
1088
+
1089
+ //# sourceMappingURL=client.js.map