@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 +12 -0
- package/README.md +4 -4
- package/README.ru.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +60 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +69 -16
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 <
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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 >
|
|
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
|
|
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:
|
|
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
|
-
|
|
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);
|