@goodandready/dsh-opencode-provider 0.5.27 → 0.5.28

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.28 — release 5: image policy and preparation timeout
4
+
5
+ - **#73 — image policy**: `maxImages`, `maxRequestImageBytes`, `requestImagePixelBudget` and `requestImageMaxBytes` are configurable in Settings; defaults preserve the previous 50 images / 24 MiB / 1568px / 1 MiB behavior. Count and pixel policies are covered by tests.
6
+ - **#78 — preparation timeout**: `requestPreparationTimeoutMs` (default 60s) bounds image resolution before dispatch, independently of the response-header timeout; caller aborts still win, and timeouts surface as `TIMEOUT`.
7
+ - 136 tests.
8
+
3
9
  ## 0.5.27 — release 4: catalog freshness
4
10
 
5
11
  - **#86 — new-model badge**: the live catalog now keeps `release_date` (or `created`) metadata; Settings marks models released within the last seven UTC days with a NEW badge and date tooltip. Dates are dropped safely when the gateway omits or malforms them.
package/README.md CHANGED
@@ -132,12 +132,12 @@ npm pack
132
132
  Install the **packed tarball** into a profile:
133
133
 
134
134
  ```bash
135
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
135
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.28.tgz
136
136
  # DSH Desktop:
137
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
137
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.28.tgz
138
138
 
139
139
  # Or install the published package:
140
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.27
140
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.28
141
141
  ```
142
142
 
143
143
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
package/README.ru.md CHANGED
@@ -131,12 +131,12 @@ npm pack
131
131
  Установка из **упакованного tarball**:
132
132
 
133
133
  ```bash
134
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
134
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.28.tgz
135
135
  # DSH Desktop:
136
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
136
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.28.tgz
137
137
 
138
138
  # Либо установка опубликованного пакета из npm:
139
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.27
139
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.28
140
140
  ```
141
141
 
142
142
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
package/README.zh.md CHANGED
@@ -126,12 +126,12 @@ npm pack
126
126
  通过**打包文件 (.tgz)**安装:
127
127
 
128
128
  ```bash
129
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
129
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.28.tgz
130
130
  # DSH 桌面端:
131
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
131
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.28.tgz
132
132
 
133
133
  # 或从 npm 安装已发布版本:
134
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.27
134
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.28
135
135
  ```
136
136
 
