dsh-opencode-go 0.1.15 → 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 +28 -4
- package/README.md +28 -4
- package/docs/development.md +64 -0
- package/docs/independent-package.md +2 -0
- package/docs/verification.md +74 -0
- package/lib/client.js +328 -253
- package/lib/index.js +167 -44
- 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 +11 -1
- 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,
|
|
@@ -716,6 +716,13 @@ import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.l
|
|
|
716
716
|
import { attributionHeaders, LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
|
|
717
717
|
|
|
718
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
|
+
}
|
|
719
726
|
function isModelEnabled(model, modelVisibility) {
|
|
720
727
|
if (model.configurationMissing) return false;
|
|
721
728
|
const enabled = modelVisibility && Object.hasOwn(modelVisibility, model.id) ? modelVisibility[model.id] : void 0;
|
|
@@ -747,6 +754,8 @@ function parseGoModels(value) {
|
|
|
747
754
|
if (typeof row.deprecated === "boolean") model.deprecated = row.deprecated;
|
|
748
755
|
if (typeof row.configurationMissing === "boolean") model.configurationMissing = row.configurationMissing;
|
|
749
756
|
if (validReleaseDate(row.releaseDate)) model.releaseDate = row.releaseDate;
|
|
757
|
+
const modalities = normalizeInputModalities(row.inputModalities);
|
|
758
|
+
if (modalities !== void 0) model.inputModalities = modalities;
|
|
750
759
|
return model;
|
|
751
760
|
});
|
|
752
761
|
}
|
|
@@ -852,9 +861,11 @@ function readModelMetadata(body, baseURL, builtin) {
|
|
|
852
861
|
const details = /* @__PURE__ */ new Map();
|
|
853
862
|
for (const [id, value] of Object.entries(entries)) {
|
|
854
863
|
const data = record(value);
|
|
864
|
+
const inputModalities = normalizeInputModalities(record(data.modalities).input);
|
|
855
865
|
details.set(id, {
|
|
856
866
|
deprecated: data.status === "deprecated",
|
|
857
|
-
...validReleaseDate(data.release_date) ? { releaseDate: data.release_date } : {}
|
|
867
|
+
...validReleaseDate(data.release_date) ? { releaseDate: data.release_date } : {},
|
|
868
|
+
...inputModalities === void 0 ? {} : { inputModalities }
|
|
858
869
|
});
|
|
859
870
|
try {
|
|
860
871
|
const metadata = record(value);
|
|
@@ -996,6 +1007,55 @@ async function readJsonResponse(response, maxBytes) {
|
|
|
996
1007
|
}
|
|
997
1008
|
}
|
|
998
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
|
+
|
|
999
1059
|
// src/catalog.ts
|
|
1000
1060
|
var PROVIDER_ID = "opencode-go";
|
|
1001
1061
|
var DISPLAY_NAME = "OpenCode Go";
|
|
@@ -1004,19 +1064,22 @@ var MODELS_FETCH_TIMEOUT_MS = 1e4;
|
|
|
1004
1064
|
var METADATA_FETCH_TIMEOUT_MS = 3e4;
|
|
1005
1065
|
var STALE_WHILE_REVALIDATE_MS = 5 * 6e4;
|
|
1006
1066
|
var MODEL_LISTING_MAX_BYTES = 1024 * 1024;
|
|
1007
|
-
var MODEL_METADATA_MAX_BYTES = 16 * 1024 * 1024;
|
|
1008
1067
|
var RETRY_MIN_MS = 5e3;
|
|
1009
1068
|
var RETRY_MAX_MS = 6e4;
|
|
1069
|
+
async function settled(promise) {
|
|
1070
|
+
const [result] = await Promise.allSettled([promise]);
|
|
1071
|
+
return result;
|
|
1072
|
+
}
|
|
1010
1073
|
function waitForSnapshot(pending, signal) {
|
|
1011
1074
|
if (signal === void 0) return pending;
|
|
1012
|
-
return new Promise((
|
|
1075
|
+
return new Promise((resolve2, reject) => {
|
|
1013
1076
|
const abort = () => {
|
|
1014
1077
|
signal.removeEventListener("abort", abort);
|
|
1015
1078
|
reject(new LlmError5("opencode-go catalog request aborted by caller", "ABORTED", { cause: signal.reason }));
|
|
1016
1079
|
};
|
|
1017
1080
|
pending.then((value) => {
|
|
1018
1081
|
signal.removeEventListener("abort", abort);
|
|
1019
|
-
|
|
1082
|
+
resolve2(value);
|
|
1020
1083
|
}, (error) => {
|
|
1021
1084
|
signal.removeEventListener("abort", abort);
|
|
1022
1085
|
reject(error);
|
|
@@ -1087,15 +1150,22 @@ function buildProvider(baseURL, models) {
|
|
|
1087
1150
|
});
|
|
1088
1151
|
}
|
|
1089
1152
|
var OpencodeGoCatalog = class {
|
|
1090
|
-
constructor(baseURL, refreshMs, onFallback, onOmitted) {
|
|
1153
|
+
constructor(baseURL, refreshMs, onFallback, onOmitted, onRefresh = () => {
|
|
1154
|
+
}) {
|
|
1091
1155
|
this.baseURL = baseURL;
|
|
1092
1156
|
this.refreshMs = refreshMs;
|
|
1093
1157
|
this.onFallback = onFallback;
|
|
1094
1158
|
this.onOmitted = onOmitted;
|
|
1159
|
+
this.onRefresh = onRefresh;
|
|
1095
1160
|
}
|
|
1096
1161
|
served;
|
|
1097
1162
|
pending;
|
|
1163
|
+
/** A cold runtime read can use disk metadata while the shared online refresh runs. */
|
|
1164
|
+
cachedPending;
|
|
1165
|
+
initialized;
|
|
1166
|
+
cachePath = metadataCachePath();
|
|
1098
1167
|
metadata;
|
|
1168
|
+
metadataBody;
|
|
1099
1169
|
metadataETag;
|
|
1100
1170
|
metadataUpdatedAtMs;
|
|
1101
1171
|
failures = 0;
|
|
@@ -1104,19 +1174,63 @@ var OpencodeGoCatalog = class {
|
|
|
1104
1174
|
if (signal?.aborted) {
|
|
1105
1175
|
return Promise.reject(new LlmError5("opencode-go catalog request aborted by caller", "ABORTED", { cause: signal.reason }));
|
|
1106
1176
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
+
});
|
|
1109
1210
|
}
|
|
1110
|
-
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) => {
|
|
1111
1222
|
this.served = snapshot;
|
|
1112
1223
|
this.failures = snapshot.live && snapshot.metadataLive ? 0 : Math.min(this.failures + 1, 5);
|
|
1113
1224
|
const lifetime = this.failures === 0 ? this.refreshMs : Math.min(this.refreshMs, RETRY_MIN_MS * 2 ** (this.failures - 1), RETRY_MAX_MS);
|
|
1114
1225
|
this.refreshAtMs = snapshot.fetchedAtMs + lifetime;
|
|
1226
|
+
if (notify) this.onRefresh();
|
|
1115
1227
|
return snapshot;
|
|
1116
1228
|
}).finally(() => {
|
|
1117
1229
|
this.pending = void 0;
|
|
1230
|
+
this.cachedPending = void 0;
|
|
1231
|
+
});
|
|
1232
|
+
void this.pending.catch(() => {
|
|
1118
1233
|
});
|
|
1119
|
-
return waitForSnapshot(this.pending, signal);
|
|
1120
1234
|
}
|
|
1121
1235
|
/** Conditional HTTP requests save bandwidth while still checking for updated metadata. */
|
|
1122
1236
|
async refreshMetadata(builtin) {
|
|
@@ -1131,6 +1245,8 @@ var OpencodeGoCatalog = class {
|
|
|
1131
1245
|
}, MODEL_METADATA_MAX_BYTES);
|
|
1132
1246
|
if (response.status === 304 && this.metadata !== void 0) {
|
|
1133
1247
|
this.metadataUpdatedAtMs = Date.now();
|
|
1248
|
+
this.metadataETag = metadataETag(response.headers.get("etag")) ?? this.metadataETag;
|
|
1249
|
+
await this.persistMetadata();
|
|
1134
1250
|
return this.metadata;
|
|
1135
1251
|
}
|
|
1136
1252
|
if (!response.ok) throw new LlmError5(`${MODEL_METADATA_URL} answered HTTP ${response.status}`, "DISCOVERY_FAILED");
|
|
@@ -1141,26 +1257,24 @@ var OpencodeGoCatalog = class {
|
|
|
1141
1257
|
throw new LlmError5(`${MODEL_METADATA_URL} returned invalid model configuration`, "DISCOVERY_FAILED", { cause: error });
|
|
1142
1258
|
}
|
|
1143
1259
|
this.metadata = metadata;
|
|
1144
|
-
this.
|
|
1260
|
+
this.metadataBody = body;
|
|
1261
|
+
this.metadataETag = metadataETag(response.headers.get("etag"));
|
|
1145
1262
|
this.metadataUpdatedAtMs = Date.now();
|
|
1263
|
+
await this.persistMetadata();
|
|
1146
1264
|
return metadata;
|
|
1147
1265
|
}
|
|
1266
|
+
async persistMetadata() {
|
|
1267
|
+
await writeMetadataCache(this.cachePath, {
|
|
1268
|
+
body: this.metadataBody,
|
|
1269
|
+
etag: this.metadataETag,
|
|
1270
|
+
savedAtMs: this.metadataUpdatedAtMs
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1148
1273
|
/** Gateway ids decide membership; online metadata decides how to call each model. */
|
|
1149
|
-
|
|
1150
|
-
const builtin = builtinModels(this.baseURL);
|
|
1151
|
-
const [listing, metadataResult] = await Promise.allSettled([
|
|
1152
|
-
fetchLiveModelIds(this.baseURL).then((ids) => ({ ids, updatedAtMs: Date.now() })),
|
|
1153
|
-
this.refreshMetadata(builtin)
|
|
1154
|
-
]);
|
|
1155
|
-
const metadata = metadataResult.status === "fulfilled" ? metadataResult.value : this.metadata;
|
|
1156
|
-
const metadataStatus = {
|
|
1157
|
-
metadataLive: metadataResult.status === "fulfilled",
|
|
1158
|
-
...this.metadataUpdatedAtMs === void 0 ? {} : { metadataUpdatedAtMs: this.metadataUpdatedAtMs },
|
|
1159
|
-
...metadataResult.status === "rejected" ? { metadataFailure: metadataResult.reason } : {}
|
|
1160
|
-
};
|
|
1274
|
+
build(builtin, listing, metadata, metadataStatus) {
|
|
1161
1275
|
const known = new Map([...builtin, ...this.served?.models ?? [], ...metadata?.models ?? []]);
|
|
1162
|
-
if (
|
|
1163
|
-
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 });
|
|
1164
1278
|
}
|
|
1165
1279
|
if (listing.status === "rejected") {
|
|
1166
1280
|
const models2 = this.served?.models ?? /* @__PURE__ */ new Map();
|
|
@@ -1188,7 +1302,9 @@ var OpencodeGoCatalog = class {
|
|
|
1188
1302
|
models.set(id, model);
|
|
1189
1303
|
}
|
|
1190
1304
|
}
|
|
1191
|
-
if (unavailable.size > 0
|
|
1305
|
+
if (unavailable.size > 0 && (metadataStatus.metadataLive || metadataStatus.metadataFailure !== void 0)) {
|
|
1306
|
+
this.onOmitted([...unavailable.keys()]);
|
|
1307
|
+
}
|
|
1192
1308
|
return {
|
|
1193
1309
|
details: new Map(listing.value.ids.map((id) => [id, metadata?.details.get(id) ?? {}])),
|
|
1194
1310
|
models,
|
|
@@ -1210,7 +1326,9 @@ var OpencodeGoCatalog = class {
|
|
|
1210
1326
|
return cached;
|
|
1211
1327
|
}
|
|
1212
1328
|
let snapshot = await this.snapshot(false, signal);
|
|
1213
|
-
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
|
+
}
|
|
1214
1332
|
if (signal?.aborted) throw new LlmError5("opencode-go catalog request aborted by caller", "ABORTED", { cause: signal.reason });
|
|
1215
1333
|
if (snapshot.unavailable.has(id)) {
|
|
1216
1334
|
throw new LlmError5(
|
|
@@ -1336,7 +1454,7 @@ function withModelLimit(model, limits) {
|
|
|
1336
1454
|
};
|
|
1337
1455
|
}
|
|
1338
1456
|
function opencodeSessionValue(sessionId) {
|
|
1339
|
-
return sessionId !== void 0 && sessionId.length > 0 ? sessionId :
|
|
1457
|
+
return sessionId !== void 0 && sessionId.length > 0 ? sessionId : randomUUID2();
|
|
1340
1458
|
}
|
|
1341
1459
|
var OpencodeGoAdapter = class extends LlmAdapter {
|
|
1342
1460
|
constructor(options) {
|
|
@@ -1360,19 +1478,20 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1360
1478
|
catalogOf(config) {
|
|
1361
1479
|
const key = `${config.baseURL}|${String(config.refreshMinutes)}`;
|
|
1362
1480
|
if (this.catalogCache?.key !== key) {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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 };
|
|
1376
1495
|
}
|
|
1377
1496
|
return this.catalogCache.catalog;
|
|
1378
1497
|
}
|
|
@@ -1548,7 +1667,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1548
1667
|
};
|
|
1549
1668
|
|
|
1550
1669
|
// src/usage.ts
|
|
1551
|
-
import { randomUUID as
|
|
1670
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1552
1671
|
import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
1553
1672
|
import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
|
|
1554
1673
|
|
|
@@ -1620,7 +1739,7 @@ var GoUsageService = class extends TypertRemoteService {
|
|
|
1620
1739
|
});
|
|
1621
1740
|
}
|
|
1622
1741
|
if (this.identity?.baseURL !== baseURL || this.identity.key !== key) {
|
|
1623
|
-
this.identity = { baseURL, key, source:
|
|
1742
|
+
this.identity = { baseURL, key, source: randomUUID3() };
|
|
1624
1743
|
}
|
|
1625
1744
|
const { source } = this.identity;
|
|
1626
1745
|
const endpoint = diagnosticURL(`${baseURL}/usage`);
|
|
@@ -1726,6 +1845,7 @@ function apply(ctx, raw) {
|
|
|
1726
1845
|
ctx.logger.warn(`llm-opencode-go: gateway models awaiting usable online metadata: ${ids.join(", ")}`);
|
|
1727
1846
|
}
|
|
1728
1847
|
};
|
|
1848
|
+
let registration;
|
|
1729
1849
|
const adapter = new OpencodeGoAdapter({
|
|
1730
1850
|
config: () => current(),
|
|
1731
1851
|
resolveApiKey,
|
|
@@ -1739,11 +1859,13 @@ function apply(ctx, raw) {
|
|
|
1739
1859
|
},
|
|
1740
1860
|
onFallback: logger.fallback,
|
|
1741
1861
|
onOmitted: logger.omitted,
|
|
1862
|
+
onCatalogRefresh: () => {
|
|
1863
|
+
registration?.replace([PROVIDER_ID]);
|
|
1864
|
+
},
|
|
1742
1865
|
onReplayDegrade: (reason) => {
|
|
1743
1866
|
ctx.logger.warn(`llm-opencode-go: unusable replay state on assistant history; sending provider-neutral content (${reason})`);
|
|
1744
1867
|
}
|
|
1745
1868
|
});
|
|
1746
|
-
let registration;
|
|
1747
1869
|
ctx.plugin(GoModelsService, {
|
|
1748
1870
|
catalog: () => adapter.catalogOf(current()),
|
|
1749
1871
|
onRefresh: () => {
|
|
@@ -1796,6 +1918,7 @@ function apply(ctx, raw) {
|
|
|
1796
1918
|
});
|
|
1797
1919
|
ctx.effect(() => () => {
|
|
1798
1920
|
registration?.();
|
|
1921
|
+
registration = void 0;
|
|
1799
1922
|
undiscover();
|
|
1800
1923
|
});
|
|
1801
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;
|
|
@@ -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. */
|
|
@@ -7,9 +7,24 @@ export interface GoModel {
|
|
|
7
7
|
maxTokens?: number;
|
|
8
8
|
deprecated?: boolean;
|
|
9
9
|
releaseDate?: string;
|
|
10
|
+
/** Input modalities models.dev declares, in {@link INPUT_MODALITIES} order. */
|
|
11
|
+
inputModalities?: readonly InputModality[];
|
|
10
12
|
/** Advertised by the gateway but lacking a usable protocol and capability configuration. */
|
|
11
13
|
configurationMissing?: boolean;
|
|
12
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Input modalities this page can name. models.dev uses these same five tokens, so
|
|
17
|
+
* an unknown token is dropped rather than rendered as an untranslatable chip.
|
|
18
|
+
*/
|
|
19
|
+
export declare const INPUT_MODALITIES: readonly ["text", "image", "audio", "video", "pdf"];
|
|
20
|
+
export type InputModality = typeof INPUT_MODALITIES[number];
|
|
21
|
+
/**
|
|
22
|
+
* Normalize one `modalities.input` array into display order.
|
|
23
|
+
* @param value - the raw declaration read from models.dev or from a Host response.
|
|
24
|
+
* @returns the declared modalities in {@link INPUT_MODALITIES} order, or
|
|
25
|
+
* `undefined` when nothing recognizable was declared.
|
|
26
|
+
*/
|
|
27
|
+
export declare function normalizeInputModalities(value: unknown): readonly InputModality[] | undefined;
|
|
13
28
|
/** Last successful check of one catalog source, plus any current refresh failure. */
|
|
14
29
|
export interface GoCatalogSourceStatus {
|
|
15
30
|
readonly updatedAt?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-opencode-go",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "OpenCode Go model provider and settings UI for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,13 +51,18 @@
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
|
-
"
|
|
54
|
+
"compile": "node scripts/build.mjs",
|
|
55
|
+
"check:dist": "node scripts/build.mjs --check",
|
|
55
56
|
"typecheck": "tsc -p tsconfig.host.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
56
57
|
"test": "vitest run",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
58
|
+
"pretest": "npm run compile",
|
|
59
|
+
"test:ci": "npm run check:dist && vitest run",
|
|
59
60
|
"verify:installed": "node --expose-internals scripts/installed-smoke.mjs",
|
|
60
|
-
"verify:headless": "node scripts/headless-smoke.mjs"
|
|
61
|
+
"verify:headless": "node scripts/headless-smoke.mjs",
|
|
62
|
+
"prepublishOnly": "npm run check:dist",
|
|
63
|
+
"test:compat": "node scripts/test-compat.mjs",
|
|
64
|
+
"test:install": "node scripts/test-install.mjs",
|
|
65
|
+
"verify": "npm run test:ci && npm run test:compat && npm run test:install"
|
|
61
66
|
},
|
|
62
67
|
"peerDependencies": {
|
|
63
68
|
"@deepseek-ai/cordis": ">=4.0.2",
|
|
@@ -90,9 +95,12 @@
|
|
|
90
95
|
"@deepseek-ai/cordis": "4.0.2",
|
|
91
96
|
"@deepseek-ai/cordis-plugin-include": "1.0.7",
|
|
92
97
|
"@deepseek-ai/cordis-plugin-loader": "1.0.3",
|
|
98
|
+
"@deepseek-ai/dsh-agent": "0.1.6-alpha.1",
|
|
99
|
+
"@deepseek-ai/dsh-agent-presets": "0.1.6-alpha.1",
|
|
93
100
|
"@deepseek-ai/dsh-api-gateway": "0.1.6-alpha.1",
|
|
94
101
|
"@deepseek-ai/dsh-api-remotes": "0.1.6-alpha.1",
|
|
95
102
|
"@deepseek-ai/dsh-api-settings-controller": "0.1.6-alpha.1",
|
|
103
|
+
"@deepseek-ai/dsh-atomic-write": "0.1.6-alpha.1",
|
|
96
104
|
"@deepseek-ai/dsh-attachment": "0.1.6-alpha.1",
|
|
97
105
|
"@deepseek-ai/dsh-attachment-local": "0.1.6-alpha.1",
|
|
98
106
|
"@deepseek-ai/dsh-brand": "0.1.6-alpha.1",
|
|
@@ -106,16 +114,29 @@
|
|
|
106
114
|
"@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.1",
|
|
107
115
|
"@deepseek-ai/dsh-credentials": "0.1.6-alpha.1",
|
|
108
116
|
"@deepseek-ai/dsh-credentials-local": "0.1.6-alpha.1",
|
|
117
|
+
"@deepseek-ai/dsh-deque": "0.1.6-alpha.1",
|
|
109
118
|
"@deepseek-ai/dsh-fs": "0.1.6-alpha.1",
|
|
110
119
|
"@deepseek-ai/dsh-home-paths": "0.1.6-alpha.1",
|
|
120
|
+
"@deepseek-ai/dsh-invariants": "0.1.6-alpha.1",
|
|
111
121
|
"@deepseek-ai/dsh-launch-environment": "0.1.6-alpha.1",
|
|
112
122
|
"@deepseek-ai/dsh-llm": "0.1.6-alpha.1",
|
|
123
|
+
"@deepseek-ai/dsh-native-command": "0.1.6-alpha.1",
|
|
124
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.1.6-alpha.1",
|
|
125
|
+
"@deepseek-ai/dsh-sandbox": "0.1.6-alpha.1",
|
|
126
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.1.6-alpha.1",
|
|
127
|
+
"@deepseek-ai/dsh-scope": "0.1.6-alpha.1",
|
|
113
128
|
"@deepseek-ai/dsh-session": "0.1.6-alpha.1",
|
|
129
|
+
"@deepseek-ai/dsh-session-projection": "0.1.6-alpha.1",
|
|
114
130
|
"@deepseek-ai/dsh-settings": "0.1.6-alpha.1",
|
|
115
131
|
"@deepseek-ai/dsh-settings-file": "0.1.6-alpha.1",
|
|
132
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.6-alpha.1",
|
|
116
133
|
"@deepseek-ai/dsh-timeout": "0.1.6-alpha.1",
|
|
134
|
+
"@deepseek-ai/dsh-tools": "0.1.6-alpha.1",
|
|
117
135
|
"@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.1",
|
|
118
136
|
"@deepseek-ai/dsh-typert-registry": "0.1.6-alpha.1",
|
|
137
|
+
"@deepseek-ai/dsh-user-approval": "0.1.6-alpha.1",
|
|
138
|
+
"@deepseek-ai/dsh-util-crypto": "0.1.6-alpha.1",
|
|
139
|
+
"@deepseek-ai/dsh-util-values": "0.1.6-alpha.1",
|
|
119
140
|
"@shikijs/langs": "^4.3.1",
|
|
120
141
|
"@testing-library/dom": "^10.4.2",
|
|
121
142
|
"@testing-library/react": "^16.3.2",
|
|
@@ -127,15 +148,6 @@
|
|
|
127
148
|
"anser": "^2.3.5",
|
|
128
149
|
"clsx": "^2.0.0",
|
|
129
150
|
"diff": "^9.0.0",
|
|
130
|
-
"dsh-attachment-local-v015-rc1": "npm:@deepseek-ai/dsh-attachment-local@0.1.5-rc.1",
|
|
131
|
-
"dsh-attachment-local-v015-rc2": "npm:@deepseek-ai/dsh-attachment-local@0.1.5-rc.2",
|
|
132
|
-
"dsh-attachment-local-v016-alpha2": "npm:@deepseek-ai/dsh-attachment-local@0.1.6-alpha.2",
|
|
133
|
-
"dsh-attachment-v015-rc1": "npm:@deepseek-ai/dsh-attachment@0.1.5-rc.1",
|
|
134
|
-
"dsh-attachment-v015-rc2": "npm:@deepseek-ai/dsh-attachment@0.1.5-rc.2",
|
|
135
|
-
"dsh-attachment-v016-alpha2": "npm:@deepseek-ai/dsh-attachment@0.1.6-alpha.2",
|
|
136
|
-
"dsh-llm-v015-rc1": "npm:@deepseek-ai/dsh-llm@0.1.5-rc.1",
|
|
137
|
-
"dsh-llm-v015-rc2": "npm:@deepseek-ai/dsh-llm@0.1.5-rc.2",
|
|
138
|
-
"dsh-llm-v016-alpha2": "npm:@deepseek-ai/dsh-llm@0.1.6-alpha.2",
|
|
139
151
|
"esbuild": "^0.25.0",
|
|
140
152
|
"immer": "^10.1.1",
|
|
141
153
|
"jsdom": "29.1.1",
|
|
@@ -208,8 +220,5 @@
|
|
|
208
220
|
"opencode-go",
|
|
209
221
|
"llm",
|
|
210
222
|
"cordis"
|
|
211
|
-
],
|
|
212
|
-
"workspaces": [
|
|
213
|
-
"tests/hosts/*"
|
|
214
223
|
]
|
|
215
224
|
}
|