dsh-opencode-go 0.1.10 → 0.1.12

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",
@@ -859,13 +877,66 @@ function readModelMetadata(body, baseURL, builtin) {
859
877
  }
860
878
 
861
879
  // src/json-response.ts
862
- import { promisify } from "node:util";
863
- import { brotliDecompress, gunzip, inflate } from "node:zlib";
864
- var decoders = [
865
- { name: "Brotli", decode: promisify(brotliDecompress) },
866
- { name: "gzip", decode: promisify(gunzip) },
867
- { name: "deflate", decode: promisify(inflate) }
868
- ];
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
+ }
869
940
  async function readBoundedBody(response, maxBytes) {
870
941
  if (response.body === null) return Buffer.alloc(0);
871
942
  const reader = response.body.getReader();
@@ -890,24 +961,11 @@ async function readBoundedBody(response, maxBytes) {
890
961
  }
891
962
  async function readJsonResponse(response, maxBytes) {
892
963
  const bytes = await readBoundedBody(response, maxBytes);
893
- const parse = (data) => JSON.parse(new TextDecoder().decode(data));
894
- const errors = [];
895
964
  try {
896
- return parse(bytes);
965
+ return JSON.parse(new TextDecoder().decode(bytes));
897
966
  } catch (error) {
898
- errors.push(error);
899
- }
900
- for (const { name: name2, decode } of decoders) {
901
- try {
902
- return parse(await decode(bytes, { maxOutputLength: maxBytes }));
903
- } catch (error) {
904
- if (error instanceof Error && "code" in error && error.code === "ERR_BUFFER_TOO_LARGE") {
905
- throw new RangeError(`${name2} decoded response exceeds ${maxBytes} byte limit`, { cause: error });
906
- }
907
- errors.push(new Error(`${name2} response decoding or JSON parsing failed`, { cause: error }));
908
- }
967
+ throw new SyntaxError("Response is not valid JSON", { cause: error });
909
968
  }
910
- throw new AggregateError(errors, "Response is not valid JSON, including after Brotli, gzip, or deflate decoding");
911
969
  }
912
970
 
913
971
  // src/catalog.ts
@@ -936,19 +994,27 @@ function readLiveModelIds(body) {
936
994
  }
937
995
  async function fetchLiveModelIds(baseURL) {
938
996
  const url = `${baseURL.replace(/\/+$/, "")}/models`;
939
- let response;
997
+ const endpoint = diagnosticURL(url);
998
+ let result;
940
999
  try {
941
- response = await fetch(url, {
1000
+ result = await fetchJsonResponse(url, {
942
1001
  method: "GET",
943
1002
  cache: "no-cache",
944
- headers: { accept: "application/json", ...attributionHeaders() },
1003
+ headers: { ...attributionHeaders(), accept: "application/json" },
945
1004
  signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
946
- });
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);
947
1015
  } catch (error) {
948
- 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 });
949
1017
  }
950
- if (!response.ok) throw new LlmError5(`${url} answered ${response.status}`, "DISCOVERY_FAILED");
951
- return readLiveModelIds(await readJsonResponse(response, MODEL_LISTING_MAX_BYTES));
952
1018
  }
