dsh-opencode-go 0.1.9 → 0.1.11

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
@@ -706,6 +706,11 @@ import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.l
706
706
  import { attributionHeaders, LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
707
707
 
708
708
  // src/models-contract.ts
709
+ function isModelEnabled(model, modelVisibility) {
710
+ if (model.configurationMissing) return false;
711
+ const enabled = modelVisibility && Object.hasOwn(modelVisibility, model.id) ? modelVisibility[model.id] : void 0;
712
+ return typeof enabled === "boolean" ? enabled : !model.deprecated;
713
+ }
709
714
  function validReleaseDate(value) {
710
715
  return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
711
716
  }
@@ -730,15 +735,28 @@ function parseGoModels(value) {
730
735
  if (typeof row[key] === "number" && Number.isSafeInteger(row[key]) && row[key] > 0) model[key] = row[key];
731
736
  }
732
737
  if (typeof row.deprecated === "boolean") model.deprecated = row.deprecated;
738
+ if (typeof row.configurationMissing === "boolean") model.configurationMissing = row.configurationMissing;
733
739
  if (validReleaseDate(row.releaseDate)) model.releaseDate = row.releaseDate;
734
740
  return model;
735
741
  });
736
742
  }
743
+ function parseGoModelCatalog(value) {
744
+ if (!value || typeof value !== "object") throw new Error("Invalid OpenCode Go model catalog");
745
+ const catalog = value;
746
+ if (typeof catalog.stale !== "boolean" || catalog.error !== void 0 && typeof catalog.error !== "string") {
747
+ throw new Error("Invalid OpenCode Go model catalog status");
748
+ }
749
+ return {
750
+ models: parseGoModels(catalog.models),
751
+ stale: catalog.stale,
752
+ ...catalog.error === void 0 ? {} : { error: catalog.error }
753
+ };
754
+ }
737
755
  var codec = {
738
756
  mode: "strict",
739
- typeSymbol: "dsh-opencode-go#GoModels",
740
- schema: { parse: parseGoModels },
741
- create: () => ({ parse: parseGoModels })
757
+ typeSymbol: "dsh-opencode-go#GoModelCatalog",
758
+ schema: { parse: parseGoModelCatalog },
759
+ create: () => ({ parse: parseGoModelCatalog })
742
760
  };
