dsh-opencode-go 0.1.14 → 0.1.16
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/README.en.md +39 -7
- package/README.md +39 -7
- package/docs/development.md +64 -0
- package/docs/fork-review-2026-09-25.md +191 -0
- package/docs/independent-package.md +2 -0
- package/docs/verification.md +84 -0
- package/lib/client.js +343 -241
- package/lib/index.js +176 -48
- package/lib/types/adapter.d.ts +2 -0
- package/lib/types/catalog.d.ts +12 -1
- package/lib/types/client/Section.d.ts +6 -5
- package/lib/types/client/locales.d.ts +13 -1
- package/lib/types/client/section-controller.d.ts +4 -0
- package/lib/types/client/staged-form.d.ts +4 -3
- package/lib/types/config.d.ts +2 -0
- package/lib/types/conversion/context.d.ts +3 -1
- package/lib/types/conversion/image-offload.d.ts +1 -0
- package/lib/types/metadata-cache.d.ts +12 -0
- package/lib/types/model-metadata.d.ts +3 -1
- package/lib/types/models-contract.d.ts +15 -0
- package/package.json +26 -17
package/lib/index.js
CHANGED
|
@@ -50,7 +50,7 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
50
50
|
import { LlmError as LlmError7, assertUsableApiKey, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
|
|
51
51
|
|
|
52
52
|
// src/adapter.ts
|
|
53
|
-
import { randomUUID } from "node:crypto";
|
|
53
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
54
54
|
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
55
55
|
import {
|
|
56
56
|
LlmAdapter,
|
|
@@ -273,15 +273,15 @@ var api = llm;
|
|
|
273
273
|
function projectRequestImages(messages, policy) {
|
|
274
274
|
if (api.requiredImageOffload !== void 0 && api.projectOffloadedImages !== void 0) {
|
|
275
275
|
if (!policy.exact) return messages;
|
|
276
|
-
if (policy.maxBytes !== void 0) {
|
|
276
|
+
if (policy.maxBytes !== void 0 || policy.maxImages !== void 0) {
|
|
277
277
|
const offloadImages = api.requiredImageOffload(
|
|
278
278
|
messages,
|
|
279
|
-
{ representation: "base64", maxBytes: policy.maxBytes },
|
|
279
|
+
{ representation: "base64", maxBytes: policy.maxBytes, maxImages: policy.maxImages, countQuantum: 1 },
|
|
280
280
|
(block) => policy.byteLength(block.attachment)
|
|
281
281
|
);
|
|
282
282
|
if (offloadImages > 0) {
|
|
283
283
|
throw new llm.LlmError(
|
|
284
|
-
`pi-ai request images exceed the
|
|
284
|
+
`pi-ai request images exceed the configured image count or base64 payload bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`,
|
|
285
285
|
api.IMAGE_OFFLOAD_REQUIRED_CODE ?? "IMAGE_OFFLOAD_REQUIRED",
|
|
286
286
|
{ offloadImages }
|
|
287
287
|
);
|
|
@@ -295,6 +295,8 @@ function projectRequestImages(messages, policy) {
|
|
|
295
295
|
return api.offloadRequestImagesWithPolicy(messages, {
|
|
296
296
|
representation: "base64",
|
|
297
297
|
byteQuantum: 1,
|
|
298
|
+
countQuantum: 1,
|
|
299
|
+
maxImages: policy.maxImages,
|
|
298
300
|
maxBytes: policy.maxBytes,
|
|
299
301
|
byteLength: policy.byteLength,
|
|
300
302
|
placeholder: policy.placeholder
|
|
@@ -478,7 +480,7 @@ function toPiContext(options, images, onReplayDegrade) {
|
|
|
478
480
|
return images === void 0 ? textOnlyContext(options, onReplayDegrade) : toPiContextWithImages(options, images, onReplayDegrade);
|
|
479
481
|
}
|
|
480
482
|
async function toPiContextWithImages(options, images, onReplayDegrade) {
|
|
481
|
-
const { attachments, resolveImageAccess, maxRequestImageBytes } = images;
|
|
483
|
+
const { attachments, resolveImageAccess, maxImages, maxRequestImageBytes } = images;
|
|
482
484
|
const requestImagePolicy = images.requestImagePolicy ?? {
|
|
483
485
|
maxPixels: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
|
|
484
486
|
maxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES
|
|
@@ -486,6 +488,7 @@ async function toPiContextWithImages(options, images, onReplayDegrade) {
|
|
|
486
488
|
assertSupportedHistory(options.messages);
|
|
487
489
|
const split = splitSystemPrompt(options);
|
|
488
490
|
const projection = {
|
|
491
|
+
maxImages,
|
|
489
492
|
maxBytes: maxRequestImageBytes,
|
|
490
493
|
placeholder: (ref) => offloadedImageText(ref, resolveImageAccess(ref))
|
|
491
494
|
};
|
|
@@ -713,6 +716,13 @@ import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.l
|
|
|
713
716
|
import { attributionHeaders, LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
|
|
714
717
|
|
|
715
718
|
// src/models-contract.ts
|
|
719
|
+
var INPUT_MODALITIES = ["text", "image", "audio", "video", "pdf"];
|
|
720
|
+
function normalizeInputModalities(value) {
|
|
721
|
+
if (!Array.isArray(value)) return void 0;
|
|
722
|
+
const declared = new Set(value.filter((item) => typeof item === "string").map((item) => item.toLowerCase()));
|
|
723
|
+
const ordered = INPUT_MODALITIES.filter((modality) => declared.has(modality));
|
|
724
|
+
return ordered.length === 0 ? void 0 : ordered;
|
|
725
|
+
}
|
|
716
726
|
function isModelEnabled(model, modelVisibility) {
|
|
717
727
|
if (model.configurationMissing) return false;
|
|
718
728
|
const enabled = modelVisibility && Object.hasOwn(modelVisibility, model.id) ? modelVisibility[model.id] : void 0;
|
|
@@ -744,6 +754,8 @@ function parseGoModels(value) {
|
|
|
744
754
|
if (typeof row.deprecated === "boolean") model.deprecated = row.deprecated;
|
|
745
755
|
if (typeof row.configurationMissing === "boolean") model.configurationMissing = row.configurationMissing;
|
|
746
756
|
if (validReleaseDate(row.releaseDate)) model.releaseDate = row.releaseDate;
|
|
757
|
+
const modalities = normalizeInputModalities(row.inputModalities);
|
|
758
|
+
if (modalities !== void 0) model.inputModalities = modalities;
|
|
747
759
|
return model;
|
|
748
760
|
});
|
|
749
761
|
}
|
|
@@ -849,9 +861,11 @@ function readModelMetadata(body, baseURL, builtin) {
|
|
|
849
861
|
const details = /* @__PURE__ */ new Map();
|
|
850
862
|
for (const [id, value] of Object.entries(entries)) {
|
|
851
863
|
const data = record(value);
|
|
864
|
+
const inputModalities = normalizeInputModalities(record(data.modalities).input);
|
|
852
865
|
details.set(id, {
|
|
853
866
|
deprecated: data.status === "deprecated",
|
|
854
|
-
...validReleaseDate(data.release_date) ? { releaseDate: data.release_date } : {}
|
|
867
|
+
...validReleaseDate(data.release_date) ? { releaseDate: data.release_date } : {},
|
|
868
|
+
...inputModalities === void 0 ? {} : { inputModalities }
|
|
855
869
|
});
|
|
856
870
|
try {
|
|
857
871
|
const metadata = record(value);
|
|
@@ -993,6 +1007,55 @@ async function readJsonResponse(response, maxBytes) {
|
|
|
993
1007
|
}
|
|
994
1008
|
}
|
|
995
1009
|
|
|
1010
|
+
// src/metadata-cache.ts
|
|
1011
|
+
import { randomUUID } from "node:crypto";
|
|
1012
|
+
import { createReadStream } from "node:fs";
|
|
1013
|
+
import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
1014
|
+
import { homedir } from "node:os";
|
|
1015
|
+
import { dirname, join, resolve } from "node:path";
|
|
1016
|
+
var MODEL_METADATA_MAX_BYTES = 16 * 1024 * 1024;
|
|
1017
|
+
var CACHE_MAX_BYTES = MODEL_METADATA_MAX_BYTES + 4096;
|
|
1018
|
+
function metadataCachePath() {
|
|
1019
|
+
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
1020
|
+
return resolve(home, "cache", "dsh-opencode-go", "models.dev.api.json");
|
|
1021
|
+
}
|
|
1022
|
+
function metadataETag(value) {
|
|
1023
|
+
return typeof value === "string" && value.length <= 1024 && /^(W\/)?"[\x21\x23-\x7e\x80-\xff]*"$/.test(value) ? value : void 0;
|
|
1024
|
+
}
|
|
1025
|
+
async function readMetadataCache(path) {
|
|
1026
|
+
try {
|
|
1027
|
+
const info = await stat(path);
|
|
1028
|
+
if (!info.isFile() || info.size > CACHE_MAX_BYTES) return void 0;
|
|
1029
|
+
const chunks = [];
|
|
1030
|
+
let size = 0;
|
|
1031
|
+
for await (const chunk of createReadStream(path)) {
|
|
1032
|
+
const bytes = chunk;
|
|
1033
|
+
size += bytes.length;
|
|
1034
|
+
if (size > CACHE_MAX_BYTES) return void 0;
|
|
1035
|
+
chunks.push(bytes);
|
|
1036
|
+
}
|
|
1037
|
+
const value = JSON.parse(Buffer.concat(chunks, size).toString("utf8"));
|
|
1038
|
+
if (!value || value.version !== 1 || value.sourceURL !== MODEL_METADATA_URL || typeof value.savedAtMs !== "number" || !Number.isSafeInteger(value.savedAtMs) || value.savedAtMs < 0 || value.savedAtMs > Date.now() || value.etag !== void 0 && metadataETag(value.etag) === void 0) return void 0;
|
|
1039
|
+
return { body: value.body, savedAtMs: value.savedAtMs, etag: metadataETag(value.etag) };
|
|
1040
|
+
} catch {
|
|
1041
|
+
return void 0;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async function writeMetadataCache(path, value) {
|
|
1045
|
+
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
1046
|
+
try {
|
|
1047
|
+
const content = JSON.stringify({ version: 1, sourceURL: MODEL_METADATA_URL, ...value });
|
|
1048
|
+
if (Buffer.byteLength(content) > CACHE_MAX_BYTES) return;
|
|
1049
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1050
|
+
await writeFile(temporary, content, { flag: "wx", mode: 384 });
|
|
1051
|
+
await rename(temporary, path);
|
|
1052
|
+
} catch {
|
|
1053
|
+
} finally {
|
|
1054
|
+
await rm(temporary, { force: true }).catch(() => {
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
996
1059
|
// src/catalog.ts
|
|
997
1060
|
var PROVIDER_ID = "opencode-go";
|
|
998
1061
|
var DISPLAY_NAME = "OpenCode Go";
|
|
@@ -1001,19 +1064,22 @@ var MODELS_FETCH_TIMEOUT_MS = 1e4;
|
|
|
1001
1064
|
var METADATA_FETCH_TIMEOUT_MS = 3e4;
|
|
1002
1065
|
var STALE_WHILE_REVALIDATE_MS = 5 * 6e4;
|
|
1003
1066
|
var MODEL_LISTING_MAX_BYTES = 1024 * 1024;
|
|
1004
|
-
var MODEL_METADATA_MAX_BYTES = 16 * 1024 * 1024;
|
|
1005
1067
|
var RETRY_MIN_MS = 5e3;
|
|
1006
1068
|
var RETRY_MAX_MS = 6e4;
|
|
1069
|
+
async function settled(promise) {
|
|
1070
|
+
const [result] = await Promise.allSettled([promise]);
|
|
1071
|
+
return result;
|
|
1072
|
+
}
|
|
1007
1073
|
function waitForSnapshot(pending, signal) {
|
|
1008
1074
|
if (signal === void 0) return pending;
|
|
1009
|
-
return new Promise((
|
|
1075
|
+
return new Promise((resolve2, reject) => {
|
|
1010
1076
|
const abort = () => {
|
|
1011
1077
|
signal.removeEventListener("abort", abort);
|
|
1012
1078
|
reject(new LlmError5("opencode-go catalog request aborted by caller", "ABORTED", { cause: signal.reason }));
|
|
1013
1079
|
};
|
|
1014
1080
|
pending.then((value) => {
|
|
1015
1081
|
signal.removeEventListener("abort", abort);
|
|
1016
|
-
|
|
1082
|
+
resolve2(value);
|
|
1017
1083
|
}, (error) => {
|
|
1018
1084
|
signal.removeEventListener("abort", abort);
|
|
1019
1085
|
reject(error);
|
|
@@ -1084,15 +1150,22 @@ function buildProvider(baseURL, models) {
|
|
|
1084
1150
|
});
|
|
1085
1151
|
}
|
|
1086
1152
|
var OpencodeGoCatalog = class {
|
|
1087
|
-
constructor(baseURL, refreshMs, onFallback, onOmitted) {
|
|
1153
|
+
constructor(baseURL, refreshMs, onFallback, onOmitted, onRefresh = () => {
|
|
1154
|
+
}) {
|
|
1088
1155
|
this.baseURL = baseURL;
|
|
1089
1156
|
this.refreshMs = refreshMs;
|
|
1090
1157
|
this.onFallback = onFallback;
|
|
1091
1158
|
this.onOmitted = onOmitted;
|
|
1159
|
+
this.onRefresh = onRefresh;
|
|
1092
1160
|
}
|
|
1093
1161
|
served;
|
|
1094
1162
|
pending;
|
|
1163
|
+
/** A cold runtime read can use disk metadata while the shared online refresh runs. */
|
|
1164
|
+
cachedPending;
|
|
1165
|
+
initialized;
|
|
1166
|
+
cachePath = metadataCachePath();
|
|
1095
1167
|
metadata;
|
|
1168
|
+
metadataBody;
|
|
1096
1169
|
metadataETag;
|
|
1097
1170
|
metadataUpdatedAtMs;
|
|
1098
1171
|
failures = 0;
|
|
@@ -1101,19 +1174,63 @@ var OpencodeGoCatalog = class {
|
|
|
1101
1174
|
if (signal?.aborted) {
|
|
1102
1175
|
return Promise.reject(new LlmError5("opencode-go catalog request aborted by caller", "ABORTED", { cause: signal.reason }));
|
|
1103
1176
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1177
|
+
return waitForSnapshot(this.served === void 0 ? this.readSnapshot(force) : this.currentSnapshot(force), signal);
|
|
1178
|
+
}
|
|
1179
|
+
async restoreMetadata() {
|
|
1180
|
+
const saved = await readMetadataCache(this.cachePath);
|
|
1181
|
+
if (saved === void 0) return;
|
|
1182
|
+
try {
|
|
1183
|
+
this.metadata = readModelMetadata(saved.body, this.baseURL, builtinModels(this.baseURL));
|
|
1184
|
+
this.metadataBody = saved.body;
|
|
1185
|
+
this.metadataETag = saved.etag;
|
|
1186
|
+
this.metadataUpdatedAtMs = saved.savedAtMs;
|
|
1187
|
+
} catch {
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async readSnapshot(force) {
|
|
1191
|
+
await (this.initialized ??= this.restoreMetadata());
|
|
1192
|
+
return this.currentSnapshot(force);
|
|
1193
|
+
}
|
|
1194
|
+
currentSnapshot(force) {
|
|
1195
|
+
if (!force && this.served !== void 0 && Date.now() < this.refreshAtMs) return Promise.resolve(this.served);
|
|
1196
|
+
if (this.pending === void 0) this.startRefresh(force);
|
|
1197
|
+
return !force && this.cachedPending !== void 0 ? this.cachedPending : this.pending;
|
|
1198
|
+
}
|
|
1199
|
+
startRefresh(force) {
|
|
1200
|
+
const builtin = builtinModels(this.baseURL);
|
|
1201
|
+
const listing = settled(fetchLiveModelIds(this.baseURL).then((ids) => ({ ids, updatedAtMs: Date.now() })));
|
|
1202
|
+
const restored = this.metadata;
|
|
1203
|
+
if (!force && this.served === void 0 && restored !== void 0) {
|
|
1204
|
+
const status = { metadataLive: false, metadataUpdatedAtMs: this.metadataUpdatedAtMs };
|
|
1205
|
+
this.cachedPending = listing.then((result) => {
|
|
1206
|
+
const snapshot = this.build(builtin, result, restored, status);
|
|
1207
|
+
this.served = snapshot;
|
|
1208
|
+
return snapshot;
|
|
1209
|
+
});
|
|
1106
1210
|
}
|
|
1107
|
-
this.
|
|
1211
|
+
const notify = this.cachedPending !== void 0 || !force && this.served !== void 0;
|
|
1212
|
+
this.pending = Promise.all([listing, settled(this.refreshMetadata(builtin)), this.cachedPending]).then(([listing2, result]) => this.build(
|
|
1213
|
+
builtin,
|
|
1214
|
+
listing2,
|
|
1215
|
+
result.status === "fulfilled" ? result.value : this.metadata,
|
|
1216
|
+
{
|
|
1217
|
+
metadataLive: result.status === "fulfilled",
|
|
1218
|
+
...this.metadataUpdatedAtMs === void 0 ? {} : { metadataUpdatedAtMs: this.metadataUpdatedAtMs },
|
|
1219
|
+
...result.status === "rejected" ? { metadataFailure: result.reason } : {}
|
|
1220
|
+
}
|
|
1221
|
+
)).then((snapshot) => {
|
|
1108
1222
|
this.served = snapshot;
|
|
1109
1223
|
this.failures = snapshot.live && snapshot.metadataLive ? 0 : Math.min(this.failures + 1, 5);
|
|
1110
1224
|
const lifetime = this.failures === 0 ? this.refreshMs : Math.min(this.refreshMs, RETRY_MIN_MS * 2 ** (this.failures - 1), RETRY_MAX_MS);
|
|
1111
1225
|
this.refreshAtMs = snapshot.fetchedAtMs + lifetime;
|
|
1226
|
+
if (notify) this.onRefresh();
|
|
1112
1227
|
return snapshot;
|
|
1113
1228
|
}).finally(() => {
|
|
1114
1229
|
this.pending = void 0;
|
|
1230
|
+
this.cachedPending = void 0;
|
|
1231
|
+
});
|
|
1232
|
+
void this.pending.catch(() => {
|
|
1115
1233
|
});
|
|
1116
|
-
return waitForSnapshot(this.pending, signal);
|
|
1117
1234
|
}
|
|
1118
1235
|
/** Conditional HTTP requests save bandwidth while still checking for updated metadata. */
|
|
1119
1236
|
async refreshMetadata(builtin) {
|
|
@@ -1128,6 +1245,8 @@ var OpencodeGoCatalog = class {
|
|
|
1128
1245
|
}, MODEL_METADATA_MAX_BYTES);
|
|
1129
1246
|
if (response.status === 304 && this.metadata !== void 0) {
|
|
1130
1247
|
this.metadataUpdatedAtMs = Date.now();
|
|
1248
|
+
this.metadataETag = metadataETag(response.headers.get("etag")) ?? this.metadataETag;
|
|
1249
|
+
await this.persistMetadata();
|
|
1131
1250
|
return this.metadata;
|
|
1132
1251
|
}
|
|
1133
1252
|
if (!response.ok) throw new LlmError5(`${MODEL_METADATA_URL} answered HTTP ${response.status}`, "DISCOVERY_FAILED");
|
|
@@ -1138,26 +1257,24 @@ var OpencodeGoCatalog = class {
|
|
|
1138
1257
|
throw new LlmError5(`${MODEL_METADATA_URL} returned invalid model configuration`, "DISCOVERY_FAILED", { cause: error });
|
|
1139
1258
|
}
|
|
1140
1259
|
this.metadata = metadata;
|
|
1141
|
-
this.
|
|
1260
|
+
this.metadataBody = body;
|
|
1261
|
+
this.metadataETag = metadataETag(response.headers.get("etag"));
|
|
1142
1262
|
this.metadataUpdatedAtMs = Date.now();
|
|
1263
|
+
await this.persistMetadata();
|
|
1143
1264
|
return metadata;
|
|
1144
1265
|
}
|
|
1266
|
+
async persistMetadata() {
|
|
1267
|
+
await writeMetadataCache(this.cachePath, {
|
|
1268
|
+
body: this.metadataBody,
|
|
1269
|
+
etag: this.metadataETag,
|
|
1270
|
+
savedAtMs: this.metadataUpdatedAtMs
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1145
1273
|
/** Gateway ids decide membership; online metadata decides how to call each model. */
|
|
1146
|
-
|
|
1147
|
-
const builtin = builtinModels(this.baseURL);
|
|
1148
|
-
const [listing, metadataResult] = await Promise.allSettled([
|
|
1149
|
-
fetchLiveModelIds(this.baseURL).then((ids) => ({ ids, updatedAtMs: Date.now() })),
|
|
1150
|
-
this.refreshMetadata(builtin)
|
|
1151
|
-
]);
|
|
1152
|
-
const metadata = metadataResult.status === "fulfilled" ? metadataResult.value : this.metadata;
|
|
1153
|
-
const metadataStatus = {
|
|
1154
|
-
metadataLive: metadataResult.status === "fulfilled",
|
|
1155
|
-
...this.metadataUpdatedAtMs === void 0 ? {} : { metadataUpdatedAtMs: this.metadataUpdatedAtMs },
|
|
1156
|
-
...metadataResult.status === "rejected" ? { metadataFailure: metadataResult.reason } : {}
|
|
1157
|
-
};
|
|
1274
|
+
build(builtin, listing, metadata, metadataStatus) {
|
|
1158
1275
|
const known = new Map([...builtin, ...this.served?.models ?? [], ...metadata?.models ?? []]);
|
|
1159
|
-
if (
|
|
1160
|
-
this.onFallback({ url: MODEL_METADATA_URL, error:
|
|
1276
|
+
if (metadataStatus.metadataFailure !== void 0) {
|
|
1277
|
+
this.onFallback({ url: MODEL_METADATA_URL, error: metadataStatus.metadataFailure, kept: known.size });
|
|
1161
1278
|
}
|
|
1162
1279
|
if (listing.status === "rejected") {
|
|
1163
1280
|
const models2 = this.served?.models ?? /* @__PURE__ */ new Map();
|
|
@@ -1185,7 +1302,9 @@ var OpencodeGoCatalog = class {
|
|
|
1185
1302
|
models.set(id, model);
|
|
1186
1303
|
}
|
|
1187
1304
|
}
|
|
1188
|
-
if (unavailable.size > 0
|
|
1305
|
+
if (unavailable.size > 0 && (metadataStatus.metadataLive || metadataStatus.metadataFailure !== void 0)) {
|
|
1306
|
+
this.onOmitted([...unavailable.keys()]);
|
|
1307
|
+
}
|
|
1189
1308
|
return {
|
|
1190
1309
|
details: new Map(listing.value.ids.map((id) => [id, metadata?.details.get(id) ?? {}])),
|
|
1191
1310
|
models,
|
|
@@ -1207,7 +1326,9 @@ var OpencodeGoCatalog = class {
|
|
|
1207
1326
|
return cached;
|
|
1208
1327
|
}
|
|
1209
1328
|
let snapshot = await this.snapshot(false, signal);
|
|
1210
|
-
if (!snapshot.models.has(id) && snapshot === cached
|
|
1329
|
+
if (!snapshot.models.has(id) && (snapshot === cached || this.cachedPending !== void 0)) {
|
|
1330
|
+
snapshot = await this.snapshot(true, signal);
|
|
1331
|
+
}
|
|
1211
1332
|
if (signal?.aborted) throw new LlmError5("opencode-go catalog request aborted by caller", "ABORTED", { cause: signal.reason });
|
|
1212
1333
|
if (snapshot.unavailable.has(id)) {
|
|
1213
1334
|
throw new LlmError5(
|
|
@@ -1286,6 +1407,7 @@ var fields = {
|
|
|
1286
1407
|
baseURL: z.string().default(DEFAULT_BASE_URL),
|
|
1287
1408
|
refreshMinutes: z.number().step(1).min(1).max(7 * 24 * 60).default(DEFAULT_REFRESH_MINUTES),
|
|
1288
1409
|
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
1410
|
+
maxImages: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
|
|
1289
1411
|
// The image defaults are the generic pi-ai adapter's: one normalized
|
|
1290
1412
|
// request image fits the budget, and fifteen of them fit the payload cap.
|
|
1291
1413
|
maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
|
|
@@ -1332,7 +1454,7 @@ function withModelLimit(model, limits) {
|
|
|
1332
1454
|
};
|
|
1333
1455
|
}
|
|
1334
1456
|
function opencodeSessionValue(sessionId) {
|
|
1335
|
-
return sessionId !== void 0 && sessionId.length > 0 ? sessionId :
|
|
1457
|
+
return sessionId !== void 0 && sessionId.length > 0 ? sessionId : randomUUID2();
|
|
1336
1458
|
}
|
|
1337
1459
|
var OpencodeGoAdapter = class extends LlmAdapter {
|
|
1338
1460
|
constructor(options) {
|
|
@@ -1356,19 +1478,20 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1356
1478
|
catalogOf(config) {
|
|
1357
1479
|
const key = `${config.baseURL}|${String(config.refreshMinutes)}`;
|
|
1358
1480
|
if (this.catalogCache?.key !== key) {
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1481
|
+
const catalog = new OpencodeGoCatalog(
|
|
1482
|
+
assertBaseURL(config.baseURL),
|
|
1483
|
+
config.refreshMinutes * 6e4,
|
|
1484
|
+
/* v8 ignore next -- the plugin always passes both observers; the defaults exist for direct construction */
|
|
1485
|
+
this.options.onFallback ?? (() => {
|
|
1486
|
+
}),
|
|
1487
|
+
/* v8 ignore next -- the plugin always passes both observers; the defaults exist for direct construction */
|
|
1488
|
+
this.options.onOmitted ?? (() => {
|
|
1489
|
+
}),
|
|
1490
|
+
() => {
|
|
1491
|
+
if (this.catalogCache?.catalog === catalog) this.options.onCatalogRefresh?.();
|
|
1492
|
+
}
|
|
1493
|
+
);
|
|
1494
|
+
this.catalogCache = { key, catalog };
|
|
1372
1495
|
}
|
|
1373
1496
|
return this.catalogCache.catalog;
|
|
1374
1497
|
}
|
|
@@ -1476,6 +1599,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1476
1599
|
imageRequest = {
|
|
1477
1600
|
attachments: store,
|
|
1478
1601
|
resolveImageAccess: (ref) => access.resolveImageAccess(store, ref),
|
|
1602
|
+
maxImages: config.maxImages ?? void 0,
|
|
1479
1603
|
maxRequestImageBytes: config.maxRequestImageBytes,
|
|
1480
1604
|
requestImagePolicy: {
|
|
1481
1605
|
maxPixels: config.requestImagePixelBudget,
|
|
@@ -1543,7 +1667,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1543
1667
|
};
|
|
1544
1668
|
|
|
1545
1669
|
// src/usage.ts
|
|
1546
|
-
import { randomUUID as
|
|
1670
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1547
1671
|
import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
1548
1672
|
import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
|
|
1549
1673
|
|
|
@@ -1615,7 +1739,7 @@ var GoUsageService = class extends TypertRemoteService {
|
|
|
1615
1739
|
});
|
|
1616
1740
|
}
|
|
1617
1741
|
if (this.identity?.baseURL !== baseURL || this.identity.key !== key) {
|
|
1618
|
-
this.identity = { baseURL, key, source:
|
|
1742
|
+
this.identity = { baseURL, key, source: randomUUID3() };
|
|
1619
1743
|
}
|
|
1620
1744
|
const { source } = this.identity;
|
|
1621
1745
|
const endpoint = diagnosticURL(`${baseURL}/usage`);
|
|
@@ -1721,6 +1845,7 @@ function apply(ctx, raw) {
|
|
|
1721
1845
|
ctx.logger.warn(`llm-opencode-go: gateway models awaiting usable online metadata: ${ids.join(", ")}`);
|
|
1722
1846
|
}
|
|
1723
1847
|
};
|
|
1848
|
+
let registration;
|
|
1724
1849
|
const adapter = new OpencodeGoAdapter({
|
|
1725
1850
|
config: () => current(),
|
|
1726
1851
|
resolveApiKey,
|
|
@@ -1734,11 +1859,13 @@ function apply(ctx, raw) {
|
|
|
1734
1859
|
},
|
|
1735
1860
|
onFallback: logger.fallback,
|
|
1736
1861
|
onOmitted: logger.omitted,
|
|
1862
|
+
onCatalogRefresh: () => {
|
|
1863
|
+
registration?.replace([PROVIDER_ID]);
|
|
1864
|
+
},
|
|
1737
1865
|
onReplayDegrade: (reason) => {
|
|
1738
1866
|
ctx.logger.warn(`llm-opencode-go: unusable replay state on assistant history; sending provider-neutral content (${reason})`);
|
|
1739
1867
|
}
|
|
1740
1868
|
});
|
|
1741
|
-
let registration;
|
|
1742
1869
|
ctx.plugin(GoModelsService, {
|
|
1743
1870
|
catalog: () => adapter.catalogOf(current()),
|
|
1744
1871
|
onRefresh: () => {
|
|
@@ -1791,6 +1918,7 @@ function apply(ctx, raw) {
|
|
|
1791
1918
|
});
|
|
1792
1919
|
ctx.effect(() => () => {
|
|
1793
1920
|
registration?.();
|
|
1921
|
+
registration = void 0;
|
|
1794
1922
|
undiscover();
|
|
1795
1923
|
});
|
|
1796
1924
|
ctx.inject(["settings"], (settingsCtx) => {
|
package/lib/types/adapter.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface OpencodeGoAdapterOptions {
|
|
|
62
62
|
onOmitted?: (ids: readonly string[]) => void;
|
|
63
63
|
/** Observe assistant history degrading to provider-neutral conversion. */
|
|
64
64
|
onReplayDegrade?: (reason: string) => void;
|
|
65
|
+
/** Re-read picker models after a background catalog refresh commits. */
|
|
66
|
+
onCatalogRefresh?: () => void;
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
67
69
|
* The single route's adapter. The catalog snapshot freezes at each operation,
|
package/lib/types/catalog.d.ts
CHANGED
|
@@ -30,9 +30,15 @@ export declare class OpencodeGoCatalog {
|
|
|
30
30
|
private readonly onFallback;
|
|
31
31
|
/** Kept for API compatibility; now reports unconfigured ids rather than hiding them. */
|
|
32
32
|
private readonly onOmitted;
|
|
33
|
+
private readonly onRefresh;
|
|
33
34
|
private served;
|
|
34
35
|
private pending;
|
|
36
|
+
/** A cold runtime read can use disk metadata while the shared online refresh runs. */
|
|
37
|
+
private cachedPending;
|
|
38
|
+
private initialized;
|
|
39
|
+
private readonly cachePath;
|
|
35
40
|
private metadata;
|
|
41
|
+
private metadataBody;
|
|
36
42
|
private metadataETag;
|
|
37
43
|
private metadataUpdatedAtMs;
|
|
38
44
|
private failures;
|
|
@@ -43,10 +49,15 @@ export declare class OpencodeGoCatalog {
|
|
|
43
49
|
kept: number;
|
|
44
50
|
}) => void,
|
|
45
51
|
/** Kept for API compatibility; now reports unconfigured ids rather than hiding them. */
|
|
46
|
-
onOmitted: (ids: readonly string[]) => void);
|
|
52
|
+
onOmitted: (ids: readonly string[]) => void, onRefresh?: () => void);
|
|
47
53
|
snapshot(force?: boolean, signal?: AbortSignal): Promise<CatalogSnapshot>;
|
|
54
|
+
private restoreMetadata;
|
|
55
|
+
private readSnapshot;
|
|
56
|
+
private currentSnapshot;
|
|
57
|
+
private startRefresh;
|
|
48
58
|
/** Conditional HTTP requests save bandwidth while still checking for updated metadata. */
|
|
49
59
|
private refreshMetadata;
|
|
60
|
+
private persistMetadata;
|
|
50
61
|
/** Gateway ids decide membership; online metadata decides how to call each model. */
|
|
51
62
|
private build;
|
|
52
63
|
/** New or previously unconfigured ids get a fresh lookup even during the runtime TTL. */
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The OpenCode Go settings
|
|
3
|
-
*
|
|
4
|
-
* the models the gateway currently serves
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* The OpenCode Go settings page. Three cards carry the page's whole order — the
|
|
3
|
+
* connection (the switch that routes the provider plus the API key it
|
|
4
|
+
* authenticates with), the models the gateway currently serves and their
|
|
5
|
+
* capacities, and the adapter tuning fields behind a collapsed disclosure at the
|
|
6
|
+
* foot. The credential reference, the endpoint, and the tuning knobs all live in
|
|
7
|
+
* the `llm-opencode-go` namespace, so every card writes the same document.
|
|
7
8
|
*/
|
|
8
9
|
import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
|
|
9
10
|
import type { OpencodeGoSectionFace } from './section-controller.ts';
|
|
@@ -19,6 +19,7 @@ export declare const en: {
|
|
|
19
19
|
usageRefreshing: string;
|
|
20
20
|
nav: string;
|
|
21
21
|
title: string;
|
|
22
|
+
titleLabel: string;
|
|
22
23
|
intro: string;
|
|
23
24
|
starProject: string;
|
|
24
25
|
starProjectTitle: string;
|
|
@@ -56,6 +57,15 @@ export declare const en: {
|
|
|
56
57
|
limitsOutput: string;
|
|
57
58
|
capacityDefault: string;
|
|
58
59
|
capacityMissing: string;
|
|
60
|
+
modalityLabel: string;
|
|
61
|
+
modalityText: string;
|
|
62
|
+
modalityImage: string;
|
|
63
|
+
modalityAudio: string;
|
|
64
|
+
modalityVideo: string;
|
|
65
|
+
modalityPdf: string;
|
|
66
|
+
modalityUnknown: string;
|
|
67
|
+
modalitiesSource: string;
|
|
68
|
+
modalitiesForwarding: string;
|
|
59
69
|
visibilityHint: string;
|
|
60
70
|
modelVisibleLabel: string;
|
|
61
71
|
configurationMissing: string;
|
|
@@ -67,7 +77,6 @@ export declare const en: {
|
|
|
67
77
|
deprecatedHint: string;
|
|
68
78
|
newHint: string;
|
|
69
79
|
newBadge: string;
|
|
70
|
-
releasedOn: string;
|
|
71
80
|
releaseSource: string;
|
|
72
81
|
modelDetails: string;
|
|
73
82
|
filterLabel: string;
|
|
@@ -81,6 +90,7 @@ export declare const en: {
|
|
|
81
90
|
limitsEmpty: string;
|
|
82
91
|
limitsContextLabel: string;
|
|
83
92
|
limitsOutputLabel: string;
|
|
93
|
+
advancedSummary: string;
|
|
84
94
|
advancedLabel: string;
|
|
85
95
|
advancedHint: string;
|
|
86
96
|
apiKeyEnvLabel: string;
|
|
@@ -91,6 +101,8 @@ export declare const en: {
|
|
|
91
101
|
refreshMinutesHint: string;
|
|
92
102
|
streamIdleTimeoutMsLabel: string;
|
|
93
103
|
streamIdleTimeoutMsHint: string;
|
|
104
|
+
maxImagesLabel: string;
|
|
105
|
+
maxImagesHint: string;
|
|
94
106
|
maxRequestImageBytesLabel: string;
|
|
95
107
|
maxRequestImageBytesHint: string;
|
|
96
108
|
requestImagePixelBudgetLabel: string;
|
|
@@ -30,6 +30,8 @@ export interface OpencodeGoSettings {
|
|
|
30
30
|
refreshMinutes?: number;
|
|
31
31
|
/** Largest idle gap between stream events, in milliseconds. */
|
|
32
32
|
streamIdleTimeoutMs?: number;
|
|
33
|
+
/** Optional retained image occurrence cap per request. */
|
|
34
|
+
maxImages?: number | null;
|
|
33
35
|
/** Accumulated base64 image payload bound for one request. */
|
|
34
36
|
maxRequestImageBytes?: number;
|
|
35
37
|
/** Total-pixel budget for one request image. */
|
|
@@ -94,6 +96,8 @@ export interface OpencodeGoSectionState extends FormShell {
|
|
|
94
96
|
refreshMinutes: FieldState;
|
|
95
97
|
/** Largest idle gap between stream events, in milliseconds. */
|
|
96
98
|
streamIdleTimeoutMs: FieldState;
|
|
99
|
+
/** Optional retained image occurrence cap; blank inherits the base configuration. */
|
|
100
|
+
maxImages: FieldState;
|
|
97
101
|
/** Accumulated base64 image payload bound for one request. */
|
|
98
102
|
maxRequestImageBytes: FieldState;
|
|
99
103
|
/** Total-pixel budget for one request image. */
|
|
@@ -88,12 +88,13 @@ export interface FormActions {
|
|
|
88
88
|
discard: () => void;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* A
|
|
92
|
-
*
|
|
91
|
+
* A numeric field. An empty draft clears the field; other drafts must be
|
|
92
|
+
* finite numbers and satisfy the optional constraint before saving.
|
|
93
93
|
* @param field - field name inside the namespace section.
|
|
94
|
+
* @param validate - additional numeric constraint, if needed.
|
|
94
95
|
* @returns the field's conversion spec.
|
|
95
96
|
*/
|
|
96
|
-
export declare function numberField(field: string): FieldSpec;
|
|
97
|
+
export declare function numberField(field: string, validate?: (value: number) => boolean): FieldSpec;
|
|
97
98
|
/**
|
|
98
99
|
* A two-state field. Unlike the text fields, the draft is the boolean itself
|
|
99
100
|
* rather than text: the control that renders it reports the state the user
|
package/lib/types/config.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface OpencodeGoConfig {
|
|
|
48
48
|
refreshMinutes: number;
|
|
49
49
|
/** Largest idle gap between stream events before the request fails. */
|
|
50
50
|
streamIdleTimeoutMs: number;
|
|
51
|
+
/** Optional retained image occurrence cap per request; omission or null leaves the count unlimited. */
|
|
52
|
+
maxImages?: number | null;
|
|
51
53
|
/** Request-level bound on base64-encoded image payload, in bytes. */
|
|
52
54
|
maxRequestImageBytes: number;
|
|
53
55
|
/** Total-pixel budget for one request image. */
|
|
@@ -12,6 +12,8 @@ export interface PiImageRequestContext {
|
|
|
12
12
|
attachments: AttachmentStore;
|
|
13
13
|
/** Resolve current tool access separately from deterministic request-image versions. */
|
|
14
14
|
resolveImageAccess: ImageAttachmentAccessResolver;
|
|
15
|
+
/** Maximum retained image occurrences across request history; omission leaves the count unlimited. */
|
|
16
|
+
maxImages?: number;
|
|
15
17
|
/** Request-level bound on the base64-encoded payload of retained images; omission leaves the bound unchecked. */
|
|
16
18
|
maxRequestImageBytes?: number;
|
|
17
19
|
/** Route pixel and raw encoded-byte budgets. */
|
|
@@ -39,7 +41,7 @@ export declare function toPiContext(options: GenerateOptions, images?: undefined
|
|
|
39
41
|
* Tool result names are recovered from preceding assistant tool calls. On DSH
|
|
40
42
|
* 0.1.5, oldest images over the request budget become transient placeholders.
|
|
41
43
|
* On newer hosts, occurrences the surface marks offloaded become placeholders; when the
|
|
42
|
-
* retained occurrences
|
|
44
|
+
* retained occurrences exceed `maxImages` or their exact base64 payload exceeds
|
|
43
45
|
* `maxRequestImageBytes`, the call fails with `IMAGE_OFFLOAD_REQUIRED` naming
|
|
44
46
|
* how many more oldest occurrences must be offloaded.
|
|
45
47
|
* @param options - the harness request; `options.system`, else a leading `system` message, maps to pi-ai's single `systemPrompt` slot.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Message } from '@deepseek-ai/dsh-llm';
|
|
2
2
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
3
3
|
interface ImageProjectionPolicy {
|
|
4
|
+
maxImages?: number;
|
|
4
5
|
maxBytes?: number;
|
|
5
6
|
byteLength: (ref: ImageAttachmentRef) => number;
|
|
6
7
|
placeholder: (ref: ImageAttachmentRef) => string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const MODEL_METADATA_MAX_BYTES: number;
|
|
2
|
+
export interface CachedMetadata {
|
|
3
|
+
readonly body: unknown;
|
|
4
|
+
readonly etag?: string;
|
|
5
|
+
/** Last successful download or HTTP 304 verification. */
|
|
6
|
+
readonly savedAtMs: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function metadataCachePath(): string;
|
|
9
|
+
/** Only persist validators that can safely become an HTTP request header. */
|
|
10
|
+
export declare function metadataETag(value: unknown): string | undefined;
|
|
11
|
+
export declare function readMetadataCache(path: string): Promise<CachedMetadata | undefined>;
|
|
12
|
+
export declare function writeMetadataCache(path: string, value: CachedMetadata): Promise<void>;
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import type { Api, Model } from '@earendil-works/pi-ai';
|
|
3
3
|
import { type GoModel } from './models-contract.ts';
|
|
4
4
|
export declare const MODEL_METADATA_URL = "https://models.dev/api.json";
|
|
5
|
+
/** Lifecycle and capability data the online catalog adds to a gateway listing. */
|
|
6
|
+
export type ModelDetails = Pick<GoModel, 'deprecated' | 'releaseDate' | 'inputModalities'>;
|
|
5
7
|
export interface ModelMetadata {
|
|
6
8
|
readonly models: ReadonlyMap<string, Model<Api>>;
|
|
7
|
-
readonly details: ReadonlyMap<string,
|
|
9
|
+
readonly details: ReadonlyMap<string, ModelDetails>;
|
|
8
10
|
readonly errors: ReadonlyMap<string, string>;
|
|
9
11
|
}
|
|
10
12
|
/** Anthropic's SDK appends /v1/messages; the OpenAI SDKs append paths below /v1. */
|