953
1019
  function harnessApiKeyAuth() {
954
1020
  return { apiKey: {
@@ -995,18 +1061,18 @@ var OpencodeGoCatalog = class {
995
1061
  }
996
1062
  /** Conditional HTTP requests save bandwidth while still checking for updated metadata. */
997
1063
  async refreshMetadata(builtin) {
998
- const response = await fetch(MODEL_METADATA_URL, {
1064
+ const { response, body } = await fetchJsonResponse(MODEL_METADATA_URL, {
999
1065
  headers: {
1000
- accept: "application/json",
1001
1066
  ...attributionHeaders(),
1067
+ accept: "application/json",
1002
1068
  ...this.metadataETag === void 0 ? {} : { "if-none-match": this.metadataETag }
1003
1069
  },
1004
1070
  cache: "no-cache",
1005
1071
  signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
1006
- });
1072
+ }, MODEL_METADATA_MAX_BYTES);
1007
1073
  if (response.status === 304 && this.metadata !== void 0) return this.metadata;
1008
1074
  if (!response.ok) throw new Error(`models.dev answered ${response.status}`);
1009
- const metadata = readModelMetadata(await readJsonResponse(response, MODEL_METADATA_MAX_BYTES), this.baseURL, builtin);
1075
+ const metadata = readModelMetadata(body, this.baseURL, builtin);
1010
1076
  this.metadata = metadata;
1011
1077
  this.metadataETag = response.headers.get("etag") ?? void 0;
1012
1078
  return metadata;
@@ -1032,7 +1098,8 @@ var OpencodeGoCatalog = class {
1032
1098
  unavailable: this.served?.unavailable ?? /* @__PURE__ */ new Map(),
1033
1099
  provider: buildProvider(this.baseURL, [...models2.values()]),
1034
1100
  live: false,
1035
- fetchedAtMs: Date.now()
1101
+ fetchedAtMs: Date.now(),
1102
+ listingFailure: listing.reason
1036
1103
  };
1037
1104
  }
1038
1105
  const models = /* @__PURE__ */ new Map();
@@ -1041,7 +1108,7 @@ var OpencodeGoCatalog = class {
1041
1108
  const error = metadata?.errors.get(id);
1042
1109
  const model = known.get(id);
1043
1110
  if (error !== void 0 || model === void 0) {
1044
- 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");
1045
1112
  } else {
1046
1113
  models.set(id, model);
1047
1114
  }
@@ -1072,26 +1139,41 @@ var OpencodeGoCatalog = class {
1072
1139
  };
1073
1140
  async function discoverCatalogModels(catalog) {
1074
1141
  const snapshot = await catalog.snapshot(true);
1075
- if (!snapshot.live) {
1076
- throw new LlmError5("llm-opencode-go: the live model listing is unreachable; try again later", "DISCOVERY_FAILED");
1077
- }
1142
+ requireLiveListing(snapshot);
1078
1143
  return describeCatalog(snapshot);
1079
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
+ }
1080
1161
  function describeCatalog(snapshot) {
1081
1162
  return [
1082
- ...[...snapshot.models.values()].map((model) => ({
1083
- id: model.id,
1084
- name: model.name,
1085
- contextWindow: model.contextWindow,
1086
- maxTokens: model.maxTokens
1087
- })),
1163
+ ...describeConfiguredModels(snapshot),
1088
1164
  ...[...snapshot.unavailable].map(([id, reason]) => ({ id, name: `${id} (metadata unavailable: ${reason})` }))
1089
1165
  ];
1090
1166
  }
1091
1167
  async function discoverSettingsModels(catalog) {
1092
1168
  const snapshot = await catalog.snapshot(true);
1093
- if (!snapshot.live) throw new LlmError5("llm-opencode-go: the live model listing is unreachable; try again later", "DISCOVERY_FAILED");
1094
- 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
+ };
1095
1177
  }
1096
1178
 
1097
1179
  // src/config.ts
@@ -1102,7 +1184,7 @@ var DEFAULT_REFRESH_MINUTES = 60;
1102
1184
  var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
1103
1185
  var fields = {
1104
1186
  enabled: z.boolean().default(true),
1105
- showDeprecatedModels: z.boolean().default(false),
1187
+ modelVisibility: z.dict(z.boolean().required()).default({}),
1106
1188
  apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
1107
1189
  baseURL: z.string().default(DEFAULT_BASE_URL),
1108
1190
  refreshMinutes: z.number().step(1).min(1).max(7 * 24 * 60).default(DEFAULT_REFRESH_MINUTES),
@@ -1123,7 +1205,7 @@ var Config = z.object(Object.fromEntries(
1123
1205
  Object.entries(fields).map(([key, schema]) => [key, schema.volatile()])
1124
1206
  ));
1125
1207
  function readConfig(config) {
1126
- 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()]));
1127
1209
  }
1128
1210
  function assertBaseURL(raw) {
1129
1211
  let url;
@@ -1198,8 +1280,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1198
1280
  }
1199
1281
  async listModels(_provider) {
1200
1282
  const config = this.options.config();
1201
- const snapshot = await this.catalogOf(config).snapshot(true);
1202
- 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) => ({
1203
1285
  provider: PROVIDER_ID,
1204
1286
  id: model.id,
1205
1287
  name: model.name,
@@ -1354,7 +1436,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1354
1436
  };
1355
1437
 
1356
1438
  // src/usage.ts
1357
- 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";
1358
1441
  import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
1359
1442
 
1360
1443
  // src/usage-contract.ts
@@ -1362,6 +1445,12 @@ function parseGoUsage(value) {
1362
1445
  if (!value || typeof value !== "object") throw new Error("Invalid OpenCode Go usage response");
1363
1446
  const source = value;
1364
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
+ }
1365
1454
  for (const key of ["rolling", "weekly", "monthly"]) {
1366
1455
  const row = source[key];
1367
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))) {
@@ -1391,23 +1480,73 @@ var usageRemote = {
1391
1480
  };
1392
1481
 
1393
1482
  // src/usage.ts
1483
+ var USAGE_MAX_BYTES = 1024 * 1024;
1394
1484
  var GoUsageService = class extends TypertRemoteService {
1395
1485
  constructor(ctx, options) {
1396
1486
  super(ctx, "opencodeGoUsage");
1397
1487
  this.options = options;
1398
1488
  }
1489
+ identity;
1399
1490
  async read() {
1400
1491
  const baseURL = assertBaseURL(this.options.baseURL()).replace(/\/$/, "");
1401
- const key = await this.options.resolveApiKey();
1402
- if (!key) throw new Error("OpenCode Go API key is not configured");
1403
- const response = await fetch(`${baseURL}/usage`, {
1404
- headers: { ...attributionHeaders3(), Authorization: `Bearer ${key}`, Accept: "application/json" },
1405
- signal: AbortSignal.timeout(1e4),
1406
- redirect: "error"
1407
- });
1408
- if (!response.ok) throw new Error(`OpenCode Go usage unavailable (HTTP ${response.status})`);
1409
- const body = await response.json();
1410
- 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
+ }
1411
1550
  }
1412
1551
  };
