@goodandready/dsh-opencode-provider 0.5.27 → 0.5.29

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.29 — release 6: catalog refresh and Free-lane decision
4
+
5
+ - **#77 — catalog refresh**: `catalogRefreshMinutes` (1–10,080, default 10) controls the live `/models` cache; explicit `/models/refresh` still bypasses it. Exposed in Settings.
6
+ - **#76 — Free-lane decision**: re-probed keyless `big-pickle` on 2026-11-05; HTTP 429 `FreeUsageLimitError`. No keyless route or header-spoofing lane is added; the keyed Console/Zen path remains supported. Evidence recorded in DESIGN/README.
7
+ - 137 tests.
8
+
9
+ ## 0.5.28 — release 5: image policy and preparation timeout
10
+
11
+ - **#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.
12
+ - **#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`.
13
+ - 136 tests.
14
+
3
15
  ## 0.5.27 — release 4: catalog freshness
4
16
 
5
17
  - **#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.29.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.29.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.29
141
141
  ```
142
142
 
143
143
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
@@ -310,7 +310,7 @@ GitHub Actions runs `npm run check` and `npm pack` on push/PR (`.github/workflow
310
310
  - Subscription request counts are OpenCode's published estimates, not enforced quotas. They can change when OpenCode updates the Go pricing table; the gateway is the final authority.
311
311
  - Usage-by-model aggregates the local projection cache and attributes a whole session to its last-used model; a session that switched models is therefore approximate. It never reads prompt text or titles.
312
312
  - `opencode-direct` (Console) billing shows no per-model allowance; the Go route is the primary target.
313
- - The anonymous free lane (`Bearer public` + CLI impersonation) is not implemented.
313
+ - The anonymous free lane (`Bearer public` + CLI impersonation) is not implemented. A keyless `big-pickle` probe on 2026-11-05 returned HTTP 429 `FreeUsageLimitError`; use the keyed Console/Zen route for free models.
314
314
  - The UI requires a web profile (the Host needs `ctx.webServer`); the TUI keeps the `/opencode` command and the model routes.
315
315
  - Write routes are loopback-only; a web profile exposed to a LAN can read state but cannot save configuration or keys until the host auth token is integrated.
