dsh-opencode-go 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +31 -4
- package/README.md +31 -4
- package/docs/verification.md +60 -2
- package/lib/client.js +258 -114
- package/lib/index.js +220 -70
- package/lib/types/catalog.d.ts +5 -3
- package/lib/types/client/ModelEditor.d.ts +6 -2
- package/lib/types/client/Section.d.ts +2 -0
- package/lib/types/client/UsagePill.d.ts +1 -0
- package/lib/types/client/locales.d.ts +14 -2
- package/lib/types/client/section-controller.d.ts +16 -11
- package/lib/types/config.d.ts +5 -5
- package/lib/types/json-response.d.ts +10 -1
- package/lib/types/models-contract.d.ts +12 -1
- package/lib/types/models.d.ts +3 -2
- package/lib/types/usage-contract.d.ts +9 -0
- package/lib/types/usage.d.ts +1 -0
- package/package.json +1 -1
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#
|
|
740
|
-
schema: { parse:
|
|
741
|
-
create: () => ({ parse:
|
|
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 {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
997
|
+
const endpoint = diagnosticURL(url);
|
|
998
|
+
let result;
|
|
940
999
|
try {
|
|
941
|
-
|
|
1000
|
+
result = await fetchJsonResponse(url, {
|
|
942
1001
|
method: "GET",
|
|
943
1002
|
cache: "no-cache",
|
|
944
|
-
headers: { accept: "application/json"
|
|
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(
|
|
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
|
|
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(
|
|
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 ?? "
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
1202
|
-
return [...snapshot.models.values()].filter((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 {
|
|
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
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1511
|
-
|
|
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");
|
package/lib/types/catalog.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
48
|
-
export declare function discoverSettingsModels(catalog: OpencodeGoCatalog): Promise<
|
|
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
|
-
/**
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
/**
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
174
|
-
|
|
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.
|