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/README.en.md +130 -0
- package/README.md +39 -60
- package/docs/assets/install-via-dsh.gif +0 -0
- package/docs/verification.md +51 -0
- package/lib/client.js +628 -172
- package/lib/index.js +189 -28
- package/lib/types/adapter.d.ts +2 -3
- package/lib/types/catalog.d.ts +5 -0
- package/lib/types/client/ModelEditor.d.ts +13 -0
- package/lib/types/client/locales.d.ts +30 -0
- package/lib/types/client/section-controller.d.ts +41 -3
- package/lib/types/client/settings.d.ts +18 -0
- package/lib/types/client/staged-form.d.ts +11 -1
- package/lib/types/config.d.ts +31 -5
- package/lib/types/index.d.ts +12 -8
- package/lib/types/model-metadata.d.ts +2 -0
- package/lib/types/models-contract.d.ts +23 -0
- package/lib/types/models.d.ts +12 -0
- package/lib/types/remote-contract.d.ts +3 -0
- package/lib/types/remotes.d.ts +3 -0
- package/package.json +27 -23
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 ??
|
|
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
|
|
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
|
|
1005
|
-
*
|
|
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
|
|
1032
|
-
|
|
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
|
|
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
|
|
1087
|
-
if (
|
|
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
|
-
...
|
|
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
|
|
1392
|
+
const config = raw && typeof raw.enabled === "object" ? raw : Config(raw);
|
|
1393
|
+
const entry = readConfig(config);
|
|
1252
1394
|
assertBaseURL(entry.baseURL);
|
|
1253
|
-
let current = () =>
|
|
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
|
|
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,
|
package/lib/types/adapter.d.ts
CHANGED
|
@@ -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
|
|
86
|
-
*
|
|
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): {
|
package/lib/types/catalog.d.ts
CHANGED
|
@@ -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 '
|
|
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
|
-
/**
|
|
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 '
|
|
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.
|