316
316
 
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.29.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.29.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.29
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.29.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.29.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.29
135
135
  ```
136
136
 
137
137
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
package/lib/client.js CHANGED
@@ -181,6 +181,12 @@ 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",
189
+ catalogRefreshMinutes: "Catalog refresh (minutes)",
184
190
  proxyUrl: "Proxy URL",
185
191
  proxyUrlHint: "HTTP, HTTPS or SOCKS5 proxy for inference, catalog and usage. Empty disables it. Credentials are not supported.",
186
192
  maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
@@ -409,6 +415,12 @@ window.__ModuleLoader__.load({
409
415
  usageUrl: "用量端点",
410
416
  requestTimeoutMs: "请求头超时(毫秒)",
411
417
  streamIdleTimeoutMs: "流空闲超时(毫秒)",
418
+ requestPreparationTimeoutMs: "准备超时(毫秒)",
419
+ maxImages: "每请求最大图片数",
420
+ maxRequestImageBytes: "请求图片预算(字节)",
421
+ requestImagePixelBudget: "图片像素预算",
422
+ requestImageMaxBytes: "单图最大字节数",
423
+ catalogRefreshMinutes: "目录刷新间隔(分钟)",
412
424
  proxyUrl: "代理地址",
413
425
  proxyUrlHint: "用于推理、目录和用量的 HTTP、HTTPS 或 SOCKS5 代理。留空则禁用。不支持代理认证。",
414
426
  maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
@@ -1696,7 +1708,13 @@ window.__ModuleLoader__.load({
1696
1708
  openCodeAuthEntry: state.config.openCodeAuthEntry,
1697
1709
  requestTimeoutMs: String(state.config.requestTimeoutMs),
1698
1710
  streamIdleTimeoutMs: String(state.config.streamIdleTimeoutMs),
1711
+ requestPreparationTimeoutMs: String(state.config.requestPreparationTimeoutMs),
1712
+ maxImages: String(state.config.maxImages),
1713
+ maxRequestImageBytes: String(state.config.maxRequestImageBytes),
1714
+ requestImagePixelBudget: String(state.config.requestImagePixelBudget),
1715
+ requestImageMaxBytes: String(state.config.requestImageMaxBytes),
1699
1716
  proxyUrl: state.config.proxyUrl,
1717
+ catalogRefreshMinutes: String(state.config.catalogRefreshMinutes),
1700
1718
  maxOutputTokens: state.config.maxOutputTokens === null ? "" : String(state.config.maxOutputTokens),
1701
1719
  contextWindow: state.config.contextWindow === null ? "" : String(state.config.contextWindow),
1702
1720
  contextWindowOverrides: Object.fromEntries(Object.entries(state.config.contextWindowOverrides).map(([id, value]) => [id, String(value)])),
@@ -1743,7 +1761,13 @@ window.__ModuleLoader__.load({
1743
1761
  openCodeAuthEntry: draft.openCodeAuthEntry.trim(),
1744
1762
  requestTimeoutMs: numberOrNull(draft.requestTimeoutMs) ?? 3e5,
1745
1763
  streamIdleTimeoutMs: numberOrNull(draft.streamIdleTimeoutMs) ?? 3e5,
1764
+ requestPreparationTimeoutMs: numberOrNull(draft.requestPreparationTimeoutMs) ?? 6e4,
1765
+ maxImages: numberOrNull(draft.maxImages) ?? 50,
1766
+ maxRequestImageBytes: numberOrNull(draft.maxRequestImageBytes) ?? 25165824,
1767
+ requestImagePixelBudget: numberOrNull(draft.requestImagePixelBudget) ?? 2458624,
1768
+ requestImageMaxBytes: numberOrNull(draft.requestImageMaxBytes) ?? 1048576,
1746
1769
  proxyUrl: draft.proxyUrl.trim(),
1770
+ catalogRefreshMinutes: numberOrNull(draft.catalogRefreshMinutes) ?? 10,
1747
1771
  maxOutputTokens: numberOrNull(draft.maxOutputTokens),
1748
1772
  contextWindow: numberOrNull(draft.contextWindow),
1749
1773
  contextWindowOverrides: numberRecordOrNull(draft.contextWindowOverrides),
@@ -3148,6 +3172,42 @@ window.__ModuleLoader__.load({
3148
3172
  value: draft.streamIdleTimeoutMs,
3149
3173
  onChange: (value) => update({ streamIdleTimeoutMs: value })
3150
3174
  }),
3175
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3176
+ label: t("requestPreparationTimeoutMs"),
3177
+ type: "number",
3178
+ value: draft.requestPreparationTimeoutMs,
3179
+ onChange: (value) => update({ requestPreparationTimeoutMs: value })
3180
+ }),
3181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3182
+ label: t("maxImages"),
3183
+ type: "number",
3184
+ value: draft.maxImages,
3185
+ onChange: (value) => update({ maxImages: value })
3186
+ }),
3187
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3188
+ label: t("maxRequestImageBytes"),
3189
+ type: "number",
3190
+ value: draft.maxRequestImageBytes,
3191
+ onChange: (value) => update({ maxRequestImageBytes: value })
3192
+ }),
3193
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3194
+ label: t("requestImagePixelBudget"),
3195
+ type: "number",
3196
+ value: draft.requestImagePixelBudget,
3197
+ onChange: (value) => update({ requestImagePixelBudget: value })
3198
+ }),
3199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3200
+ label: t("requestImageMaxBytes"),
3201
+ type: "number",
3202
+ value: draft.requestImageMaxBytes,
3203
+ onChange: (value) => update({ requestImageMaxBytes: value })
3204
+ }),
3205
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3206
+ label: t("catalogRefreshMinutes"),
3207
+ type: "number",
3208
+ value: draft.catalogRefreshMinutes,
3209
+ onChange: (value) => update({ catalogRefreshMinutes: value })
3210
+ }),
3151
3211
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3152
3212
  label: t("proxyUrl"),
3153
3213
  hint: t("proxyUrlHint"),
package/lib/index.d.ts CHANGED
@@ -37,8 +37,20 @@ 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;
52
+ /** Live catalog cache lifetime in minutes; defaults to 10. */
53
+ catalogRefreshMinutes?: number;
42
54
  /** Visible-model allowlist; an empty list means "show everything". */
43
55
  visibleModels?: string[];
44
56
  /** Per-model visibility overrides; wins over `visibleModels`. */
@@ -85,7 +97,13 @@ interface ResolvedConfig {
85
97
  openCodeAuthEntry: string;
86
98
  requestTimeoutMs: number;
87
99
  streamIdleTimeoutMs: number;
100
+ requestPreparationTimeoutMs: number;
101
+ maxImages: number;
102
+ maxRequestImageBytes: number;
103
+ requestImagePixelBudget: number;
104
+ requestImageMaxBytes: number;
88
105
  proxyUrl: string;
106
+ catalogRefreshMinutes: number;
89
107
  visibleModels: readonly string[];
90
108
  modelVisibility: Readonly<Record<string, boolean>>;
91
109
  filterModelsByPlan: 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.29";
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,7 +103,13 @@ 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("")),
112
+ catalogRefreshMinutes: markVolatile(z.number().min(1).max(10080).step(1).default(10)),
103
113
  visibleModels: markVolatile(z.array(z.string()).default([])),
104
114
  modelVisibility: markVolatile(z.dict(z.boolean()).default({})),
105
115
  filterModelsByPlan: markVolatile(z.boolean().default(true)),
@@ -212,7 +222,13 @@ function resolveConfig(config) {
212
222
  openCodeAuthEntry: typeof config.openCodeAuthEntry === "string" && config.openCodeAuthEntry !== "" ? config.openCodeAuthEntry : DEFAULT_AUTH_ENTRY,
213
223
  requestTimeoutMs: positiveInt(config.requestTimeoutMs, 3e5),
214
224
  streamIdleTimeoutMs: positiveInt(config.streamIdleTimeoutMs, 3e5),
225
+ requestPreparationTimeoutMs: positiveInt(config.requestPreparationTimeoutMs, DEFAULT_REQUEST_PREPARATION_TIMEOUT_MS),
226
+ maxImages: positiveInt(config.maxImages, 50),
227
+ maxRequestImageBytes: positiveInt(config.maxRequestImageBytes, DEFAULT_MAX_REQUEST_IMAGE_BYTES),
228
+ requestImagePixelBudget: positiveInt(config.requestImagePixelBudget, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
229
+ requestImageMaxBytes: positiveInt(config.requestImageMaxBytes, DEFAULT_REQUEST_IMAGE_MAX_BYTES),
215
230
  proxyUrl: normalizeProxyUrl(config.proxyUrl),
231
+ catalogRefreshMinutes: positiveInt(config.catalogRefreshMinutes, 10),
216
232
  visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : [],
217
233
  modelVisibility: booleanRecord(config.modelVisibility),
218
234
  filterModelsByPlan: config.filterModelsByPlan !== false,
@@ -1052,7 +1068,6 @@ function describeLimitsShort(limits) {
1052
1068
  return `Go ${formatRequests(limits.go.fiveHour)}/5h · ${formatRequests(limits.go.weekly)}/wk`;
1053
1069
  }
1054
1070
  // src/catalog.ts
1055
- const CATALOG_TTL_MS = 6e5;
1056
1071
  const CATALOG_TIMEOUT_MS = 15e3;
1057
1072
  const catalogCache = /* @__PURE__ */ new Map();
1058
1073
  const FALLBACK_META = {
@@ -1130,7 +1145,7 @@ async function fetchLiveIds(provider, config, apiKey, fetchImpl) {
1130
1145
  const url = modelsUrlFor(provider, config);
1131
1146
  const cacheKey = `${provider}|${url}`;
1132
1147
  const cached = catalogCache.get(cacheKey);
1133
- if (cached !== void 0 && Date.now() - cached.at < CATALOG_TTL_MS) return cached.ids;
1148
+ if (cached !== void 0 && Date.now() - cached.at < config.catalogRefreshMinutes * 6e4) return cached.ids;
1134
1149
  const headers = {
1135
1150
  accept: "application/json",
1136
1151
  "user-agent": config.userAgent,
@@ -1292,16 +1307,21 @@ function classifyStreamError(value, status = 200) {
1292
1307
  }
1293
1308
  // src/images.ts
1294
1309
  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;
1310
+ function limitsOf(policy) {
1311
+ return {
1312
+ maxImages: policy?.maxImages ?? 50,
1313
+ maxRequestImageBytes: policy?.maxRequestImageBytes ?? 25165824,
1314
+ requestImagePixelBudget: policy?.requestImagePixelBudget ?? 2458624,
1315
+ requestImageMaxBytes: policy?.requestImageMaxBytes ?? 1048576
1316
+ };
1317
+ }
1299
1318
  /**
1300
1319
  * Keep the newest images under one request-wide budget. The oldest images are
1301
1320
  * dropped first; the transports render them as an "image omitted" placeholder
1302
1321
  * instead of failing the whole request.
1303
1322
  */
1304
- function selectRequestImages(refs, occurrences) {
1323
+ function selectRequestImages(refs, occurrences, policy) {
1324
+ const limits = limitsOf(policy);
1305
1325
  const unique = [];
1306
1326
  const seen = /* @__PURE__ */ new Set();
1307
1327
  for (const ref of refs) {
@@ -1314,7 +1334,7 @@ function selectRequestImages(refs, occurrences) {
1314
1334
  let start = 0;
1315
1335
  let totalImages = unique.reduce((sum, ref) => sum + countOf(ref), 0);
1316
1336
  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)) {
1337
+ while (start < unique.length && (totalImages > limits.maxImages || bytes > limits.maxRequestImageBytes)) {
1318
1338
  const dropped = unique[start];
1319
1339
  if (dropped !== void 0) {
1320
1340
  const count = countOf(dropped);
@@ -1333,20 +1353,23 @@ function selectRequestImages(refs, occurrences) {
1333
1353
  dropped: unique.slice(0, start)
1334
1354
  };
1335
1355
  }
1336
- function targetOf(ref) {
1356
+ function targetOf(ref, policy) {
1357
+ const limits = limitsOf(policy);
1337
1358
  const longEdge = Math.max(ref.width, ref.height);
1338
- const scale = longEdge > MAX_LONG_EDGE ? MAX_LONG_EDGE / longEdge : 1;
1359
+ const pixelEdge = Math.max(1, Math.floor(Math.sqrt(limits.requestImagePixelBudget)));
1360
+ const maxEdge = Math.min(MAX_LONG_EDGE, pixelEdge);
1361
+ const scale = longEdge > maxEdge ? maxEdge / longEdge : 1;
1339
1362
  return {
1340
1363
  width: Math.max(1, Math.round(ref.width * scale)),
1341
1364
  height: Math.max(1, Math.round(ref.height * scale)),
1342
- maxBytes: MAX_BYTES
1365
+ maxBytes: limits.requestImageMaxBytes
1343
1366
  };
1344
1367
  }
1345
1368
  function hasImages(options) {
1346
1369
  return options.messages.some((message) => message.content.some((block) => block.type === "image" && block.offloaded !== true));
1347
1370
  }
1348
1371
  /** Decode the distinct request images that fit the request-wide budget. */
1349
- async function resolveImages(options, store, signal) {
1372
+ async function resolveImages(options, store, signal, policy) {
1350
1373
  const refs = [];
1351
1374
  const counts = /* @__PURE__ */ new Map();
1352
1375
  const seen = /* @__PURE__ */ new Set();
@@ -1359,8 +1382,8 @@ async function resolveImages(options, store, signal) {
1359
1382
  refs.push(ref);
1360
1383
  }
1361
1384
  const images = /* @__PURE__ */ new Map();
1362
- for (const ref of selectRequestImages(refs, counts).kept) {
1363
- const request = await store.readImageRequest(ref, targetOf(ref), signal);
1385
+ for (const ref of selectRequestImages(refs, counts, policy).kept) {
1386
+ const request = await store.readImageRequest(ref, targetOf(ref, policy), signal);
1364
1387
  const base64 = Buffer.from(request.data).toString("base64");
1365
1388
  images.set(ref.attachmentId, {
1366
1389
  mediaType: request.mediaType,
@@ -3369,7 +3392,25 @@ var OpenCodeAdapter = class extends LlmAdapter {
3369
3392
  if (hasImages(options)) {
3370
3393
  const store = this.deps.resolveAttachments?.();
3371
3394
  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);
3395
+ const preparation = new AbortController();
3396
+ let timedOut = false;
3397
+ const onAbort = () => preparation.abort(options.signal?.reason);
3398
+ options.signal?.addEventListener("abort", onAbort, { once: true });
3399
+ const timer = setTimeout(() => {
3400
+ timedOut = true;
3401
+ preparation.abort();
3402
+ }, config.requestPreparationTimeoutMs);
3403
+ timer.unref?.();
3404
+ try {
3405
+ images = await resolveImages(options, store, preparation.signal, config);
3406
+ } catch (error) {
3407
+ if (options.signal?.aborted === true) throw options.signal.reason ?? error;
3408
+ if (timedOut) throw new LlmError(`OpenCode image preparation exceeded ${config.requestPreparationTimeoutMs}ms`, "TIMEOUT", { cause: error });
3409
+ throw error;
3410
+ } finally {
3411
+ clearTimeout(timer);
3412
+ options.signal?.removeEventListener("abort", onAbort);
3413
+ }
3373
3414
  }
3374
3415
  let bodyOptions = options;
3375
3416
  if (options.tools !== void 0) {
@@ -4748,7 +4789,13 @@ function publicConfig(config) {
4748
4789
  openCodeAuthEntry: config.openCodeAuthEntry,
4749
4790
  requestTimeoutMs: config.requestTimeoutMs,
4750
4791
  streamIdleTimeoutMs: config.streamIdleTimeoutMs,
4792
+ requestPreparationTimeoutMs: config.requestPreparationTimeoutMs,
4793
+ maxImages: config.maxImages,
4794
+ maxRequestImageBytes: config.maxRequestImageBytes,
4795
+ requestImagePixelBudget: config.requestImagePixelBudget,
4796
+ requestImageMaxBytes: config.requestImageMaxBytes,
4751
4797
  proxyUrl: config.proxyUrl,
4798
+ catalogRefreshMinutes: config.catalogRefreshMinutes,
4752
4799
  visibleModels: config.visibleModels,
4753
4800
  modelVisibility: config.modelVisibility,
4754
4801
  filterModelsByPlan: config.filterModelsByPlan,
@@ -4944,7 +4991,13 @@ function configOps(patch) {
4944
4991
  if ("openCodeAuthEntry" in patch) set("openCodeAuthEntry", patch.openCodeAuthEntry);
4945
4992
  if ("requestTimeoutMs" in patch) set("requestTimeoutMs", patch.requestTimeoutMs);
4946
4993
  if ("streamIdleTimeoutMs" in patch) set("streamIdleTimeoutMs", patch.streamIdleTimeoutMs);
4994
+ if ("requestPreparationTimeoutMs" in patch) set("requestPreparationTimeoutMs", patch.requestPreparationTimeoutMs);
4995
+ if ("maxImages" in patch) set("maxImages", patch.maxImages);
4996
+ if ("maxRequestImageBytes" in patch) set("maxRequestImageBytes", patch.maxRequestImageBytes);
4997
+ if ("requestImagePixelBudget" in patch) set("requestImagePixelBudget", patch.requestImagePixelBudget);
4998
+ if ("requestImageMaxBytes" in patch) set("requestImageMaxBytes", patch.requestImageMaxBytes);
4947
4999
  if ("proxyUrl" in patch) set("proxyUrl", normalizeProxyUrl(typeof patch.proxyUrl === "string" ? patch.proxyUrl : ""));
5000
+ if ("catalogRefreshMinutes" in patch) set("catalogRefreshMinutes", patch.catalogRefreshMinutes);
4948
5001
  if ("visibleModels" in patch) set("visibleModels", cleanStringArray(patch.visibleModels));
4949
5002
  if ("modelVisibility" in patch) set("modelVisibility", cleanBooleanRecord(patch.modelVisibility));
4950
5003
  if ("filterModelsByPlan" in patch) set("filterModelsByPlan", patch.filterModelsByPlan === true);
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.29",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",