dsh-opencode-go 0.1.6 → 0.1.8

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
@@ -308,9 +308,13 @@ function flattenText(message) {
308
308
  function toolResultText(blocks) {
309
309
  return blocks.map((block) => block.type === "text" ? block.text : block.type === "tool-result" ? toolResultText(block.content) : "").join("");
310
310
  }
311
+ function toolMessage(message) {
312
+ if (message.role !== "tool") return void 0;
313
+ return message;
314
+ }
311
315
  function assertSupportedImageRoles(messages) {
312
316
  for (const message of messages) {
313
- if (message.role !== "user" && contentHasImage(message.content)) {
317
+ if (message.role !== "user" && !toolMessage(message) && contentHasImage(message.content)) {
314
318
  throw new LlmError3(
315
319
  `pi-ai cannot represent an image in an in-history ${message.role} message`,
316
320
  "UNSUPPORTED_CONTENT"
@@ -417,6 +421,18 @@ function textOnlyContext(options, onReplayDegrade) {
417
421
  if (contentHasImage(message.content)) {
418
422
  throw new LlmError3("pi-ai image conversion requires the durable attachment service", "UNSUPPORTED_CONTENT");
419
423
  }
424
+ const tool = toolMessage(message);
425
+ if (tool) {
426
+ messages.push({
427
+ role: "toolResult",
428
+ toolCallId: tool.toolCallId,
429
+ toolName: toolNames.get(tool.toolCallId) ?? "unknown",
430
+ content: [{ type: "text", text: toolResultText(tool.content) || "(no output)" }],
431
+ isError: tool.isError ?? false,
432
+ timestamp: 0
433
+ });
434
+ continue;
435
+ }
420
436
  if (message.role === "system") {
421
437
  messages.push({ role: "user", content: flattenText(message), timestamp: 0 });
422
438
  continue;
@@ -480,6 +496,19 @@ async function toPiContextWithImages(options, images, onReplayDegrade) {
480
496
  const toolNames = /* @__PURE__ */ new Map();
481
497
  const messages = [];
482
498
  for (const message of exactMessages) {
499
+ const tool = toolMessage(message);
500
+ if (tool) {
501
+ const content2 = await userContent(tool.content, requestImages, resolveImageAccess);
502
+ messages.push({
503
+ role: "toolResult",
504
+ toolCallId: tool.toolCallId,
505
+ toolName: toolNames.get(tool.toolCallId) ?? "unknown",
506
+ content: typeof content2 === "string" ? [{ type: "text", text: content2 || "(no output)" }] : content2,
507
+ isError: tool.isError ?? false,
508
+ timestamp: 0
509
+ });
510
+ continue;
511
+ }
483
512
  if (message.role === "system") {
484
513
  messages.push({ role: "user", content: flattenText(message), timestamp: 0 });
485
514
  continue;
@@ -676,6 +705,54 @@ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completio
676
705
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
677
706
  import { attributionHeaders, LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
678
707
 
708
+ // src/models-contract.ts
709
+ function validReleaseDate(value) {
710
+ 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
+ }
712
+ function isNewModel(model, now = Date.now()) {
713
+ if (model.deprecated || !validReleaseDate(model.releaseDate)) return false;
714
+ const days = Math.floor(now / 864e5) - Date.parse(model.releaseDate) / 864e5;
715
+ return days >= 0 && days < 7;
716
+ }
717
+ function sortModels(models, now = Date.now()) {
718
+ const rank = (model) => model.deprecated ? 2 : isNewModel(model, now) ? 0 : 1;
719
+ return [...models].sort((a, b) => rank(a) - rank(b) || (isNewModel(a, now) && isNewModel(b, now) ? b.releaseDate.localeCompare(a.releaseDate) : 0));
720
+ }
721
+ function parseGoModels(value) {
722
+ if (!Array.isArray(value)) throw new Error("Invalid OpenCode Go model list");
723
+ return value.map((entry) => {
724
+ if (!entry || typeof entry !== "object") throw new Error("Invalid OpenCode Go model");
725
+ const row = entry;
726
+ if (typeof row.id !== "string" || !row.id) throw new Error("Missing OpenCode Go model id");
727
+ const model = { id: row.id };
728
+ if (typeof row.name === "string") model.name = row.name;
729
+ for (const key of ["contextWindow", "maxTokens"]) {
730
+ if (typeof row[key] === "number" && Number.isSafeInteger(row[key]) && row[key] > 0) model[key] = row[key];
731
+ }
732
+ if (typeof row.deprecated === "boolean") model.deprecated = row.deprecated;
733
+ if (validReleaseDate(row.releaseDate)) model.releaseDate = row.releaseDate;
734
+ return model;
735
+ });
736
+ }
737
+ var codec = {
738
+ mode: "strict",
739
+ typeSymbol: "dsh-opencode-go#GoModels",
740
+ schema: { parse: parseGoModels },
741
+ create: () => ({ parse: parseGoModels })
742
+ };
743
+ var modelsRemote = {
744
+ package: "dsh-opencode-go",
745
+ descriptors: [{
746
+ id: "dsh-opencode-go#opencodeGoModels/read",
747
+ service: "opencodeGoModels",
748
+ namespace: "opencodeGoModels",
749
+ method: "read",
750
+ invocation: { kind: "direct" },
751
+ parameters: [],
752
+ result: codec
753
+ }]
754
+ };
755
+
679
756
  // src/model-metadata.ts
680
757
  var MODEL_METADATA_URL = "https://models.dev/api.json";
681
758
  function record(value) {
@@ -726,7 +803,13 @@ function readModelMetadata(body, baseURL, builtin) {
726
803
  const entries = record(provider.models);
727
804
  const models = /* @__PURE__ */ new Map();
728
805
  const errors = /* @__PURE__ */ new Map();
806
+ const details = /* @__PURE__ */ new Map();
729
807
  for (const [id, value] of Object.entries(entries)) {
808
+ const data = record(value);
809
+ details.set(id, {
810
+ deprecated: data.status === "deprecated",
811
+ ...validReleaseDate(data.release_date) ? { releaseDate: data.release_date } : {}
812
+ });
730
813
  try {
731
814
  const metadata = record(value);
732
815
  const npm = record(metadata.provider).npm ?? provider.npm;
@@ -772,7 +855,7 @@ function readModelMetadata(body, baseURL, builtin) {
772
855
  errors.set(id, error instanceof Error ? error.message : String(error));
773
856
  }
774
857
  }
775
- return { models, errors };
858
+ return { models, errors, details };
776
859
  }
777
860
 
778
861
  // src/catalog.ts
@@ -887,9 +970,10 @@ var OpencodeGoCatalog = class {
887
970
  this.onFallback({ url: MODEL_METADATA_URL, error: metadataResult.reason, kept: known.size });
888
971
  }
889
972
  if (listing.status === "rejected") {
890
- const models2 = this.served?.models ?? known;
973
+ const models2 = this.served?.models ?? /* @__PURE__ */ new Map();
891
974
  this.onFallback({ url: `${this.baseURL.replace(/\/+$/, "")}/models`, error: listing.reason, kept: models2.size });
892
975
  return {
976
+ details: this.served?.details ?? /* @__PURE__ */ new Map(),
893
977
  models: models2,
894
978
  unavailable: this.served?.unavailable ?? /* @__PURE__ */ new Map(),
895
979
  provider: buildProvider(this.baseURL, [...models2.values()]),
@@ -910,6 +994,7 @@ var OpencodeGoCatalog = class {
910
994
  }
911
995
  if (unavailable.size > 0) this.onOmitted([...unavailable.keys()]);
912
996
  return {
997
+ details: new Map(listing.value.map((id) => [id, metadata?.details.get(id) ?? {}])),
913
998
  models,
914
999
  unavailable,
915
1000
  provider: buildProvider(this.baseURL, [...models.values()]),
@@ -936,6 +1021,9 @@ async function discoverCatalogModels(catalog) {
936
1021
  if (!snapshot.live) {
937
1022
  throw new LlmError5("llm-opencode-go: the live model listing is unreachable; try again later", "DISCOVERY_FAILED");
938
1023
  }
1024
+ return describeCatalog(snapshot);
1025
+ }
1026
+ function describeCatalog(snapshot) {
939
1027
  return [
940
1028
  ...[...snapshot.models.values()].map((model) => ({
941
1029
  id: model.id,
@@ -946,6 +1034,11 @@ async function discoverCatalogModels(catalog) {
946
1034
  ...[...snapshot.unavailable].map(([id, reason]) => ({ id, name: `${id} (metadata unavailable: ${reason})` }))
947
1035
  ];
948
1036
  }
1037
+ async function discoverSettingsModels(catalog) {
1038
+ 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) })));
1041
+ }
949
1042
 
950
1043
  // src/config.ts
951
1044
  import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
@@ -953,8 +1046,9 @@ import z from "@deepseek-ai/schemastery";
953
1046
  var DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
954
1047
  var DEFAULT_REFRESH_MINUTES = 60;
955
1048
  var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
956
- var Config = z.object({
1049
+ var fields = {
957
1050
  enabled: z.boolean().default(true),
1051
+ showDeprecatedModels: z.boolean().default(false),
958
1052
  apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
959
1053
  baseURL: z.string().default(DEFAULT_BASE_URL),
960
1054
  refreshMinutes: z.number().step(1).min(1).max(7 * 24 * 60).default(DEFAULT_REFRESH_MINUTES),
@@ -963,8 +1057,20 @@ var Config = z.object({
963
1057
  // request image fits the budget, and fifteen of them fit the payload cap.
964
1058
  maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
965
1059
  requestImagePixelBudget: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
966
- requestImageMaxBytes: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES)
967
- });
1060
+ requestImageMaxBytes: z.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES),
1061
+ // null explicitly selects catalog values, overriding even inherited profile limits.
1062
+ modelLimits: z.dict(z.union([z.const(null), z.object({
1063
+ contextWindow: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)]),
1064
+ maxTokens: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)])
1065
+ })])).default({})
1066
+ };
1067
+ var PlainConfig = z.object(fields);
1068
+ var Config = z.object(Object.fromEntries(
1069
+ Object.entries(fields).map(([key, schema]) => [key, schema.volatile()])
1070
+ ));
1071
+ function readConfig(config) {
1072
+ return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, value.get()]));
1073
+ }
968
1074
  function assertBaseURL(raw) {
969
1075
  let url;
970
1076
  try {
@@ -982,6 +1088,15 @@ function assertBaseURL(raw) {
982
1088
  }
983
1089
 
984
1090
  // src/adapter.ts
1091
+ function withModelLimit(model, limits) {
1092
+ const limit = limits[model.id];
1093
+ if (limit == null) return model;
1094
+ return {
1095
+ ...model,
1096
+ contextWindow: limit.contextWindow ?? model.contextWindow,
1097
+ maxTokens: limit.maxTokens ?? model.maxTokens
1098
+ };
1099
+ }
985
1100
  function opencodeSessionValue(sessionId) {
986
1101
  return sessionId !== void 0 && sessionId.length > 0 ? sessionId : randomUUID();
987
1102
  }
@@ -1001,9 +1116,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1001
1116
  * The catalog resolver for one configuration, rebuilding on the facts it
1002
1117
  * owns. Public for the plugin's discovery registration, which resolves the
1003
1118
  * current configuration the same way the adapter does.
1004
- * @param config - the configuration whose endpoint and refresh interval the
1005
- * resolver serves; a change to either yields a fresh resolver.
1006
- * @returns the resolver caching one snapshot per endpoint/refresh pair.
1119
+ * @param config - the endpoint and refresh interval for the raw catalog.
1120
+ * @returns the resolver caching catalog values, independent of deployment limits.
1007
1121
  */
1008
1122
  catalogOf(config) {
1009
1123
  const key = `${config.baseURL}|${String(config.refreshMinutes)}`;
@@ -1028,8 +1142,9 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1028
1142
  return { id: provider, name: DISPLAY_NAME };
1029
1143
  }
1030
1144
  async listModels(_provider) {
1031
- const snapshot = await this.catalogOf(this.options.config()).snapshot(true);
1032
- return [...snapshot.models.values()].map((model) => ({
1145
+ 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) => ({
1033
1148
  provider: PROVIDER_ID,
1034
1149
  id: model.id,
1035
1150
  name: model.name,
@@ -1037,12 +1152,13 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1037
1152
  }));
1038
1153
  }
1039
1154
  async resolveModel(_provider, model, _signal) {
1040
- const snapshot = await this.catalogOf(this.options.config()).forModel(model);
1155
+ const config = this.options.config();
1156
+ const snapshot = await this.catalogOf(config).forModel(model);
1041
1157
  const resolved = snapshot.models.get(model);
1042
1158
  if (resolved === void 0) {
1043
1159
  throw new LlmError6(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
1044
1160
  }
1045
- return this.modelInfo(resolved);
1161
+ return this.modelInfo(withModelLimit(resolved, config.modelLimits));
1046
1162
  }
1047
1163
  /** Describe one model: capacities plus the reasoning levels it actually offers. */
1048
1164
  modelInfo(model) {
@@ -1083,10 +1199,13 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1083
1199
  }
1084
1200
  const config = this.options.config();
1085
1201
  const snapshot = await this.catalogOf(config).forModel(options.model);
1086
- const model = snapshot.models.get(options.model);
1087
- if (model === void 0) {
1202
+ const advertised = snapshot.models.get(options.model);
1203
+ if (advertised === void 0) {
1088
1204
  throw new LlmError6(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
1089
1205
  }
1206
+ const model = withModelLimit(advertised, config.modelLimits);
1207
+ const outputLimit = config.modelLimits[model.id]?.maxTokens;
1208
+ const maxTokens = outputLimit == null ? options.maxTokens : Math.min(options.maxTokens ?? outputLimit, outputLimit);
1090
1209
  const apiKey = await this.options.resolveApiKey();
1091
1210
  if (apiKey === void 0 || apiKey.length === 0) {
1092
1211
  throw new LlmError6("llm-opencode-go: no credential resolved for the route", "MISSING_CREDENTIAL");
@@ -1122,7 +1241,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1122
1241
  apiKey,
1123
1242
  ...reasoning === void 0 || reasoning === "off" ? {} : { reasoning },
1124
1243
  ...options.temperature === void 0 ? {} : { temperature: options.temperature },
1125
- ...options.maxTokens === void 0 ? {} : { maxTokens: options.maxTokens },
1244
+ ...maxTokens === void 0 ? {} : { maxTokens },
1126
1245
  ...options.sessionId === void 0 ? {} : { sessionId: String(options.sessionId) },
1127
1246
  signal: watchdog.signal,
1128
1247
  // Harness-owned request identity: the gateway refuses requests without
@@ -1218,15 +1337,6 @@ var GoUsageService = class extends TypertRemoteService {
1218
1337
  constructor(ctx, options) {
1219
1338
  super(ctx, "opencodeGoUsage");
1220
1339
  this.options = options;
1221
- ctx.inject(["typert"], (scope) => {
1222
- scope.effect(() => scope.typert.register({
1223
- package: usageRemote.package,
1224
- face: "host",
1225
- schemas: [],
1226
- model: { services: [], events: [], objects: [] },
1227
- invocations: usageRemote.descriptors
1228
- }));
1229
- });
1230
1340
  }
1231
1341
  async read() {
1232
1342
  const baseURL = assertBaseURL(this.options.baseURL()).replace(/\/$/, "");
@@ -1243,14 +1353,46 @@ var GoUsageService = class extends TypertRemoteService {
1243
1353
  }
1244
1354
  };
1245
1355
 
1356
+ // src/models.ts
1357
+ import { TypertRemoteService as TypertRemoteService2 } from "@deepseek-ai/dsh-typert-protocol";
1358
+ var GoModelsService = class extends TypertRemoteService2 {
1359
+ constructor(ctx, options) {
1360
+ super(ctx, "opencodeGoModels");
1361
+ this.options = options;
1362
+ }
1363
+ read() {
1364
+ return discoverSettingsModels(this.options.catalog());
1365
+ }
1366
+ };
1367
+
1368
+ // src/remote-contract.ts
1369
+ var goRemote = {
1370
+ package: "dsh-opencode-go",
1371
+ descriptors: [...usageRemote.descriptors, ...modelsRemote.descriptors]
1372
+ };
1373
+
1374
+ // src/remotes.ts
1375
+ function registerGoRemotes(ctx) {
1376
+ ctx.inject(["typert"], (scope) => {
1377
+ scope.effect(() => scope.typert.register({
1378
+ package: goRemote.package,
1379
+ face: "host",
1380
+ schemas: [],
1381
+ model: { services: [], events: [], objects: [] },
1382
+ invocations: goRemote.descriptors
1383
+ }));
1384
+ });
1385
+ }
1386
+
1246
1387
  // src/index.ts
1247
1388
  var name = "llm-opencode-go";
1248
1389
  var inject = ["llm"];
1249
1390
  var NS = "llm-opencode-go";
1250
1391
  function apply(ctx, raw) {
1251
- const entry = Config(raw);
1392
+ const config = raw && typeof raw.enabled === "object" ? raw : Config(raw);
1393
+ const entry = readConfig(config);
1252
1394
  assertBaseURL(entry.baseURL);
1253
- let current = () => entry;
1395
+ let current = () => readConfig(config);
1254
1396
  const resolveApiKey = async () => {
1255
1397
  const ref = current().apiKeyEnv;
1256
1398
  const credentials = ctx.get("credentials");
@@ -1261,6 +1403,7 @@ function apply(ctx, raw) {
1261
1403
  "MISSING_CREDENTIAL"
1262
1404
  );
1263
1405
  };
1406
+ registerGoRemotes(ctx);
1264
1407
  ctx.plugin(GoUsageService, { baseURL: () => current().baseURL, resolveApiKey });
1265
1408
  const logger = {
1266
1409
  fallback: ({ url, error }) => {
@@ -1287,6 +1430,8 @@ function apply(ctx, raw) {
1287
1430
  ctx.logger.warn(`llm-opencode-go: unusable replay state on assistant history; sending provider-neutral content (${reason})`);
1288
1431
  }
1289
1432
  });
1433
+ ctx.plugin(GoModelsService, { catalog: () => adapter.catalogOf(current()) });
1434
+ let pickerVisibility = current().showDeprecatedModels;
1290
1435
  let registration;
1291
1436
  const applyRoute = (configured) => {
1292
1437
  if (configured && current().enabled && registration === void 0) {
@@ -1304,6 +1449,10 @@ function apply(ctx, raw) {
1304
1449
  }
1305
1450
  };
1306
1451
  const syncRoute = () => {
1452
+ if (pickerVisibility !== current().showDeprecatedModels) {
1453
+ pickerVisibility = current().showDeprecatedModels;
1454
+ registration?.replace([PROVIDER_ID]);
1455
+ }
1307
1456
  const credentials = ctx.get("credentials");
1308
1457
  if (credentials === void 0) {
1309
1458
  applyRoute(launchEnvironmentOf(ctx).get(current().apiKeyEnv)?.value !== void 0);
@@ -1330,7 +1479,12 @@ function apply(ctx, raw) {
1330
1479
  undiscover();
1331
1480
  });
1332
1481
  ctx.inject(["settings"], (settingsCtx) => {
1333
- settingsCtx.settings.installSection(ctx, NS, Config, entry, {
1482
+ if ("configure" in settingsCtx.settings) {
1483
+ const settings = settingsCtx.settings;
1484
+ settingsCtx.effect(() => settings.configure({ auto: false }, ctx.fiber));
1485
+ return;
1486
+ }
1487
+ settingsCtx.settings.installSection(ctx, NS, PlainConfig, entry, {
1334
1488
  validate: (value) => {
1335
1489
  assertBaseURL(value.baseURL);
1336
1490
  },
@@ -1342,6 +1496,12 @@ function apply(ctx, raw) {
1342
1496
  }
1343
1497
  });
1344
1498
  });
1499
+ ctx.on("internal/config", function(_raw, next) {
1500
+ const value = next();
1501
+ if (this === ctx.fiber) assertBaseURL(PlainConfig(value).baseURL);
1502
+ return value;
1503
+ });
1504
+ ctx.on("loader/volatile-update", syncRoute);
1345
1505
  ctx.inject(["credentials"], (credentialsCtx) => {
1346
1506
  credentialsCtx.on("credentials/reference-updated", (ref) => {
1347
1507
  if (ref === current().apiKeyEnv) syncRoute();
@@ -1358,6 +1518,7 @@ export {
1358
1518
  OpencodeGoAdapter,
1359
1519
  OpencodeGoCatalog,
1360
1520
  PROVIDER_ID,
1521
+ PlainConfig,
1361
1522
  apply,
1362
1523
  assertBaseURL,
1363
1524
  discoverCatalogModels,
@@ -82,9 +82,8 @@ export declare class OpencodeGoAdapter extends LlmAdapter {
82
82
  * The catalog resolver for one configuration, rebuilding on the facts it
83
83
  * owns. Public for the plugin's discovery registration, which resolves the
84
84
  * current configuration the same way the adapter does.
85
- * @param config - the configuration whose endpoint and refresh interval the
86
- * resolver serves; a change to either yields a fresh resolver.
87
- * @returns the resolver caching one snapshot per endpoint/refresh pair.
85
+ * @param config - the endpoint and refresh interval for the raw catalog.
86
+ * @returns the resolver caching catalog values, independent of deployment limits.
88
87
  */
89
88
  catalogOf(config: OpencodeGoConfig): OpencodeGoCatalog;
90
89
  providerInfo(provider: string): {
@@ -1,9 +1,12 @@
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';
4
+ import type { ModelMetadata } from './model-metadata.ts';
3
5
  export declare const PROVIDER_ID = "opencode-go";
4
6
  export declare const DISPLAY_NAME = "OpenCode Go";
5
7
  export declare const DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
6
8
  export interface CatalogSnapshot {
9
+ readonly details: ModelMetadata['details'];
7
10
  readonly models: ReadonlyMap<string, Model<Api>>;
8
11
  /** Advertised ids with missing/unsupported metadata stay visible with a diagnostic. */
9
12
  readonly unavailable: ReadonlyMap<string, string>;
@@ -41,3 +44,5 @@ export declare class OpencodeGoCatalog {
41
44
  }
42
45
  /** Explicit discovery always revalidates both sources, including during the runtime TTL. */
43
46
  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[]>;
@@ -0,0 +1,13 @@
1
+ import type { OpencodeGoModelLimit, OpencodeGoModelLimits, OpencodeGoModels } from './section-controller.ts';
2
+ import type { en } from './locales.ts';
3
+ type Translate = (key: keyof typeof en, params?: Record<string, unknown>) => string;
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 }: {
7
+ models: OpencodeGoModels;
8
+ draft: OpencodeGoModelLimits;
9
+ t: Translate;
10
+ disabled: boolean;
11
+ onEdit: (next: OpencodeGoModelLimits) => void;
12
+ }): import("react").JSX.Element;
13
+ export {};
@@ -28,6 +28,36 @@ export declare const en: {
28
28
  modelsFailed: string;
29
29
  modelsEmpty: string;
30
30
  modelsCount: string;
31
+ limitsLabel: string;
32
+ limitsHint: string;
33
+ limitsFilterLabel: string;
34
+ limitsFilterPlaceholder: string;
35
+ limitsSummary: string;
36
+ limitsModel: string;
37
+ limitsContext: string;
38
+ limitsOutput: string;
39
+ capacityDefault: string;
40
+ capacityMissing: string;
41
+ showDeprecatedLabel: string;
42
+ showDeprecatedHint: string;
43
+ pickerFailed: string;
44
+ deprecatedBadge: string;
45
+ deprecatedHint: string;
46
+ newHint: string;
47
+ releasedOn: string;
48
+ releaseSource: string;
49
+ modelDetails: string;
50
+ filterLabel: string;
51
+ filterAll: string;
52
+ filterNew: string;
53
+ filterCustom: string;
54
+ filterDeprecated: string;
55
+ limitsResetModel: string;
56
+ limitsResetAll: string;
57
+ limitsNoMatches: string;
58
+ limitsEmpty: string;
59
+ limitsContextLabel: string;
60
+ limitsOutputLabel: string;
31
61
  advancedLabel: string;
32
62
  advancedHint: string;
33
63
  apiKeyEnvLabel: string;
@@ -9,8 +9,10 @@
9
9
  * covers everything the page shows.
10
10
  */
11
11
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
12
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
13
+ import type { GoModel } from '../models-contract.ts';
12
14
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
13
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
15
+ import type { SettingsScope } from './settings.ts';
14
16
  import { type FieldState, type FormActions, type FormShell } from './staged-form.ts';
15
17
  /** Namespace of the OpenCode Go adapter. Spelled here rather than imported: a client package must not depend on a Host package. */
16
18
  export declare const OPENCODE_GO_NS = "llm-opencode-go";
@@ -18,6 +20,8 @@ export declare const OPENCODE_GO_NS = "llm-opencode-go";
18
20
  export interface OpencodeGoSettings {
19
21
  /** Whether the adapter serves its route; false withdraws it from every picker. */
20
22
  enabled?: boolean;
23
+ /** Include gateway-served deprecated models in conversation pickers. */
24
+ showDeprecatedModels?: boolean;
21
25
  /** Credential reference naming the environment key. */
22
26
  apiKeyEnv?: string;
23
27
  /** The gateway endpoint; also the live listing base. */
@@ -32,7 +36,15 @@ export interface OpencodeGoSettings {
32
36
  requestImagePixelBudget?: number;
33
37
  /** Raw encoded-byte target for one request image. */
34
38
  requestImageMaxBytes?: number;
39
+ /** Per-model capacity overrides, keyed by the gateway model id. */
40
+ modelLimits?: OpencodeGoModelLimits;
35
41
  }
42
+ /** The two capacity values the settings table can override. */
43
+ export interface OpencodeGoModelLimit {
44
+ contextWindow?: number | null;
45
+ maxTokens?: number | null;
46
+ }
47
+ export type OpencodeGoModelLimits = Record<string, OpencodeGoModelLimit | null>;
36
48
  /** The gateway's model listing as the page reports it. */
37
49
  export type OpencodeGoModels =
38
50
  /** Not asked for yet; the page asks once it mounts. */
@@ -43,11 +55,15 @@ export type OpencodeGoModels =
43
55
  | {
44
56
  readonly status: 'loading';
45
57
  }
46
- /** The gateway answered: how many models it serves, and a preview of their names. */
58
+ /**
59
+ * The gateway answered: a count/preview for the compact summary plus every
60
+ * discovered entry used by the capacity editor.
61
+ */
47
62
  | {
48
63
  readonly status: 'ready';
49
64
  readonly count: number;
50
65
  readonly preview: readonly string[];
66
+ readonly entries: readonly GoModel[];
51
67
  }
52
68
  /** The listing could not be read; `message` is the Host's own diagnostic. */
53
69
  | {
@@ -62,6 +78,9 @@ export interface OpencodeGoSectionState extends FormShell {
62
78
  * a withdrawn route is what the user is trying to observe.
63
79
  */
64
80
  enabled: boolean;
81
+ showDeprecatedModels: boolean;
82
+ pickerSaving: boolean;
83
+ pickerFailed: boolean;
65
84
  /** Credential reference naming the environment key. */
66
85
  apiKeyEnv: FieldState;
67
86
  /** The gateway endpoint. */
@@ -84,6 +103,10 @@ export interface OpencodeGoSectionState extends FormShell {
84
103
  apiKeyWritable: boolean;
85
104
  /** The gateway's current model listing. */
86
105
  models: OpencodeGoModels;
106
+ /** The staged JSON field backing the capacity table. */
107
+ modelLimits: FieldState;
108
+ /** The parsed overrides currently shown by the capacity table. */
109
+ modelLimitDraft: OpencodeGoModelLimits;
87
110
  }
88
111
  /** The registration-side face the page's slot entry injects. */
89
112
  export interface OpencodeGoSectionFace extends FormActions {
@@ -98,24 +121,37 @@ export interface OpencodeGoSectionFace extends FormActions {
98
121
  * @param next - the state the switch asks for.
99
122
  */
100
123
  setEnabled: (next: boolean) => void;
124
+ setShowDeprecatedModels: (next: boolean) => void;
101
125
  }
102
126
  /** Bridges the `llm-opencode-go` scope and the credentials domain onto the page. */
103
127
  export declare class OpencodeGoSectionController {
104
128
  private readonly scope;
105
129
  private readonly ctx;
130
+ private readonly readModels;
106
131
  private readonly form;
107
132
  private readonly store;
108
133
  private credential;
109
134
  private models;
110
135
  private modelsRequest;
136
+ private pickerSaving;
137
+ private pickerFailed;
111
138
  private face;
139
+ private readonly unsubscribe;
112
140
  /**
113
141
  * @param scope - the bound settings scope for the `llm-opencode-go` namespace.
114
142
  * @param ctx - the page plugin's context, whose `remote.credentials` namespace
115
143
  * answers for the credential the section references.
116
144
  */
117
- constructor(scope: SettingsScope<OpencodeGoSettings>, ctx: ClientContext);
145
+ constructor(scope: SettingsScope<OpencodeGoSettings>, ctx: ClientContext, readModels?: () => Promise<RemoteResult<readonly GoModel[]>>);
146
+ /** Release subscriptions without disposing the host's shared form. */
147
+ dispose(): void;
118
148
  private projection;
149
+ /**
150
+ * Read the overrides as the page currently shows them. A staged JSON draft
151
+ * wins while it is valid; malformed text falls back to the last accepted
152
+ * settings value so the table never renders phantom rows.
153
+ */
154
+ private limitDraft;
119
155
  /**
120
156
  * The adapter's effective switch state: the resolved section's value, over
121
157
  * the Host's own default when the section carries none.
@@ -134,6 +170,8 @@ export declare class OpencodeGoSectionController {
134
170
  * @param next - the state the switch asks for.
135
171
  */
136
172
  setEnabled(next: boolean): void;
173
+ /** Visibility is immediate; failed writes leave the committed switch state visible. */
174
+ setShowDeprecatedModels(next: boolean): Promise<void>;
137
175
  /**
138
176
  * Read the gateway's model listing through the Host's discovery for this
139
177
  * adapter. Called when the page mounts and again from its refresh control.
@@ -0,0 +1,18 @@
1
+ import type { SettingsPathOpView } from '@deepseek-ai/dsh-api-remotes/client';
2
+ /** Common read/write contract of legacy SettingsScope and 0.1.7 ConfigForm. */
3
+ export interface SettingsScopeSnapshot<T> {
4
+ status: 'loading' | 'ready' | 'unavailable';
5
+ value: T | undefined;
6
+ base: unknown;
7
+ user: unknown;
8
+ revision: number | undefined;
9
+ writable: boolean;
10
+ mode: 'host' | 'memory';
11
+ }
12
+ export interface SettingsScope<T> {
13
+ getSnapshot(): SettingsScopeSnapshot<T>;
14
+ subscribe(listener: () => void): () => void;
15
+ mutate(ops: readonly SettingsPathOpView[], expectedRevision?: number): Promise<void | boolean>;
16
+ set(field: string, value: unknown): Promise<void | boolean>;
17
+ unset(field: string): Promise<void | boolean>;
18
+ }
@@ -17,7 +17,7 @@
17
17
  * forms evolve independently anyway.
18
18
  */
19
19
  import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
20
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
20
+ import type { SettingsScope } from './settings.ts';
21
21
  /** The write one field's staged text performs when the page is saved. */
22
22
  export type FieldWrite = {
23
23
  kind: 'set';
@@ -111,6 +111,13 @@ export declare function booleanField(field: string): FieldSpec;
111
111
  * @returns the field's conversion spec.
112
112
  */
113
113
  export declare function textField(field: string): FieldSpec;
114
+ /**
115
+ * A structured field edited as JSON text. Empty text clears the field and
116
+ * malformed JSON blocks the save instead of reaching the Host as a string.
117
+ * @param field - field name inside the namespace section.
118
+ * @returns the field's conversion spec.
119
+ */
120
+ export declare function jsonField(field: string): FieldSpec;
114
121
  /**
115
122
  * Stages the page's edits over the `llm-opencode-go` namespace and writes them
116
123
  * on save.
@@ -125,6 +132,7 @@ export declare class StagedForm {
125
132
  private readonly secretSpecs;
126
133
  private readonly staged;
127
134
  private readonly listeners;
135
+ private readonly unsubscribe;
128
136
  private saving;
129
137
  private failed;
130
138
  /**
@@ -133,6 +141,8 @@ export declare class StagedForm {
133
141
  * @param secrets - the page's write-only controls, written outside the section.
134
142
  */
135
143
  constructor(scope: SettingsScope<Record<string, unknown>>, specs: FieldSpec[], secrets?: SecretSpec[]);
144
+ /** Shared 0.1.7 forms outlive individual settings pages. */
145
+ dispose(): void;
136
146
  /**
137
147
  * Publish a projection of this form, rebuilt whenever the scope or a draft changes.
138
148
  * @param project - build the page state from the form's current reads.