137
137
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
package/lib/client.js CHANGED
@@ -181,6 +181,11 @@ window.__ModuleLoader__.load({
181
181
  usageUrl: "Usage endpoint",
182
182
  requestTimeoutMs: "Header timeout (ms)",
183
183
  streamIdleTimeoutMs: "Stream idle timeout (ms)",
184
+ requestPreparationTimeoutMs: "Preparation timeout (ms)",
185
+ maxImages: "Max images per request",
186
+ maxRequestImageBytes: "Request image budget (bytes)",
187
+ requestImagePixelBudget: "Image pixel budget",
188
+ requestImageMaxBytes: "Per-image max bytes",
184
189
  proxyUrl: "Proxy URL",
185
190
  proxyUrlHint: "HTTP, HTTPS or SOCKS5 proxy for inference, catalog and usage. Empty disables it. Credentials are not supported.",
186
191
  maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
@@ -409,6 +414,11 @@ window.__ModuleLoader__.load({
409
414
  usageUrl: "用量端点",
410
415
  requestTimeoutMs: "请求头超时(毫秒)",
411
416
  streamIdleTimeoutMs: "流空闲超时(毫秒)",
417
+ requestPreparationTimeoutMs: "准备超时(毫秒)",
418
+ maxImages: "每请求最大图片数",
419
+ maxRequestImageBytes: "请求图片预算(字节)",
420
+ requestImagePixelBudget: "图片像素预算",
421
+ requestImageMaxBytes: "单图最大字节数",
412
422
  proxyUrl: "代理地址",
413
423
  proxyUrlHint: "用于推理、目录和用量的 HTTP、HTTPS 或 SOCKS5 代理。留空则禁用。不支持代理认证。",
414
424
  maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
@@ -1696,6 +1706,11 @@ window.__ModuleLoader__.load({
1696
1706
  openCodeAuthEntry: state.config.openCodeAuthEntry,
1697
1707
  requestTimeoutMs: String(state.config.requestTimeoutMs),
1698
1708
  streamIdleTimeoutMs: String(state.config.streamIdleTimeoutMs),
1709
+ requestPreparationTimeoutMs: String(state.config.requestPreparationTimeoutMs),
1710
+ maxImages: String(state.config.maxImages),
1711
+ maxRequestImageBytes: String(state.config.maxRequestImageBytes),
1712
+ requestImagePixelBudget: String(state.config.requestImagePixelBudget),
1713
+ requestImageMaxBytes: String(state.config.requestImageMaxBytes),
1699
1714
  proxyUrl: state.config.proxyUrl,
1700
1715
  maxOutputTokens: state.config.maxOutputTokens === null ? "" : String(state.config.maxOutputTokens),
1701
1716
  contextWindow: state.config.contextWindow === null ? "" : String(state.config.contextWindow),
@@ -1743,6 +1758,11 @@ window.__ModuleLoader__.load({
1743
1758
  openCodeAuthEntry: draft.openCodeAuthEntry.trim(),
1744
1759
  requestTimeoutMs: numberOrNull(draft.requestTimeoutMs) ?? 3e5,
1745
1760
  streamIdleTimeoutMs: numberOrNull(draft.streamIdleTimeoutMs) ?? 3e5,
1761
+ requestPreparationTimeoutMs: numberOrNull(draft.requestPreparationTimeoutMs) ?? 6e4,
1762
+ maxImages: numberOrNull(draft.maxImages) ?? 50,
1763
+ maxRequestImageBytes: numberOrNull(draft.maxRequestImageBytes) ?? 25165824,
1764
+ requestImagePixelBudget: numberOrNull(draft.requestImagePixelBudget) ?? 2458624,
1765
+ requestImageMaxBytes: numberOrNull(draft.requestImageMaxBytes) ?? 1048576,
1746
1766
  proxyUrl: draft.proxyUrl.trim(),
1747
1767
  maxOutputTokens: numberOrNull(draft.maxOutputTokens),
1748
1768
  contextWindow: numberOrNull(draft.contextWindow),
@@ -3148,6 +3168,36 @@ window.__ModuleLoader__.load({
3148
3168
  value: draft.streamIdleTimeoutMs,
3149
3169
  onChange: (value) => update({ streamIdleTimeoutMs: value })
3150
3170
  }),
3171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3172
+ label: t("requestPreparationTimeoutMs"),
3173
+ type: "number",
3174
+ value: draft.requestPreparationTimeoutMs,
3175
+ onChange: (value) => update({ requestPreparationTimeoutMs: value })
3176
+ }),
3177
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3178
+ label: t("maxImages"),
3179
+ type: "number",
3180
+ value: draft.maxImages,
3181
+ onChange: (value) => update({ maxImages: value })
3182
+ }),
3183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3184
+ label: t("maxRequestImageBytes"),
3185
+ type: "number",
3186
+ value: draft.maxRequestImageBytes,
3187
+ onChange: (value) => update({ maxRequestImageBytes: value })
3188
+ }),
3189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3190
+ label: t("requestImagePixelBudget"),
3191
+ type: "number",
3192
+ value: draft.requestImagePixelBudget,
3193
+ onChange: (value) => update({ requestImagePixelBudget: value })
3194
+ }),
3195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3196
+ label: t("requestImageMaxBytes"),
3197
+ type: "number",
3198
+ value: draft.requestImageMaxBytes,
3199
+ onChange: (value) => update({ requestImageMaxBytes: value })
3200
+ }),
3151
3201
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3152
3202
  label: t("proxyUrl"),
3153
3203
  hint: t("proxyUrlHint"),
