@goodandready/dsh-opencode-provider 0.5.26 → 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,17 @@
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
+
9
+ ## 0.5.27 — release 4: catalog freshness
10
+
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.
12
+ - **#85 — output ceiling**: the default generated-output cap is 65,536 tokens (was 32,768), matching the reference plugin while still respecting each model's own `maxTokens` and explicit caller limits.
13
+ - 133 tests.
14
+
3
15
  ## 0.5.26 — release 3: quota display and switch notices
4
16
 
5
17
  - **#75 — quota display modes**: new `usageDisplay` setting with `auto`/`always`/`off` (legacy `showSidebarQuota` maps to `always`); `auto` shows the sidebar card when a Go usage report exists. Exposed in Settings and TUI.
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.26.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.26.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.26
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.26.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.26.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.26
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.26.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.26.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.26
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
@@ -153,6 +153,8 @@ window.__ModuleLoader__.load({
153
153
  transportAuto: "Auto",
154
154
  free: "FREE",
155
155
  vision: "Image",
156
+ newModel: "NEW",
157
+ newModelHint: "Released within the last seven UTC days.",
156
158
  sectionFree: "Free models",
157
159
  sectionFreeHint: "No charge on any plan.",
158
160
  sectionGo: "Go subscription",
@@ -179,6 +181,11 @@ window.__ModuleLoader__.load({
179
181
  usageUrl: "Usage endpoint",
180
182
  requestTimeoutMs: "Header timeout (ms)",
181
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",
182
189
  proxyUrl: "Proxy URL",
183
190
  proxyUrlHint: "HTTP, HTTPS or SOCKS5 proxy for inference, catalog and usage. Empty disables it. Credentials are not supported.",
184
191
  maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
@@ -379,6 +386,8 @@ window.__ModuleLoader__.load({
379
386
  transportAuto: "自动",
380
387
  free: "免费",
381
388
  vision: "图片",
389
+ newModel: "NEW",
390
+ newModelHint: "最近七个 UTC 日内发布。",
382
391
  sectionFree: "免费模型",
383
392
  sectionFreeHint: "任何计划均不收费。",
384
393
  sectionGo: "Go 订阅",
@@ -405,6 +414,11 @@ window.__ModuleLoader__.load({
405
414
  usageUrl: "用量端点",
406
415
  requestTimeoutMs: "请求头超时(毫秒)",
407
416
  streamIdleTimeoutMs: "流空闲超时(毫秒)",
417
+ requestPreparationTimeoutMs: "准备超时(毫秒)",
418
+ maxImages: "每请求最大图片数",
419
+ maxRequestImageBytes: "请求图片预算(字节)",
420
+ requestImagePixelBudget: "图片像素预算",
421
+ requestImageMaxBytes: "单图最大字节数",
408
422
  proxyUrl: "代理地址",
409
423
  proxyUrlHint: "用于推理、目录和用量的 HTTP、HTTPS 或 SOCKS5 代理。留空则禁用。不支持代理认证。",
410
424
  maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
@@ -1692,6 +1706,11 @@ window.__ModuleLoader__.load({
1692
1706
  openCodeAuthEntry: state.config.openCodeAuthEntry,
1693
1707
  requestTimeoutMs: String(state.config.requestTimeoutMs),
1694
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),
1695
1714
  proxyUrl: state.config.proxyUrl,
1696
1715
  maxOutputTokens: state.config.maxOutputTokens === null ? "" : String(state.config.maxOutputTokens),
1697
1716
  contextWindow: state.config.contextWindow === null ? "" : String(state.config.contextWindow),
@@ -1739,6 +1758,11 @@ window.__ModuleLoader__.load({
1739
1758
  openCodeAuthEntry: draft.openCodeAuthEntry.trim(),
1740
1759
  requestTimeoutMs: numberOrNull(draft.requestTimeoutMs) ?? 3e5,
1741
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,
1742
1766
  proxyUrl: draft.proxyUrl.trim(),
1743
1767
  maxOutputTokens: numberOrNull(draft.maxOutputTokens),
1744
1768
  contextWindow: numberOrNull(draft.contextWindow),
@@ -2158,6 +2182,11 @@ window.__ModuleLoader__.load({
2158
2182
  title: t("legendFree"),
2159
2183
  children: [" ", t("free")]
2160
2184
  }) : null,
2185
+ model.isNew === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2186
+ className: "oc-badge oc-badge-ok",
2187
+ title: `${t("newModelHint")} ${model.releaseDate ?? ""}`,
2188
+ children: t("newModel")
2189
+ }) : null,
2161
2190
  !model.free && hasPrice ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2162
2191
  className: "oc-badge",
2163
2192
  title: t("legendPrice"),
@@ -3139,6 +3168,36 @@ window.__ModuleLoader__.load({
3139
3168
  value: draft.streamIdleTimeoutMs,
3140
3169
  onChange: (value) => update({ streamIdleTimeoutMs: value })
3141
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
+ }),
3142
3201
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3143
3202
  label: t("proxyUrl"),
3144
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.26";
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
- const DEFAULT_GENERATE_MAX_TOKENS = 32768;
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),
@@ -1108,6 +1122,24 @@ function modelInGoPlan(id, config) {
1108
1122
  if (GENERATED_MODELS[id] === void 0) return true;
1109
1123
  return effectiveModel(id, config).free || limitsFor(id) !== void 0;
1110
1124
  }
1125
+ function releaseDateOf(entry) {
1126
+ const raw = entry.release_date;
1127
+ if (typeof raw === "string" && raw !== "") {
1128
+ const parsed = Date.parse(raw.length === 10 ? `${raw}T00:00:00Z` : raw);
1129
+ if (Number.isFinite(parsed)) return new Date(parsed).toISOString().slice(0, 10);
1130
+ }
1131
+ if (typeof entry.created === "number" && Number.isFinite(entry.created)) return (/* @__PURE__ */ new Date(entry.created * 1e3)).toISOString().slice(0, 10);
1132
+ }
1133
+ /** Seven UTC calendar days, used by the picker's NEW badge. */
1134
+ function isNewRelease(date, now = Date.now()) {
1135
+ if (date === void 0) return false;
1136
+ const parsed = Date.parse(date.length === 10 ? `${date}T00:00:00Z` : date);
1137
+ return Number.isFinite(parsed) && parsed <= now && now - parsed < 6048e5;
1138
+ }
1139
+ /** Release date learned from the live catalog, if the gateway published it. */
1140
+ function catalogReleaseDate(provider, id, config) {
1141
+ return catalogCache.get(`${provider}|${modelsUrlFor(provider, config)}`)?.releaseDates[id];
1142
+ }
1111
1143
  async function fetchLiveIds(provider, config, apiKey, fetchImpl) {
1112
1144
  const url = modelsUrlFor(provider, config);
1113
1145
  const cacheKey = `${provider}|${url}`;
@@ -1127,9 +1159,16 @@ async function fetchLiveIds(provider, config, apiKey, fetchImpl) {
1127
1159
  const payload = await response.json();
1128
1160
  const ids = Array.isArray(payload.data) ? payload.data.map((entry) => entry.id).filter((id) => typeof id === "string" && id !== "") : Object.keys(payload.models ?? {});
1129
1161
  if (ids.length === 0) return void 0;
1162
+ const releaseDates = {};
1163
+ if (Array.isArray(payload.data)) for (const entry of payload.data) {
1164
+ if (typeof entry.id !== "string" || entry.id === "") continue;
1165
+ const date = releaseDateOf(entry);
1166
+ if (date !== void 0) releaseDates[entry.id] = date;
1167
+ }
1130
1168
  catalogCache.set(cacheKey, {
1131
1169
  at: Date.now(),
1132
- ids
1170
+ ids,
1171
+ releaseDates
1133
1172
  });
1134
1173
  return ids;
1135
1174
  } catch {
@@ -1267,16 +1306,21 @@ function classifyStreamError(value, status = 200) {
1267
1306
  }
1268
1307
  // src/images.ts
1269
1308
  const MAX_LONG_EDGE = 1568;
1270
- const MAX_BYTES = 1048576;
1271
- /** Total base64 image budget for one request; leaves room under the gateway cap. */
1272
- const MAX_REQUEST_IMAGE_BYTES = 25165824;
1273
- 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
+ }
1274
1317
  /**
1275
1318
  * Keep the newest images under one request-wide budget. The oldest images are
1276
1319
  * dropped first; the transports render them as an "image omitted" placeholder
1277
1320
  * instead of failing the whole request.
1278
1321
  */
1279
- function selectRequestImages(refs, occurrences) {
1322
+ function selectRequestImages(refs, occurrences, policy) {
1323
+ const limits = limitsOf(policy);
1280
1324
  const unique = [];
1281
1325
  const seen = /* @__PURE__ */ new Set();
1282
1326
  for (const ref of refs) {
@@ -1289,7 +1333,7 @@ function selectRequestImages(refs, occurrences) {
1289
1333
  let start = 0;
1290
1334
  let totalImages = unique.reduce((sum, ref) => sum + countOf(ref), 0);
1291
1335
  let bytes = unique.reduce((sum, ref) => sum + encoded(ref.bytes) * countOf(ref), 0);
1292
- while (start < unique.length && (totalImages > MAX_REQUEST_IMAGES || bytes > MAX_REQUEST_IMAGE_BYTES)) {
1336
+ while (start < unique.length && (totalImages > limits.maxImages || bytes > limits.maxRequestImageBytes)) {
1293
1337
  const dropped = unique[start];
1294
1338
  if (dropped !== void 0) {
1295
1339
  const count = countOf(dropped);
@@ -1308,20 +1352,23 @@ function selectRequestImages(refs, occurrences) {
1308
1352
  dropped: unique.slice(0, start)
1309
1353
  };
1310
1354
  }
1311
- function targetOf(ref) {
1355
+ function targetOf(ref, policy) {
1356
+ const limits = limitsOf(policy);
1312
1357
  const longEdge = Math.max(ref.width, ref.height);
1313
- 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;
1314
1361
  return {
1315
1362
  width: Math.max(1, Math.round(ref.width * scale)),
1316
1363
  height: Math.max(1, Math.round(ref.height * scale)),
1317
- maxBytes: MAX_BYTES
1364
+ maxBytes: limits.requestImageMaxBytes
1318
1365
  };
1319
1366
  }
1320
1367
  function hasImages(options) {
1321
1368
  return options.messages.some((message) => message.content.some((block) => block.type === "image" && block.offloaded !== true));
1322
1369
  }
1323
1370
  /** Decode the distinct request images that fit the request-wide budget. */
1324
- async function resolveImages(options, store, signal) {
1371
+ async function resolveImages(options, store, signal, policy) {
1325
1372
  const refs = [];
1326
1373
  const counts = /* @__PURE__ */ new Map();
1327
1374
  const seen = /* @__PURE__ */ new Set();
@@ -1334,8 +1381,8 @@ async function resolveImages(options, store, signal) {
1334
1381
  refs.push(ref);
1335
1382
  }
1336
1383
  const images = /* @__PURE__ */ new Map();
1337
- for (const ref of selectRequestImages(refs, counts).kept) {
1338
- 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);
1339
1386
  const base64 = Buffer.from(request.data).toString("base64");
1340
1387
  images.set(ref.attachmentId, {
1341
1388
  mediaType: request.mediaType,
@@ -3344,7 +3391,25 @@ var OpenCodeAdapter = class extends LlmAdapter {
3344
3391
  if (hasImages(options)) {
3345
3392
  const store = this.deps.resolveAttachments?.();
3346
3393
  if (store === void 0) throw new LlmError("llm-opencode: image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3347
- 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
+ }
3348
3413
  }
3349
3414
  let bodyOptions = options;
3350
3415
  if (options.tools !== void 0) {
@@ -4723,6 +4788,11 @@ function publicConfig(config) {
4723
4788
  openCodeAuthEntry: config.openCodeAuthEntry,
4724
4789
  requestTimeoutMs: config.requestTimeoutMs,
4725
4790
  streamIdleTimeoutMs: config.streamIdleTimeoutMs,
4791
+ requestPreparationTimeoutMs: config.requestPreparationTimeoutMs,
4792
+ maxImages: config.maxImages,
4793
+ maxRequestImageBytes: config.maxRequestImageBytes,
4794
+ requestImagePixelBudget: config.requestImagePixelBudget,
4795
+ requestImageMaxBytes: config.requestImageMaxBytes,
4726
4796
  proxyUrl: config.proxyUrl,
4727
4797
  visibleModels: config.visibleModels,
4728
4798
  modelVisibility: config.modelVisibility,
@@ -4919,6 +4989,11 @@ function configOps(patch) {
4919
4989
  if ("openCodeAuthEntry" in patch) set("openCodeAuthEntry", patch.openCodeAuthEntry);
4920
4990
  if ("requestTimeoutMs" in patch) set("requestTimeoutMs", patch.requestTimeoutMs);
4921
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);
4922
4997
  if ("proxyUrl" in patch) set("proxyUrl", normalizeProxyUrl(typeof patch.proxyUrl === "string" ? patch.proxyUrl : ""));
4923
4998
  if ("visibleModels" in patch) set("visibleModels", cleanStringArray(patch.visibleModels));
4924
4999
  if ("modelVisibility" in patch) set("modelVisibility", cleanBooleanRecord(patch.modelVisibility));
@@ -5030,7 +5105,9 @@ async function buildState(ctx, deps) {
5030
5105
  limits: limitsFor(model.id) ?? null,
5031
5106
  input: meta.input,
5032
5107
  reasoning: meta.reasoning,
5033
- visible: modelVisible(model.id, config)
5108
+ visible: modelVisible(model.id, config),
5109
+ ...catalogReleaseDate("opencode-go-direct", model.id, config) === void 0 ? {} : { releaseDate: catalogReleaseDate(GO_PROVIDER, model.id, config) },
5110
+ isNew: isNewRelease(catalogReleaseDate(GO_PROVIDER, model.id, config))
5034
5111
  };
5035
5112
  });
5036
5113
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.26",
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",