743
761
  var modelsRemote = {
744
762
  package: "dsh-opencode-go",
@@ -858,11 +876,105 @@ function readModelMetadata(body, baseURL, builtin) {
858
876
  return { models, errors, details };
859
877
  }
860
878
 
879
+ // src/json-response.ts
880
+ import { setTimeout as delay } from "node:timers/promises";
881
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "EPIPE", "UND_ERR_SOCKET", "EAI_AGAIN"]);
882
+ function errorCauses(error) {
883
+ const causes = [];
884
+ for (let depth = 0; depth < 5 && error !== null && typeof error === "object"; depth += 1) {
885
+ causes.push(error);
886
+ error = "cause" in error ? error.cause : void 0;
887
+ }
888
+ return causes;
889
+ }
890
+ function retryableTransportFailure(error) {
891
+ if (error instanceof SyntaxError || error instanceof RangeError) return false;
892
+ for (const cause of errorCauses(error)) {
893
+ if (cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError")) return false;
894
+ if ("code" in cause && typeof cause.code === "string") return RETRYABLE_CODES.has(cause.code);
895
+ }
896
+ return false;
897
+ }
898
+ function transportFailure(error) {
899
+ const causes = errorCauses(error);
900
+ if (causes.some((cause) => cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError"))) {
901
+ return "request timed out or was aborted";
902
+ }
903
+ for (const cause of causes) {
904
+ if ("code" in cause && typeof cause.code === "string" && /^[A-Z][A-Z0-9_]+$/.test(cause.code)) {
905
+ return `network error (${cause.code})`;
906
+ }
907
+ }
908
+ return "network request failed";
909
+ }
910
+ function diagnosticURL(raw) {
911
+ const url = new URL(raw);
912
+ url.username = "";
913
+ url.password = "";
914
+ url.search = "";
915
+ url.hash = "";
916
+ return url.href;
917
+ }
918
+ async function fetchJsonResponse(url, init, maxBytes) {
919
+ if ((init.method ?? "GET").toUpperCase() !== "GET") throw new TypeError("JSON requests must use GET");
920
+ const headers = new Headers(init.headers);
921
+ headers.set("accept-encoding", "identity");
922
+ const options = { ...init, method: "GET", headers };
923
+ for (let attempt = 0; ; attempt += 1) {
924
+ init.signal?.throwIfAborted();
925
+ try {
926
+ const response = await fetch(url, options);
927
+ if (!response.ok) {
928
+ await response.body?.cancel().catch(() => {
929
+ });
930
+ return { response, body: void 0 };
931
+ }
932
+ return { response, body: await readJsonResponse(response, maxBytes) };
933
+ } catch (error) {
934
+ init.signal?.throwIfAborted();
935
+ if (attempt > 0 || !retryableTransportFailure(error)) throw error;
936
+ await delay(150, void 0, { signal: init.signal ?? void 0 });
937
+ }
938
+ }
939
+ }
940
+ async function readBoundedBody(response, maxBytes) {
941
+ if (response.body === null) return Buffer.alloc(0);
942
+ const reader = response.body.getReader();
943
+ const chunks = [];
944
+ let size = 0;
945
+ try {
946
+ while (true) {
947
+ const { done, value } = await reader.read();
948
+ if (done) return Buffer.concat(chunks, size);
949
+ if (value.byteLength > maxBytes - size) {
950
+ const error = new RangeError(`Response body exceeds ${maxBytes} byte limit`);
951
+ await reader.cancel(error).catch(() => {
952
+ });
953
+ throw error;
954
+ }
955
+ size += value.byteLength;
956
+ chunks.push(value);
957
+ }
958
+ } finally {
959
+ reader.releaseLock();
960
+ }
961
+ }
962
+ async function readJsonResponse(response, maxBytes) {
963
+ const bytes = await readBoundedBody(response, maxBytes);
964
+ try {
965
+ return JSON.parse(new TextDecoder().decode(bytes));
966
+ } catch (error) {
967
+ throw new SyntaxError("Response is not valid JSON", { cause: error });
968
+ }
969
+ }
970
+
861
971
  // src/catalog.ts
862
972
  var PROVIDER_ID = "opencode-go";
863
973
  var DISPLAY_NAME = "OpenCode Go";
864
974
  var DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
865
975
  var MODELS_FETCH_TIMEOUT_MS = 1e4;
976
+ var MODEL_LISTING_MAX_BYTES = 1024 * 1024;
977
+ var MODEL_METADATA_MAX_BYTES = 16 * 1024 * 1024;
866
978
  function builtinModels(baseURL) {
867
979
  return new Map(getBuiltinModels("opencode-go").map((model) => [model.id, {
868
980
  ...model,
@@ -882,19 +994,27 @@ function readLiveModelIds(body) {
882
994
  }
883
995
  async function fetchLiveModelIds(baseURL) {
884
996
  const url = `${baseURL.replace(/\/+$/, "")}/models`;
885
- let response;
997
+ const endpoint = diagnosticURL(url);
998
+ let result;
886
999
  try {
887
- response = await fetch(url, {
1000
+ result = await fetchJsonResponse(url, {
888
1001
  method: "GET",
889
1002
  cache: "no-cache",
890
- headers: { accept: "application/json", ...attributionHeaders() },
1003
+ headers: { ...attributionHeaders(), accept: "application/json" },
891
1004
  signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
892
- });
1005
+ }, MODEL_LISTING_MAX_BYTES);
1006
+ } catch (error) {
1007
+ const detail = error instanceof SyntaxError ? "invalid JSON response" : error instanceof RangeError ? `response exceeds the ${MODEL_LISTING_MAX_BYTES} byte limit` : transportFailure(error);
1008
+ const action = error instanceof SyntaxError || error instanceof RangeError ? "read" : "reach";
1009
+ throw new LlmError5(`could not ${action} ${endpoint}: ${detail}`, "DISCOVERY_FAILED", { cause: error });
1010
+ }
1011
+ const { response, body } = result;
1012
+ if (!response.ok) throw new LlmError5(`${endpoint} answered HTTP ${response.status}`, "DISCOVERY_FAILED");
1013
+ try {
1014
+ return readLiveModelIds(body);
893
1015
  } catch (error) {
894
- throw new LlmError5(`could not reach ${url}`, "DISCOVERY_FAILED", { cause: error });
1016
+ throw new LlmError5(`${endpoint} returned an invalid model listing: expected a "data" array`, "DISCOVERY_FAILED", { cause: error });
895
1017
  }
896
- if (!response.ok) throw new LlmError5(`${url} answered ${response.status}`, "DISCOVERY_FAILED");
897
- return readLiveModelIds(await response.json());
898
1018
  }
899
1019
  function harnessApiKeyAuth() {
900
1020
  return { apiKey: {
@@ -941,18 +1061,18 @@ var OpencodeGoCatalog = class {
941
1061
  }
942
1062
  /** Conditional HTTP requests save bandwidth while still checking for updated metadata. */
943
1063
  async refreshMetadata(builtin) {
944
- const response = await fetch(MODEL_METADATA_URL, {
1064
+ const { response, body } = await fetchJsonResponse(MODEL_METADATA_URL, {
945
1065
  headers: {
946
- accept: "application/json",
947
1066
  ...attributionHeaders(),
1067
+ accept: "application/json",
948
1068
  ...this.metadataETag === void 0 ? {} : { "if-none-match": this.metadataETag }
949
1069
  },
950
1070
  cache: "no-cache",
951
1071
  signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
952
- });
1072
+ }, MODEL_METADATA_MAX_BYTES);
953
1073
  if (response.status === 304 && this.metadata !== void 0) return this.metadata;
954
1074
  if (!response.ok) throw new Error(`models.dev answered ${response.status}`);
955
- const metadata = readModelMetadata(await response.json(), this.baseURL, builtin);
1075
+ const metadata = readModelMetadata(body, this.baseURL, builtin);
956
1076
  this.metadata = metadata;
957
1077
  this.metadataETag = response.headers.get("etag") ?? void 0;
958
1078
  return metadata;
@@ -978,7 +1098,8 @@ var OpencodeGoCatalog = class {
978
1098
  unavailable: this.served?.unavailable ?? /* @__PURE__ */ new Map(),
979
1099
  provider: buildProvider(this.baseURL, [...models2.values()]),
980
1100
  live: false,
981
- fetchedAtMs: Date.now()
1101
+ fetchedAtMs: Date.now(),
1102
+ listingFailure: listing.reason
982
1103
  };
983
1104
  }
984
1105
  const models = /* @__PURE__ */ new Map();
@@ -987,7 +1108,7 @@ var OpencodeGoCatalog = class {
987
1108
  const error = metadata?.errors.get(id);
988
1109
  const model = known.get(id);
989
1110
  if (error !== void 0 || model === void 0) {
990
- unavailable.set(id, error ?? "model metadata has not been published on models.dev yet");
1111
+ unavailable.set(id, error ?? "no usable configuration was found for this OpenCode Go model");
991
1112
  } else {
992
1113
  models.set(id, model);
993
1114
  }
@@ -1018,26 +1139,41 @@ var OpencodeGoCatalog = class {
1018
1139
  };
1019
1140
  async function discoverCatalogModels(catalog) {
1020
1141
  const snapshot = await catalog.snapshot(true);
1021
- if (!snapshot.live) {
1022
- throw new LlmError5("llm-opencode-go: the live model listing is unreachable; try again later", "DISCOVERY_FAILED");
1023
- }
1142
+ requireLiveListing(snapshot);
1024
1143
  return describeCatalog(snapshot);
1025
1144
  }
1145
+ function requireLiveListing(snapshot) {
1146
+ if (snapshot.live) return;
1147
+ throw listingError(snapshot);
1148
+ }
1149
+ function listingError(snapshot) {
1150
+ const detail = snapshot.listingFailure instanceof LlmError5 ? snapshot.listingFailure.message : "the live model listing is unreachable";
1151
+ return new LlmError5(`llm-opencode-go: ${detail}; refresh the model list to retry`, "DISCOVERY_FAILED", { cause: snapshot.listingFailure });
1152
+ }
1153
+ function describeConfiguredModels(snapshot) {
1154
+ return [...snapshot.models.values()].map((model) => ({
1155
+ id: model.id,
1156
+ name: model.name,
1157
+ contextWindow: model.contextWindow,
1158
+ maxTokens: model.maxTokens
1159
+ }));
1160
+ }
1026
1161
  function describeCatalog(snapshot) {
1027
1162
  return [
1028
- ...[...snapshot.models.values()].map((model) => ({
1029
- id: model.id,
1030
- name: model.name,
1031
- contextWindow: model.contextWindow,
1032
- maxTokens: model.maxTokens
1033
- })),
1163
+ ...describeConfiguredModels(snapshot),
1034
1164
  ...[...snapshot.unavailable].map(([id, reason]) => ({ id, name: `${id} (metadata unavailable: ${reason})` }))
1035
1165
  ];
1036
1166
  }
1037
1167
  async function discoverSettingsModels(catalog) {
1038
1168
  const snapshot = await catalog.snapshot(true);
1039
- if (!snapshot.live) throw new LlmError5("llm-opencode-go: the live model listing is unreachable; try again later", "DISCOVERY_FAILED");
1040
- return sortModels(describeCatalog(snapshot).map((model) => ({ ...model, ...snapshot.details.get(model.id) })));
1169
+ return {
1170
+ models: sortModels([
1171
+ ...describeConfiguredModels(snapshot),
1172
+ ...[...snapshot.unavailable.keys()].map((id) => ({ id, name: id, configurationMissing: true }))
1173
+ ].map((model) => ({ ...model, ...snapshot.details.get(model.id) }))),
1174
+ stale: !snapshot.live,
1175
+ ...snapshot.live ? {} : { error: listingError(snapshot).message }
1176
+ };
1041
1177
  }
1042
1178
 
1043
1179
  // src/config.ts
@@ -1048,7 +1184,7 @@ var DEFAULT_REFRESH_MINUTES = 60;
1048
1184
  var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
1049
1185
  var fields = {
1050
1186
  enabled: z.boolean().default(true),
1051
- showDeprecatedModels: z.boolean().default(false),
1187
+ modelVisibility: z.dict(z.boolean().required()).default({}),
1052
1188
  apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
1053
1189
  baseURL: z.string().default(DEFAULT_BASE_URL),
1054
1190
  refreshMinutes: z.number().step(1).min(1).max(7 * 24 * 60).default(DEFAULT_REFRESH_MINUTES),
@@ -1069,7 +1205,7 @@ var Config = z.object(Object.fromEntries(
1069
1205
  Object.entries(fields).map(([key, schema]) => [key, schema.volatile()])
1070
1206
  ));
1071
1207
  function readConfig(config) {
1072
- return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, value.get()]));
1208
+ return Object.fromEntries(Object.keys(fields).map((key) => [key, config[key].get()]));
1073
1209
  }
1074
1210
  function assertBaseURL(raw) {
1075
1211
  let url;
@@ -1088,6 +1224,7 @@ function assertBaseURL(raw) {
1088
1224
  }
1089
1225
 
1090
1226
  // src/adapter.ts
1227
+ var DISABLES_THINKING_WHEN_UNSET = /* @__PURE__ */ new Set(["deepseek", "zai", "qwen", "qwen-chat-template"]);
1091
1228
  function withModelLimit(model, limits) {
1092
1229
  const limit = limits[model.id];
1093
1230
  if (limit == null) return model;
@@ -1143,8 +1280,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1143
1280
  }
1144
1281
  async listModels(_provider) {
1145
1282
  const config = this.options.config();
1146
- const snapshot = await this.catalogOf(config).snapshot(true);
1147
- return [...snapshot.models.values()].filter((model) => config.showDeprecatedModels || !snapshot.details.get(model.id)?.deprecated).map((model) => ({
1283
+ const snapshot = await this.catalogOf(config).snapshot();
1284
+ return [...snapshot.models.values()].filter((model) => isModelEnabled({ id: model.id, ...snapshot.details.get(model.id) }, config.modelVisibility)).map((model) => ({
1148
1285
  provider: PROVIDER_ID,
1149
1286
  id: model.id,
1150
1287
  name: model.name,
@@ -1165,11 +1302,14 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1165
1302
  const reasoning = {};
1166
1303
  const levels = model.reasoning ? getSupportedThinkingLevels(model) : [];
1167
1304
  if (levels.length > 0) {
1305
+ const format = model.compat?.thinkingFormat;
1306
+ const fallback = format !== void 0 && DISABLES_THINKING_WHEN_UNSET.has(format) ? levels.includes("high") ? "high" : levels.findLast((level) => level !== "off") : void 0;
1168
1307
  reasoning.reasoning = {
1169
1308
  efforts: levels.map((level) => ({
1170
1309
  id: ReasoningEffortId(level),
1171
1310
  name: `${level.charAt(0).toUpperCase()}${level.slice(1)}`
1172
- }))
1311
+ })),
1312
+ ...fallback === void 0 ? {} : { defaultEffort: ReasoningEffortId(fallback) }
1173
1313
  };
1174
1314
  }
1175
1315
  return {
@@ -1296,7 +1436,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1296
1436
  };
1297
1437
 
1298
1438
  // src/usage.ts
1299
- import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
1439
+ import { randomUUID as randomUUID2 } from "node:crypto";
1440
+ import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
1300
1441
  import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
1301
1442
 
1302
1443
  // src/usage-contract.ts
@@ -1304,6 +1445,12 @@ function parseGoUsage(value) {
1304
1445
  if (!value || typeof value !== "object") throw new Error("Invalid OpenCode Go usage response");
1305
1446
  const source = value;
1306
1447
  const result = {};
1448
+ if (source.source !== void 0) {
1449
+ if (typeof source.source !== "string" || source.source.length === 0 || source.source.length > 128) {
1450
+ throw new Error("Invalid OpenCode Go usage source");
1451
+ }
1452
+ result.source = source.source;
1453
+ }
1307
1454
  for (const key of ["rolling", "weekly", "monthly"]) {
1308
1455
  const row = source[key];
1309
1456
  if (!row || row.status !== "ok" && row.status !== "rate-limited" || typeof row.percent !== "number" || !Number.isFinite(row.percent) || row.percent < 0 || typeof row.resetsAt !== "string" || !Number.isFinite(Date.parse(row.resetsAt))) {
@@ -1333,23 +1480,73 @@ var usageRemote = {
1333
1480
  };
1334
1481
 
1335
1482
  // src/usage.ts
1483
+ var USAGE_MAX_BYTES = 1024 * 1024;
1336
1484
  var GoUsageService = class extends TypertRemoteService {
1337
1485
  constructor(ctx, options) {
1338
1486
  super(ctx, "opencodeGoUsage");
1339
1487
  this.options = options;
1340
1488
  }
1489
+ identity;
1341
1490
  async read() {
1342
1491
  const baseURL = assertBaseURL(this.options.baseURL()).replace(/\/$/, "");
1343
- const key = await this.options.resolveApiKey();
1344
- if (!key) throw new Error("OpenCode Go API key is not configured");
1345
- const response = await fetch(`${baseURL}/usage`, {
1346
- headers: { ...attributionHeaders3(), Authorization: `Bearer ${key}`, Accept: "application/json" },
1347
- signal: AbortSignal.timeout(1e4),
1348
- redirect: "error"
1349
- });
1350
- if (!response.ok) throw new Error(`OpenCode Go usage unavailable (HTTP ${response.status})`);
1351
- const body = await response.json();
1352
- return parseGoUsage(body && typeof body === "object" ? body.usage : void 0);
1492
+ let key;
1493
+ try {
1494
+ key = await this.options.resolveApiKey();
1495
+ } catch (error) {
1496
+ this.identity = void 0;
1497
+ const missing = error instanceof Error && "code" in error && error.code === "MISSING_CREDENTIAL";
1498
+ throw new RemoteError("opencode-go/usage-unavailable", missing ? "OpenCode Go API key is not configured" : "Could not resolve the OpenCode Go API key", {
1499
+ retryable: !missing,
1500
+ retainPrevious: false
1501
+ }, { cause: error });
1502
+ }
1503
+ if (!key) {
1504
+ this.identity = void 0;
1505
+ throw new RemoteError("opencode-go/usage-unavailable", "OpenCode Go API key is not configured", {
1506
+ retryable: false,
1507
+ retainPrevious: false
1508
+ });
1509
+ }
1510
+ if (this.identity?.baseURL !== baseURL || this.identity.key !== key) {
1511
+ this.identity = { baseURL, key, source: randomUUID2() };
1512
+ }
1513
+ const { source } = this.identity;
1514
+ const endpoint = diagnosticURL(`${baseURL}/usage`);
1515
+ let result;
1516
+ try {
1517
+ result = await fetchJsonResponse(`${baseURL}/usage`, {
1518
+ headers: { ...attributionHeaders3(), Authorization: `Bearer ${key}`, Accept: "application/json" },
1519
+ signal: AbortSignal.timeout(1e4),
1520
+ redirect: "error"
1521
+ }, USAGE_MAX_BYTES);
1522
+ } catch (error) {
1523
+ const invalid = error instanceof SyntaxError || error instanceof RangeError;
1524
+ const detail = error instanceof SyntaxError ? "invalid JSON response" : error instanceof RangeError ? `response exceeds the ${USAGE_MAX_BYTES} byte limit` : transportFailure(error);
1525
+ throw new RemoteError("opencode-go/usage-unavailable", `Could not read ${endpoint}: ${detail}`, {
1526
+ retryable: true,
1527
+ retainPrevious: !invalid,
1528
+ source
1529
+ }, { cause: error });
1530
+ }
1531
+ const { response, body } = result;
1532
+ if (!response.ok) {
1533
+ const temporary = response.status === 408 || response.status === 429 || response.status >= 500;
1534
+ throw new RemoteError("opencode-go/usage-unavailable", `OpenCode Go usage unavailable (HTTP ${response.status})`, {
1535
+ retryable: temporary,
1536
+ retainPrevious: temporary,
1537
+ source
1538
+ });
1539
+ }
1540
+ try {
1541
+ const usage = parseGoUsage(body && typeof body === "object" ? body.usage : void 0);
1542
+ return { ...usage, source };
1543
+ } catch (error) {
1544
+ throw new RemoteError("opencode-go/usage-unavailable", "Invalid OpenCode Go usage response", {
1545
+ retryable: true,
1546
+ retainPrevious: false,
1547
+ source
1548
+ }, { cause: error });
1549
+ }
1353
1550
  }
1354
1551
  };
1355
1552
 
@@ -1360,8 +1557,12 @@ var GoModelsService = class extends TypertRemoteService2 {
1360
1557
  super(ctx, "opencodeGoModels");
1361
1558
  this.options = options;
1362
1559
  }
1363
- read() {
1364
- return discoverSettingsModels(this.options.catalog());
1560
+ async read() {
1561
+ try {
1562
+ return await discoverSettingsModels(this.options.catalog());
1563
+ } finally {
1564
+ this.options.onRefresh?.();
1565
+ }
1365
1566
  }
1366
1567
  };
1367
1568
 
@@ -1430,9 +1631,15 @@ function apply(ctx, raw) {
1430
1631
  ctx.logger.warn(`llm-opencode-go: unusable replay state on assistant history; sending provider-neutral content (${reason})`);
1431
1632
  }
1432
1633
  });
1433
- ctx.plugin(GoModelsService, { catalog: () => adapter.catalogOf(current()) });
1434
- let pickerVisibility = current().showDeprecatedModels;
1435
1634
  let registration;
1635
+ ctx.plugin(GoModelsService, {
1636
+ catalog: () => adapter.catalogOf(current()),
1637
+ onRefresh: () => {
1638
+ registration?.replace([PROVIDER_ID]);
1639
+ }
1640
+ });
1641
+ const pickerVisibilityOf = () => JSON.stringify(current().modelVisibility ?? {});
1642
+ let pickerVisibility = pickerVisibilityOf();
1436
1643
  const applyRoute = (configured) => {
1437
1644
  if (configured && current().enabled && registration === void 0) {
1438
1645
  try {
@@ -1449,8 +1656,9 @@ function apply(ctx, raw) {
1449
1656
  }
1450
1657
  };
1451
1658
  const syncRoute = () => {
1452
- if (pickerVisibility !== current().showDeprecatedModels) {
1453
- pickerVisibility = current().showDeprecatedModels;
1659
+ const visibility = pickerVisibilityOf();
1660
+ if (pickerVisibility !== visibility) {
1661
+ pickerVisibility = visibility;
1454
1662
  registration?.replace([PROVIDER_ID]);
1455
1663
  }
1456
1664
  const credentials = ctx.get("credentials");
@@ -1,6 +1,6 @@
1
1
  import type { Api, Model, Provider } from '@earendil-works/pi-ai';
2
2
  import type { LlmDiscoveredModel } from '@deepseek-ai/dsh-llm';
3
- import { type GoModel } from './models-contract.ts';
3
+ import { type GoModelCatalog } from './models-contract.ts';
4
4
  import type { ModelMetadata } from './model-metadata.ts';
5
5
  export declare const PROVIDER_ID = "opencode-go";
6
6
  export declare const DISPLAY_NAME = "OpenCode Go";
@@ -12,6 +12,8 @@ export interface CatalogSnapshot {
12
12
  readonly unavailable: ReadonlyMap<string, string>;
13
13
  readonly provider: Provider;
14
14
  readonly live: boolean;
15
+ /** Retained for explicit discovery; runtime callers may still use the last catalog. */
16
+ readonly listingFailure?: unknown;
15
17
  readonly fetchedAtMs: number;
16
18
  }
17
19
  /** A valid empty listing means the gateway serves nothing; malformed replies are failures. */
@@ -44,5 +46,5 @@ export declare class OpencodeGoCatalog {
44
46
  }
45
47
  /** Explicit discovery always revalidates both sources, including during the runtime TTL. */
46
48
  export declare function discoverCatalogModels(catalog: OpencodeGoCatalog): Promise<readonly LlmDiscoveredModel[]>;
47
- /** Settings retain deprecated gateway entries regardless of picker visibility. */
48
- export declare function discoverSettingsModels(catalog: OpencodeGoCatalog): Promise<readonly GoModel[]>;
49
+ /** Settings expose the same retained catalog as requests, with a failed-refresh diagnostic. */
50
+ export declare function discoverSettingsModels(catalog: OpencodeGoCatalog): Promise<GoModelCatalog>;
@@ -2,12 +2,16 @@ import type { OpencodeGoModelLimit, OpencodeGoModelLimits, OpencodeGoModels } fr
2
2
  import type { en } from './locales.ts';
3
3
  type Translate = (key: keyof typeof en, params?: Record<string, unknown>) => string;
4
4
  export declare function hasCapacityOverride(limit: OpencodeGoModelLimit | null | undefined): boolean;
5
- /** One gateway-backed list and its selected model's staged capacity settings. */
6
- export declare function ModelEditor({ models, draft, t, disabled, onEdit }: {
5
+ /** Per-model switches apply immediately; capacity edits stay in the staged form. */
6
+ export declare function ModelEditor({ models, draft, modelVisibility, t, locale, disabled, visibilitySaving, onEdit, onModelEnabled }: {
7
7
  models: OpencodeGoModels;
8
8
  draft: OpencodeGoModelLimits;
9
+ modelVisibility: Readonly<Record<string, boolean>>;
9
10
  t: Translate;
11
+ locale?: string;
10
12
  disabled: boolean;
13
+ visibilitySaving: boolean;
11
14
  onEdit: (next: OpencodeGoModelLimits) => void;
15
+ onModelEnabled: (id: string, enabled: boolean) => void;
12
16
  }): import("react").JSX.Element;
13
17
  export {};
@@ -15,6 +15,8 @@ type SectionTranslate = (key: keyof typeof en, params?: Record<string, unknown>)
15
15
  export interface OpencodeGoSectionInjected extends OpencodeGoSectionFace {
16
16
  /** Section copy. */
17
17
  t: SectionTranslate;
18
+ /** Read at render time because the Host caches injected values across locale changes. */
19
+ getLocale?: () => string;
18
20
  }
19
21
  /** Props delivered by the slot outlet: the inject face spread flat. */
20
22
  export type OpencodeGoSectionProps = Partial<InjectFace<OpencodeGoSectionInjected>>;
@@ -5,6 +5,7 @@ export interface UsagePillProps {
5
5
  directory: SnapshotStore<ModelDirectoryState>;
6
6
  readUsage: () => Promise<GoUsage>;
7
7
  t: (key: string) => string;
8
+ getLocale?: () => string;
8
9
  }
9
10
  /** Only the selected Go provider mounts a poller, so other models send no usage traffic. */
10
11
  export declare function UsagePill({ directory, ...props }: UsagePillProps): import("react").JSX.Element | null;
@@ -1,6 +1,7 @@
1
1
  /** Copy dictionaries for the OpenCode Go settings page. English is the key-set source of truth. */
2
2
  export declare const en: {
3
3
  usageTitle: string;
4
+ usageRollingShort: string;
4
5
  usageHint: string;
5
6
  usageWeekShort: string;
6
7
  usage_rolling: string;
@@ -10,6 +11,12 @@ export declare const en: {
10
11
  usageLimited: string;
11
12
  usageLoading: string;
12
13
  usageUnavailable: string;
14
+ usageStaleShort: string;
15
+ usageStaleHint: string;
16
+ usageRefreshFailed: string;
17
+ usageLastUpdated: string;
18
+ usageRetry: string;
19
+ usageRefreshing: string;
13
20
  nav: string;
14
21
  title: string;
15
22
  intro: string;
@@ -26,6 +33,8 @@ export declare const en: {
26
33
  modelsRefresh: string;
27
34
  modelsLoading: string;
28
35
  modelsFailed: string;
36
+ modelsStale: string;
37
+ modelsRefreshing: string;
29
38
  modelsEmpty: string;
30
39
  modelsCount: string;
31
40
  limitsLabel: string;
@@ -38,12 +47,15 @@ export declare const en: {
38
47
  limitsOutput: string;
39
48
  capacityDefault: string;
40
49
  capacityMissing: string;
41
- showDeprecatedLabel: string;
42
- showDeprecatedHint: string;
50
+ visibilityHint: string;
51
+ modelVisibleLabel: string;
52
+ configurationMissing: string;
53
+ configurationMissingHint: string;
43
54
  pickerFailed: string;
44
55
  deprecatedBadge: string;
45
56
  deprecatedHint: string;
46
57
  newHint: string;
58
+ newBadge: string;
47
59
  releasedOn: string;
48
60
  releaseSource: string;
49
61
  modelDetails: string;