package/lib/index.d.ts CHANGED
@@ -37,6 +37,16 @@ interface Config {
37
37
  requestTimeoutMs?: number;
38
38
  /** Milliseconds a stream may stall; defaults to 300s. */
39
39
  streamIdleTimeoutMs?: number;
40
+ /** Milliseconds allowed for image resolution and request preparation. */
41
+ requestPreparationTimeoutMs?: number;
42
+ /** Request-wide image count cap; defaults to 50. */
43
+ maxImages?: number;
44
+ /** Request-wide base64 image budget; defaults to 24 MiB. */
45
+ maxRequestImageBytes?: number;
46
+ /** Total-pixel budget for one normalized image. */
47
+ requestImagePixelBudget?: number;
48
+ /** Raw encoded-byte target for one image before base64 expansion. */
49
+ requestImageMaxBytes?: number;
40
50
  /** HTTP/HTTPS/SOCKS5 proxy for inference, catalog and usage requests. */
41
51
  proxyUrl?: string;
42
52
  /** Visible-model allowlist; an empty list means "show everything". */
@@ -85,6 +95,11 @@ interface ResolvedConfig {
85
95
  openCodeAuthEntry: string;
86
96
  requestTimeoutMs: number;
87
97
  streamIdleTimeoutMs: number;
98
+ requestPreparationTimeoutMs: number;
99
+ maxImages: number;
100
+ maxRequestImageBytes: number;
101
+ requestImagePixelBudget: number;
102
+ requestImageMaxBytes: number;
88
103
  proxyUrl: string;
89
104
  visibleModels: readonly string[];
90
105
  modelVisibility: Readonly<Record<string, boolean>>;
package/lib/index.js CHANGED
@@ -81,10 +81,14 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
81
81
  const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
82
82
  const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
83
83
  const DEFAULT_AUTH_ENTRY = "opencode-go";
84
- const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.27";
84
+ const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.28";
85
85
  const DEFAULT_CONTEXT_WINDOW = 262144;
86
86
  const DEFAULT_MAX_TOKENS = 32768;
87
87
  const DEFAULT_GENERATE_MAX_TOKENS = 65536;
88
+ const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 25165824;
89
+ const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2458624;
90
+ const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1048576;
91
+ const DEFAULT_REQUEST_PREPARATION_TIMEOUT_MS = 6e4;
88
92
  function markVolatile(schema) {
89
93
  const mark = schema.volatile;
90
94
  return typeof mark === "function" ? mark.call(schema) : schema;
@@ -99,6 +103,11 @@ const Config = z.object({
99
103
  openCodeAuthEntry: markVolatile(z.string().default(DEFAULT_AUTH_ENTRY)),
100
104
  requestTimeoutMs: markVolatile(z.number().min(1).max(2147483647).default(3e5)),
101
105
  streamIdleTimeoutMs: markVolatile(z.number().min(1).max(2147483647).default(3e5)),
106
+ requestPreparationTimeoutMs: markVolatile(z.number().min(1).max(2147483647).default(DEFAULT_REQUEST_PREPARATION_TIMEOUT_MS)),
107
+ maxImages: markVolatile(z.number().min(1).max(1e4).step(1).default(50)),
108
+ maxRequestImageBytes: markVolatile(z.number().min(1).max(1e9).step(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES)),
109
+ requestImagePixelBudget: markVolatile(z.number().min(1).max(1e8).step(1).default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET)),
110
+ requestImageMaxBytes: markVolatile(z.number().min(1).max(1e8).step(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES)),
102
111
  proxyUrl: markVolatile(z.string().default("")),
103
112
  visibleModels: markVolatile(z.array(z.string()).default([])),
104
113
  modelVisibility: markVolatile(z.dict(z.boolean()).default({})),
@@ -212,6 +221,11 @@ function resolveConfig(config) {
212
221
  openCodeAuthEntry: typeof config.openCodeAuthEntry === "string" && config.openCodeAuthEntry !== "" ? config.openCodeAuthEntry : DEFAULT_AUTH_ENTRY,
213
222
  requestTimeoutMs: positiveInt(config.requestTimeoutMs, 3e5),
214
223
  streamIdleTimeoutMs: positiveInt(config.streamIdleTimeoutMs, 3e5),
224
+ requestPreparationTimeoutMs: positiveInt(config.requestPreparationTimeoutMs, DEFAULT_REQUEST_PREPARATION_TIMEOUT_MS),
225
+ maxImages: positiveInt(config.maxImages, 50),
226
+ maxRequestImageBytes: positiveInt(config.maxRequestImageBytes, DEFAULT_MAX_REQUEST_IMAGE_BYTES),
227
+ requestImagePixelBudget: positiveInt(config.requestImagePixelBudget, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
228
+ requestImageMaxBytes: positiveInt(config.requestImageMaxBytes, DEFAULT_REQUEST_IMAGE_MAX_BYTES),
215
229
  proxyUrl: normalizeProxyUrl(config.proxyUrl),
216
230
  visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : [],
217
231
  modelVisibility: booleanRecord(config.modelVisibility),
@@ -1292,16 +1306,21 @@ function classifyStreamError(value, status = 200) {
1292
1306
  }
1293
1307
  // src/images.ts
1294
1308
  const MAX_LONG_EDGE = 1568;
1295
- const MAX_BYTES = 1048576;
1296
- /** Total base64 image budget for one request; leaves room under the gateway cap. */
1297
- const MAX_REQUEST_IMAGE_BYTES = 25165824;
1298
- const MAX_REQUEST_IMAGES = 50;
1309
+ function limitsOf(policy) {
1310
+ return {
1311
+ maxImages: policy?.maxImages ?? 50,
1312
+ maxRequestImageBytes: policy?.maxRequestImageBytes ?? 25165824,
1313
+ requestImagePixelBudget: policy?.requestImagePixelBudget ?? 2458624,
1314
+ requestImageMaxBytes: policy?.requestImageMaxBytes ?? 1048576
1315
+ };
1316
+ }
1299
1317
  /**
1300
1318
  * Keep the newest images under one request-wide budget. The oldest images are
1301
1319
  * dropped first; the transports render them as an "image omitted" placeholder
1302
1320
  * instead of failing the whole request.
1303
1321
  */
1304
- function selectRequestImages(refs, occurrences) {
1322
+ function selectRequestImages(refs, occurrences, policy) {
1323
+ const limits = limitsOf(policy);
1305
1324
  const unique = [];
1306
1325
  const seen = /* @__PURE__ */ new Set();
1307
1326
  for (const ref of refs) {
@@ -1314,7 +1333,7 @@ function selectRequestImages(refs, occurrences) {
1314
1333
  let start = 0;
1315
1334
  let totalImages = unique.reduce((sum, ref) => sum + countOf(ref), 0);
1316
1335
  let bytes = unique.reduce((sum, ref) => sum + encoded(ref.bytes) * countOf(ref), 0);
1317
- while (start < unique.length && (totalImages > MAX_REQUEST_IMAGES || bytes > MAX_REQUEST_IMAGE_BYTES)) {
1336
+ while (start < unique.length && (totalImages > limits.maxImages || bytes > limits.maxRequestImageBytes)) {
1318
1337
  const dropped = unique[start];
1319
1338
  if (dropped !== void 0) {
1320
1339
  const count = countOf(dropped);
@@ -1333,20 +1352,23 @@ function selectRequestImages(refs, occurrences) {
1333
1352
  dropped: unique.slice(0, start)
1334
1353
  };
1335
1354
  }
1336
- function targetOf(ref) {
1355
+ function targetOf(ref, policy) {
1356
+ const limits = limitsOf(policy);
1337
1357
  const longEdge = Math.max(ref.width, ref.height);
1338
- const scale = longEdge > MAX_LONG_EDGE ? MAX_LONG_EDGE / longEdge : 1;
1358
+ const pixelEdge = Math.max(1, Math.floor(Math.sqrt(limits.requestImagePixelBudget)));
1359
+ const maxEdge = Math.min(MAX_LONG_EDGE, pixelEdge);
1360
+ const scale = longEdge > maxEdge ? maxEdge / longEdge : 1;
1339
1361
  return {
1340
1362
  width: Math.max(1, Math.round(ref.width * scale)),
1341
1363
  height: Math.max(1, Math.round(ref.height * scale)),
1342
- maxBytes: MAX_BYTES
1364
+ maxBytes: limits.requestImageMaxBytes
1343
1365
  };
1344
1366
  }
1345
1367
  function hasImages(options) {
1346
1368
  return options.messages.some((message) => message.content.some((block) => block.type === "image" && block.offloaded !== true));
1347
1369
  }
1348
1370
  /** Decode the distinct request images that fit the request-wide budget. */
1349
- async function resolveImages(options, store, signal) {
1371
+ async function resolveImages(options, store, signal, policy) {
1350
1372
  const refs = [];
1351
1373
  const counts = /* @__PURE__ */ new Map();
1352
1374
  const seen = /* @__PURE__ */ new Set();
@@ -1359,8 +1381,8 @@ async function resolveImages(options, store, signal) {
1359
1381
  refs.push(ref);
1360
1382
  }
1361
1383
  const images = /* @__PURE__ */ new Map();
1362
- for (const ref of selectRequestImages(refs, counts).kept) {
1363
- const request = await store.readImageRequest(ref, targetOf(ref), signal);
1384
+ for (const ref of selectRequestImages(refs, counts, policy).kept) {
1385
+ const request = await store.readImageRequest(ref, targetOf(ref, policy), signal);
1364
1386
  const base64 = Buffer.from(request.data).toString("base64");
1365
1387
  images.set(ref.attachmentId, {
1366
1388
  mediaType: request.mediaType,
@@ -3369,7 +3391,25 @@ var OpenCodeAdapter = class extends LlmAdapter {
3369
3391
  if (hasImages(options)) {
3370
3392
  const store = this.deps.resolveAttachments?.();
3371
3393
  if (store === void 0) throw new LlmError("llm-opencode: image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3372
- images = await resolveImages(options, store, options.signal);
3394
+ const preparation = new AbortController();
3395
+ let timedOut = false;
3396
+ const onAbort = () => preparation.abort(options.signal?.reason);
3397
+ options.signal?.addEventListener("abort", onAbort, { once: true });
3398
+ const timer = setTimeout(() => {
3399
+ timedOut = true;
3400
+ preparation.abort();
3401
+ }, config.requestPreparationTimeoutMs);
3402
+ timer.unref?.();
3403
+ try {
3404
+ images = await resolveImages(options, store, preparation.signal, config);
3405
+ } catch (error) {
3406
+ if (options.signal?.aborted === true) throw options.signal.reason ?? error;
3407
+ if (timedOut) throw new LlmError(`OpenCode image preparation exceeded ${config.requestPreparationTimeoutMs}ms`, "TIMEOUT", { cause: error });
3408
+ throw error;
3409
+ } finally {
3410
+ clearTimeout(timer);
3411
+ options.signal?.removeEventListener("abort", onAbort);
3412
+ }
3373
3413
  }
3374
3414
  let bodyOptions = options;
3375
3415
  if (options.tools !== void 0) {
@@ -4748,6 +4788,11 @@ function publicConfig(config) {
4748
4788
  openCodeAuthEntry: config.openCodeAuthEntry,
4749
4789
  requestTimeoutMs: config.requestTimeoutMs,
4750
4790
  streamIdleTimeoutMs: config.streamIdleTimeoutMs,
4791
+ requestPreparationTimeoutMs: config.requestPreparationTimeoutMs,
4792
+ maxImages: config.maxImages,
4793
+ maxRequestImageBytes: config.maxRequestImageBytes,
4794
+ requestImagePixelBudget: config.requestImagePixelBudget,
4795
+ requestImageMaxBytes: config.requestImageMaxBytes,
4751
4796
  proxyUrl: config.proxyUrl,
4752
4797
  visibleModels: config.visibleModels,
4753
4798
  modelVisibility: config.modelVisibility,
@@ -4944,6 +4989,11 @@ function configOps(patch) {
4944
4989
  if ("openCodeAuthEntry" in patch) set("openCodeAuthEntry", patch.openCodeAuthEntry);
4945
4990
  if ("requestTimeoutMs" in patch) set("requestTimeoutMs", patch.requestTimeoutMs);
4946
4991
  if ("streamIdleTimeoutMs" in patch) set("streamIdleTimeoutMs", patch.streamIdleTimeoutMs);
4992
+ if ("requestPreparationTimeoutMs" in patch) set("requestPreparationTimeoutMs", patch.requestPreparationTimeoutMs);
4993
+ if ("maxImages" in patch) set("maxImages", patch.maxImages);
4994
+ if ("maxRequestImageBytes" in patch) set("maxRequestImageBytes", patch.maxRequestImageBytes);
4995
+ if ("requestImagePixelBudget" in patch) set("requestImagePixelBudget", patch.requestImagePixelBudget);
4996
+ if ("requestImageMaxBytes" in patch) set("requestImageMaxBytes", patch.requestImageMaxBytes);
4947
4997
  if ("proxyUrl" in patch) set("proxyUrl", normalizeProxyUrl(typeof patch.proxyUrl === "string" ? patch.proxyUrl : ""));
4948
4998
  if ("visibleModels" in patch) set("visibleModels", cleanStringArray(patch.visibleModels));
4949
4999
  if ("modelVisibility" in patch) set("modelVisibility", cleanBooleanRecord(patch.modelVisibility));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.27",
3
+ "version": "0.5.28",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",