1413
1552
 
@@ -1418,8 +1557,12 @@ var GoModelsService = class extends TypertRemoteService2 {
1418
1557
  super(ctx, "opencodeGoModels");
1419
1558
  this.options = options;
1420
1559
  }
1421
- read() {
1422
- return discoverSettingsModels(this.options.catalog());
1560
+ async read() {
1561
+ try {
1562
+ return await discoverSettingsModels(this.options.catalog());
1563
+ } finally {
1564
+ this.options.onRefresh?.();
1565
+ }
1423
1566
  }
1424
1567
  };
1425
1568
 
@@ -1488,9 +1631,15 @@ function apply(ctx, raw) {
1488
1631
  ctx.logger.warn(`llm-opencode-go: unusable replay state on assistant history; sending provider-neutral content (${reason})`);
1489
1632
  }
1490
1633
  });
1491
- ctx.plugin(GoModelsService, { catalog: () => adapter.catalogOf(current()) });
1492
- let pickerVisibility = current().showDeprecatedModels;
1493
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();
1494
1643
  const applyRoute = (configured) => {
1495
1644
  if (configured && current().enabled && registration === void 0) {
1496
1645
  try {
@@ -1507,8 +1656,9 @@ function apply(ctx, raw) {
1507
1656
  }
1508
1657
  };
1509
1658
  const syncRoute = () => {
1510
- if (pickerVisibility !== current().showDeprecatedModels) {
1511
- pickerVisibility = current().showDeprecatedModels;
1659
+ const visibility = pickerVisibilityOf();
1660
+ if (pickerVisibility !== visibility) {
1661
+ pickerVisibility = visibility;
1512
1662
  registration?.replace([PROVIDER_ID]);
1513
1663
  }
1514
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;
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
12
12
  import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
13
- import type { GoModel } from '../models-contract.ts';
13
+ import type { GoModel, GoModelCatalog } from '../models-contract.ts';
14
14
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
15
15
  import type { SettingsScope } from './settings.ts';
16
16
  import { type FieldState, type FormActions, type FormShell } from './staged-form.ts';
@@ -20,8 +20,8 @@ export declare const OPENCODE_GO_NS = "llm-opencode-go";
20
20
  export interface OpencodeGoSettings {
21
21
  /** Whether the adapter serves its route; false withdraws it from every picker. */
22
22
  enabled?: boolean;
23
- /** Include gateway-served deprecated models in conversation pickers. */
24
- showDeprecatedModels?: boolean;
23
+ /** Per-model switches; normal models default on, deprecated models default off. */
24
+ modelVisibility?: Record<string, boolean>;
25
25
  /** Credential reference naming the environment key. */
26
26
  apiKeyEnv?: string;
27
27
  /** The gateway endpoint; also the live listing base. */
@@ -64,11 +64,14 @@ export type OpencodeGoModels =
64
64
  readonly count: number;
65
65
  readonly preview: readonly string[];
66
66
  readonly entries: readonly GoModel[];
67
+ readonly stale?: boolean;
68
+ readonly message?: string;
69
+ readonly refreshing?: boolean;
67
70
  }
68
71
  /** The listing could not be read; `message` is the Host's own diagnostic. */
69
72
  | {
70
73
  readonly status: 'failed';
71
- readonly message: string;
74
+ readonly message?: string;
72
75
  };
73
76
  /** What the settings page renders. */
74
77
  export interface OpencodeGoSectionState extends FormShell {
@@ -78,7 +81,7 @@ export interface OpencodeGoSectionState extends FormShell {
78
81
  * a withdrawn route is what the user is trying to observe.
79
82
  */
80
83
  enabled: boolean;
81
- showDeprecatedModels: boolean;
84
+ modelVisibility: Readonly<Record<string, boolean>>;
82
85
  pickerSaving: boolean;
83
86
  pickerFailed: boolean;
84
87
  /** Credential reference naming the environment key. */
@@ -121,7 +124,7 @@ export interface OpencodeGoSectionFace extends FormActions {
121
124
  * @param next - the state the switch asks for.
122
125
  */
123
126
  setEnabled: (next: boolean) => void;
124
- setShowDeprecatedModels: (next: boolean) => void;
127
+ setModelEnabled: (id: string, next: boolean) => void;
125
128
  }
126
129
  /** Bridges the `llm-opencode-go` scope and the credentials domain onto the page. */
127
130
  export declare class OpencodeGoSectionController {
@@ -142,7 +145,7 @@ export declare class OpencodeGoSectionController {
142
145
  * @param ctx - the page plugin's context, whose `remote.credentials` namespace
143
146
  * answers for the credential the section references.
144
147
  */
145
- constructor(scope: SettingsScope<OpencodeGoSettings>, ctx: ClientContext, readModels?: () => Promise<RemoteResult<readonly GoModel[]>>);
148
+ constructor(scope: SettingsScope<OpencodeGoSettings>, ctx: ClientContext, readModels?: () => Promise<RemoteResult<GoModelCatalog>>);
146
149
  /** Release subscriptions without disposing the host's shared form. */
147
150
  dispose(): void;
148
151
  private projection;
@@ -161,7 +164,7 @@ export declare class OpencodeGoSectionController {
161
164
  /**
162
165
  * Flip the switch by writing the field on the click itself.
163
166
  *
164
- * This is the one control on the page that does not wait for Save: the point
167
+ * Like the per-model switches, this control does not wait for Save: the point
165
168
  * of turning it off is to watch the models leave the pickers, and the point
166
169
  * of turning it back on is to use the route again — staging either behind a
167
170
  * second gesture would report a state the Host does not hold. The write is
@@ -169,9 +172,11 @@ export declare class OpencodeGoSectionController {
169
172
  * failed save through the shared shell rather than a silent revert.
170
173
  * @param next - the state the switch asks for.
171
174
  */
172
- setEnabled(next: boolean): void;
173
- /** Visibility is immediate; failed writes leave the committed switch state visible. */
174
- setShowDeprecatedModels(next: boolean): Promise<void>;
175
+ setEnabled(next: boolean): Promise<void>;
176
+ /** Each model switch has one committed value and takes effect without Save. */
177
+ setModelEnabled(id: string, next: boolean): Promise<void>;
178
+ /** Serialize immediate switches with form saves; refused writes retain committed values. */
179
+ private writePickerSetting;
175
180
  /**
176
181
  * Read the gateway's model listing through the Host's discovery for this
177
182
  * adapter. Called when the page mounts and again from its refresh control.