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/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((resolve, reject) => {
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
- resolve(value);
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
- if (!force && this.served !== void 0 && Date.now() < this.refreshAtMs) {
1108
- return Promise.resolve(this.served);
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.pending ??= this.build().then((snapshot) => {
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.metadataETag = response.headers.get("etag") ?? void 0;
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
- async build() {
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 (metadataResult.status === "rejected") {
1163
- this.onFallback({ url: MODEL_METADATA_URL, error: metadataResult.reason, kept: known.size });
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) this.onOmitted([...unavailable.keys()]);
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) snapshot = await this.snapshot(true, signal);
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 : randomUUID();
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
- this.catalogCache = {
1364
- key,
1365
- catalog: new OpencodeGoCatalog(
1366
- assertBaseURL(config.baseURL),
1367
- config.refreshMinutes * 6e4,
1368
- /* v8 ignore next -- the plugin always passes both observers; the defaults exist for direct construction */
1369
- this.options.onFallback ?? (() => {
1370
- }),
1371
- /* v8 ignore next -- the plugin always passes both observers; the defaults exist for direct construction */
1372
- this.options.onOmitted ?? (() => {
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 randomUUID2 } from "node:crypto";
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: randomUUID2() };
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) => {
@@ -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,
@@ -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 section. It leads with the one value a user has to
3
- * supply — the API key, stored write-only through the credentials domain — and
4
- * the models the gateway currently serves, then keeps the credential
5
- * reference, the endpoint, and the adapter tuning fields in the
6
- * `llm-opencode-go` namespace behind a collapsed disclosure.
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, Pick<GoModel, 'deprecated' | 'releaseDate'>>;
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.15",
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
- "build": "node scripts/build.mjs",
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
- "prepack": "npm run build",
58
- "pretest": "npm run build",
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
  }