dsh-plugin-effort-declare 0.1.3 → 0.1.4

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 CHANGED
@@ -36,6 +36,13 @@ window.__ModuleLoader__.load({
36
36
  const LLM_PI_AI_NS = "llm-pi-ai";
37
37
  /** Any route key walks a dict schema to the same profile node. */
38
38
  const SCHEMA_PROBE_ROUTE = "\0probe";
39
+ /** Request modalities llm-pi-ai accepts on `models[].input` / `defaultInput`. */
40
+ const INPUT_MODALITIES = ["text", "image"];
41
+ /**
42
+ * Canonical write for a hand-declared vision model.
43
+ * Must include `text`; image-only is not a serviceable OpenAI-completions route.
44
+ */
45
+ const IMAGE_CAPABLE_INPUT = ["text", "image"];
39
46
  //#endregion
40
47
  //#region src/core/paths.ts
41
48
  /**
@@ -231,25 +238,29 @@ window.__ModuleLoader__.load({
231
238
  }
232
239
  return map;
233
240
  }
234
- function effortsPresence(row) {
235
- if (row === void 0 || !Object.hasOwn(row, "reasoningEfforts")) return {
241
+ /** Model-row fields this page edits; other keys follow the Models page. */
242
+ const MODEL_OVERLAY_KEYS = ["reasoningEfforts", "input"];
243
+ function fieldPresence(row, key) {
244
+ if (row === void 0 || !Object.hasOwn(row, key)) return {
236
245
  present: false,
237
246
  value: void 0
238
247
  };
239
248
  return {
240
249
  present: true,
241
- value: row.reasoningEfforts
250
+ value: row[key]
242
251
  };
243
252
  }
244
- function effortsEqual(left, right) {
245
- if (left.present !== right.present) return false;
246
- if (!left.present) return true;
247
- return sliceEqual(left.value, right.value);
253
+ function fieldEqual(left, right, key) {
254
+ const a = fieldPresence(left, key);
255
+ const b = fieldPresence(right, key);
256
+ if (a.present !== b.present) return false;
257
+ if (!a.present) return true;
258
+ return sliceEqual(a.value, b.value);
248
259
  }
249
- function overlayLocalEfforts(incomingRow, prevRow) {
260
+ function overlayLocalFields(incomingRow, prevRow, keys) {
250
261
  const next = structuredClone(incomingRow);
251
- if (Object.hasOwn(prevRow, "reasoningEfforts")) next.reasoningEfforts = structuredClone(prevRow.reasoningEfforts);
252
- else delete next.reasoningEfforts;
262
+ for (const key of keys) if (Object.hasOwn(prevRow, key)) next[key] = structuredClone(prevRow[key]);
263
+ else delete next[key];
253
264
  return next;
254
265
  }
255
266
  function objectKeyChanged(left, right, key) {
@@ -260,7 +271,8 @@ window.__ModuleLoader__.load({
260
271
  }
261
272
  /**
262
273
  * Membership follows the latest user-layer models list (Models page add/delete).
263
- * Local unsaved `reasoningEfforts` (including a cleared key) overlay by id.
274
+ * Local unsaved overlay keys (including a cleared key) overlay by id; other
275
+ * fields on the row follow incoming.
264
276
  */
265
277
  function mergeModelsById(args) {
266
278
  const prevById = indexById(args.prevModels);
@@ -273,16 +285,19 @@ window.__ModuleLoader__.load({
273
285
  const prevRow = prevById.get(id);
274
286
  if (prevRow === void 0) return structuredClone(incomingRow);
275
287
  const prevOrig = prevOrigById.get(id);
276
- if (!!effortsEqual(effortsPresence(prevRow), effortsPresence(prevOrig))) return structuredClone(incomingRow);
288
+ const dirtyKeys = MODEL_OVERLAY_KEYS.filter((key) => !fieldEqual(prevRow, prevOrig, key));
289
+ if (dirtyKeys.length === 0) return structuredClone(incomingRow);
277
290
  const incomingOrig = incomingOrigById.get(id);
278
- if (!effortsEqual(effortsPresence(prevOrig), effortsPresence(incomingOrig))) conflicted = true;
279
- return overlayLocalEfforts(incomingRow, prevRow);
291
+ for (const key of dirtyKeys) if (!fieldEqual(prevOrig, incomingOrig, key)) conflicted = true;
292
+ return overlayLocalFields(incomingRow, prevRow, dirtyKeys);
280
293
  });
281
294
  for (const [id, prevRow] of prevById) {
282
295
  if (incomingIds.has(id)) continue;
283
296
  const prevOrig = prevOrigById.get(id);
284
- if (effortsEqual(effortsPresence(prevRow), effortsPresence(prevOrig))) continue;
285
- if (!effortsEqual(effortsPresence(prevOrig), effortsPresence(incomingOrigById.get(id)))) conflicted = true;
297
+ const dirtyKeys = MODEL_OVERLAY_KEYS.filter((key) => !fieldEqual(prevRow, prevOrig, key));
298
+ if (dirtyKeys.length === 0) continue;
299
+ const incomingOrig = incomingOrigById.get(id);
300
+ for (const key of dirtyKeys) if (!fieldEqual(prevOrig, incomingOrig, key)) conflicted = true;
286
301
  }
287
302
  return {
288
303
  models,
@@ -314,9 +329,9 @@ window.__ModuleLoader__.load({
314
329
  }
315
330
  /**
316
331
  * Apply a freshly loaded table. Membership and metadata follow incoming;
317
- * unsaved reasoningEfforts / dirty compat keys overlay by id. Conflict only
318
- * when a locally dirty field also changed in originals (revision-only bumps
319
- * and sibling-card saves do not warn).
332
+ * unsaved overlay keys (`reasoningEfforts`, `input`) / dirty compat keys
333
+ * overlay by id. Conflict only when a locally dirty field also changed in
334
+ * originals (revision-only bumps and sibling-card saves do not warn).
320
335
  */
321
336
  function mergeLoadedDrafts(current, incoming, options) {
322
337
  const currentByProvider = new Map(current.map((draft) => [draft.provider, draft]));
@@ -524,12 +539,58 @@ window.__ModuleLoader__.load({
524
539
  return next;
525
540
  }
526
541
  //#endregion
542
+ //#region src/core/input.ts
543
+ /**
544
+ * Per-model `input` modalities as stored in llm-pi-ai settings.
545
+ * Absence falls through to route `defaultInput` (`[text]`). Empty list is
546
+ * treated as absent by the official resolver — do not write `[]`.
547
+ */
548
+ /** Whether the stored `input` list includes image. Absence is false. */
549
+ function readImageCapable(row) {
550
+ const value = row.input;
551
+ return Array.isArray(value) && value.includes("image");
552
+ }
553
+ /**
554
+ * Declare or drop image input on a spread row.
555
+ * Enabled writes `[text, image]`; disabled unsets the key (not `[]`, not `[text]`).
556
+ */
557
+ function writeImageCapable(row, enabled) {
558
+ const next = { ...row };
559
+ if (enabled) next.input = [...IMAGE_CAPABLE_INPUT];
560
+ else delete next.input;
561
+ return next;
562
+ }
563
+ /**
564
+ * Validate a stored `input` field. Returns an error code; never throws.
565
+ * Absence is valid. Empty list is rejected so a bad stamp cannot be re-saved.
566
+ */
567
+ function validateInput(input) {
568
+ if (input === void 0) return void 0;
569
+ if (!Array.isArray(input) || input.length === 0) return "bad-input";
570
+ const known = new Set(INPUT_MODALITIES);
571
+ const seen = /* @__PURE__ */ new Set();
572
+ for (const item of input) {
573
+ if (typeof item !== "string" || !known.has(item) || seen.has(item)) return "bad-input";
574
+ seen.add(item);
575
+ }
576
+ if (!seen.has("text")) return "bad-input";
577
+ }
578
+ /** Per-model client-side check used to show errors without throwing. */
579
+ function modelInputError(row) {
580
+ if (!("input" in row) || row.input === void 0) return void 0;
581
+ return validateInput(row.input);
582
+ }
583
+ //#endregion
527
584
  //#region src/core/validate.ts
528
585
  /** Per-model client-side check used to show errors without throwing. */
529
586
  function modelEffortError(row) {
530
587
  if (!("reasoningEfforts" in row) || row.reasoningEfforts === void 0) return void 0;
531
588
  return validateReasoningEfforts(row.reasoningEfforts);
532
589
  }
590
+ /** First blocking error on a model row (efforts, then input). */
591
+ function modelRowError(row) {
592
+ return modelEffortError(row) ?? modelInputError(row);
593
+ }
533
594
  //#endregion
534
595
  //#region src/core/attribution.ts
535
596
  /**
@@ -555,7 +616,7 @@ window.__ModuleLoader__.load({
555
616
  * package.json at DSH startup — host apply is empty and the settings page
556
617
  * runs in the browser.
557
618
  */
558
- const PLUGIN_FOOTER_TEXT = formatAttribution("0.1.3", COPYRIGHT_FROM, 2026);
619
+ const PLUGIN_FOOTER_TEXT = formatAttribution("0.1.4", COPYRIGHT_FROM, 2026);
559
620
  //#endregion
560
621
  //#region src/client/load-drafts.ts
561
622
  function schemaDefaultString(node) {
@@ -759,8 +820,8 @@ window.__ModuleLoader__.load({
759
820
  //#endregion
760
821
  //#region src/client/EffortDeclareSection.tsx
761
822
  /**
762
- * Settings section: per-model reasoningEfforts + openai-completions compat
763
- * for hand-declared llm-pi-ai routes.
823
+ * Settings section: per-model reasoningEfforts, image input, and
824
+ * openai-completions compat for hand-declared llm-pi-ai routes.
764
825
  */
765
826
  function compatSummary(compat) {
766
827
  const parts = [];
@@ -773,6 +834,7 @@ window.__ModuleLoader__.load({
773
834
  if (code === "empty") return t("errorEmpty");
774
835
  if (code === "off-only") return t("errorOffOnly");
775
836
  if (code === "bad-wire") return t("errorBadWire");
837
+ if (code === "bad-input") return t("errorBadInput");
776
838
  }
777
839
  function ModelRowEditor(props) {
778
840
  const { row, disabled, t, onChange } = props;
@@ -808,6 +870,21 @@ window.__ModuleLoader__.load({
808
870
  })
809
871
  ]
810
872
  }),
873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
874
+ className: effort_declare_module_css_default.check,
875
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
876
+ type: "checkbox",
877
+ checked: readImageCapable(row),
878
+ disabled,
879
+ onChange: (event) => {
880
+ onChange(writeImageCapable(row, event.target.checked));
881
+ }
882
+ }), t("imageInput")]
883
+ }),
884
+ readImageCapable(row) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
885
+ className: effort_declare_module_css_default.notice,
886
+ children: t("imageInputHint")
887
+ }) : null,
811
888
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
812
889
  className: effort_declare_module_css_default.fieldLabel,
813
890
  children: t("levels")
@@ -891,7 +968,7 @@ window.__ModuleLoader__.load({
891
968
  const formats = thinkingFormatChoices(props.formats, draft.compat.thinkingFormat);
892
969
  const summary = compatSummary(draft.compat);
893
970
  const sameWire = draft.compat.supportsReasoningEffort === false;
894
- const clientError = draft.models.map((row) => errorText(modelEffortError(row), t)).find((text) => text !== void 0);
971
+ const clientError = draft.models.map((row) => errorText(modelRowError(row), t)).find((text) => text !== void 0);
895
972
  const dirty = draftDirty(draft);
896
973
  const applyPreset = (id) => {
897
974
  const preset = PRESETS[id];
@@ -1245,7 +1322,7 @@ window.__ModuleLoader__.load({
1245
1322
  });
1246
1323
  return;
1247
1324
  }
1248
- const blocking = draft.models.map((row) => errorText(modelEffortError(row), t)).find((text) => text !== void 0);
1325
+ const blocking = draft.models.map((row) => errorText(modelRowError(row), t)).find((text) => text !== void 0);
1249
1326
  if (blocking !== void 0) {
1250
1327
  patchNotice(draft.provider, {
1251
1328
  kind: "error",
@@ -1415,7 +1492,7 @@ window.__ModuleLoader__.load({
1415
1492
  const zh = {
1416
1493
  nav: "推理档位",
1417
1494
  title: "推理档位声明",
1418
- intro: "本页声明「这模型能选哪些推理档」。对话里的档位选择仍在输入框模型菜单。没声明 = 和现在一样没有 Effort 行。不会改密钥、目录或当次选择。",
1495
+ intro: "声明手工模型可提供的推理档位,以及是否接受图片输入。未声明则不显示 Effort 行,图片附件也会被拒绝。档位仍在输入框菜单中选择;密钥、目录与当前选中项不在本页修改。",
1419
1496
  empty: "没有可编辑的手工 openai-completions 路由。",
1420
1497
  emptyHint: "请先到「模型」页添加提供方(llm-pi-ai 手工路由)。官方 DeepSeek 与 catalog 路由不在本页编辑。需要 DSH ≥ 0.1.0-rc.8(providers.declared)。",
1421
1498
  loadError: "无法加载提供方或设置。",
@@ -1425,7 +1502,7 @@ window.__ModuleLoader__.load({
1425
1502
  saving: "保存中…",
1426
1503
  saveBusy: "另有路由正在保存,请稍候。",
1427
1504
  cancel: "取消",
1428
- saved: "已保存。对话选择器会按新的能力声明显示 Effort 行。",
1505
+ saved: "已保存。对话会按新的能力声明更新 Effort 行和是否允许贴图。",
1429
1506
  conflict: "设置已被其他地方改过,请重新加载后再保存。",
1430
1507
  dirtyConflict: "此路由有未保存修改,设置已在其他地方更新。保存会写到最新文档上,或先取消再改。",
1431
1508
  presets: "预设",
@@ -1453,12 +1530,15 @@ window.__ModuleLoader__.load({
1453
1530
  errorOffOnly: "不能只开 Off,必须再开一档思考档。",
1454
1531
  errorBadWire: "思考档必须填写线上拼写;只有 Off 可以留空。不能使用未知档位键。",
1455
1532
  noModels: "这条路由还没有 models 列表。请先到「模型」页添加模型。",
1456
- reload: "重新加载"
1533
+ reload: "重新加载",
1534
+ imageInput: "支持图片输入",
1535
+ imageInputHint: "只在这个模型确实能看图时勾选。勾错了图会进聊天记录,然后被接口打回来。",
1536
+ errorBadInput: "input 只能是 text / image,必须包含 text,不能重复或留空。不支持图片请清除该键,不要写成空列表。"
1457
1537
  };
1458
1538
  const en = {
1459
1539
  nav: "Reasoning efforts",
1460
1540
  title: "Reasoning effort declarations",
1461
- intro: "This page declares which reasoning levels a model can offer. The per-turn choice stays in the composer model menu. Undeclared models keep having no Effort row. Keys, catalogs, and the current selection are not edited here.",
1541
+ intro: "Declare reasoning levels and image input for hand-added models. Undeclared models omit the Effort row and refuse image attachments. Effort is still chosen in the composer menu; keys, catalogs, and the current selection are not modified here.",
1462
1542
  empty: "No editable hand-declared openai-completions routes.",
1463
1543
  emptyHint: "Add a provider on the Models page first (an llm-pi-ai hand-declared route). Official DeepSeek and catalog routes are not edited here. Requires DSH ≥ 0.1.0-rc.8 (providers.declared).",
1464
1544
  loadError: "Could not load providers or settings.",
@@ -1468,7 +1548,7 @@ window.__ModuleLoader__.load({
1468
1548
  saving: "Saving…",
1469
1549
  saveBusy: "Another route is saving. Wait, then save this card.",
1470
1550
  cancel: "Cancel",
1471
- saved: "Saved. The composer Effort row follows this capability declaration.",
1551
+ saved: "Saved. The composer Effort row and image admission follow this capability declaration.",
1472
1552
  conflict: "Settings changed elsewhere. Reload, then save again.",
1473
1553
  dirtyConflict: "This route has unsaved edits and settings changed elsewhere. Save writes onto the latest document, or cancel first.",
1474
1554
  presets: "Presets",
@@ -1496,7 +1576,10 @@ window.__ModuleLoader__.load({
1496
1576
  errorOffOnly: "Off alone is not enough; declare at least one thinking level.",
1497
1577
  errorBadWire: "Thinking levels need a wire spelling; only Off may be empty. Unknown effort keys are rejected.",
1498
1578
  noModels: "This route has no models list yet. Add models on the Models page first.",
1499
- reload: "Reload"
1579
+ reload: "Reload",
1580
+ imageInput: "Accepts image input",
1581
+ imageInputHint: "Check only if this model actually sees images. Over-claiming admits a picture into history, then the gateway rejects the turn.",
1582
+ errorBadInput: "input may only list text and image, must include text, and must not be empty or duplicated. To drop vision, unset the key; do not write []."
1500
1583
  };
1501
1584
  //#endregion
1502
1585
  //#region src/client/index.ts