dsh-opencode-go 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +36 -7
- package/README.md +36 -7
- package/docs/verification.md +77 -1
- package/lib/client.js +258 -114
- package/lib/index.js +257 -49
- 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 +11 -0
- 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",
|
|
@@ -858,11 +876,105 @@ function readModelMetadata(body, baseURL, builtin) {
|
|
|
858
876
|
return { models, errors, details };
|
|
859
877
|
}
|
|
860
878
|
|
|
879
|
+
// src/json-response.ts
|
|
880
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
881
|
+
var RETRYABLE_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "EPIPE", "UND_ERR_SOCKET", "EAI_AGAIN"]);
|
|
882
|
+
function errorCauses(error) {
|
|
883
|
+
const causes = [];
|
|
884
|
+
for (let depth = 0; depth < 5 && error !== null && typeof error === "object"; depth += 1) {
|
|
885
|
+
causes.push(error);
|
|
886
|
+
error = "cause" in error ? error.cause : void 0;
|
|
887
|
+
}
|
|
888
|
+
return causes;
|
|
889
|
+
}
|
|
890
|
+
function retryableTransportFailure(error) {
|
|
891
|
+
if (error instanceof SyntaxError || error instanceof RangeError) return false;
|
|
892
|
+
for (const cause of errorCauses(error)) {
|
|
893
|
+
if (cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError")) return false;
|
|
894
|
+
if ("code" in cause && typeof cause.code === "string") return RETRYABLE_CODES.has(cause.code);
|
|
895
|
+
}
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
function transportFailure(error) {
|
|
899
|
+
const causes = errorCauses(error);
|
|
900
|
+
if (causes.some((cause) => cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError"))) {
|
|
901
|
+
return "request timed out or was aborted";
|
|
902
|
+
}
|
|
903
|
+
for (const cause of causes) {
|
|
904
|
+
if ("code" in cause && typeof cause.code === "string" && /^[A-Z][A-Z0-9_]+$/.test(cause.code)) {
|
|
905
|
+
return `network error (${cause.code})`;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return "network request failed";
|
|
909
|
+
}
|
|
910
|
+
function diagnosticURL(raw) {
|
|
911
|
+
const url = new URL(raw);
|
|
912
|
+
url.username = "";
|
|
913
|
+
url.password = "";
|
|
914
|
+
url.search = "";
|
|
915
|
+
url.hash = "";
|
|
916
|
+
return url.href;
|
|
917
|
+
}
|
|
918
|
+
async function fetchJsonResponse(url, init, maxBytes) {
|
|
919
|
+
if ((init.method ?? "GET").toUpperCase() !== "GET") throw new TypeError("JSON requests must use GET");
|
|
920
|
+
const headers = new Headers(init.headers);
|
|
921
|
+
headers.set("accept-encoding", "identity");
|
|
922
|
+
const options = { ...init, method: "GET", headers };
|
|
923
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
924
|
+
init.signal?.throwIfAborted();
|
|
925
|
+
try {
|
|
926
|
+
const response = await fetch(url, options);
|
|
927
|
+
if (!response.ok) {
|
|
928
|
+
await response.body?.cancel().catch(() => {
|
|
929
|
+
});
|
|
930
|
+
return { response, body: void 0 };
|
|
931
|
+
}
|
|
932
|
+
return { response, body: await readJsonResponse(response, maxBytes) };
|
|
933
|
+
} catch (error) {
|
|
934
|
+
init.signal?.throwIfAborted();
|
|
935
|
+
if (attempt > 0 || !retryableTransportFailure(error)) throw error;
|
|
936
|
+
await delay(150, void 0, { signal: init.signal ?? void 0 });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async function readBoundedBody(response, maxBytes) {
|
|
941
|
+
if (response.body === null) return Buffer.alloc(0);
|
|
942
|
+
const reader = response.body.getReader();
|
|
943
|
+
const chunks = [];
|
|
944
|
+
let size = 0;
|
|
945
|
+
try {
|
|
946
|
+
while (true) {
|
|
947
|
+
const { done, value } = await reader.read();
|
|
948
|
+
if (done) return Buffer.concat(chunks, size);
|
|
949
|
+
if (value.byteLength > maxBytes - size) {
|
|
950
|
+
const error = new RangeError(`Response body exceeds ${maxBytes} byte limit`);
|
|
951
|
+
await reader.cancel(error).catch(() => {
|
|
952
|
+
});
|
|
953
|
+
throw error;
|
|
954
|
+
}
|
|
955
|
+
size += value.byteLength;
|
|
956
|
+
chunks.push(value);
|
|
957
|
+
}
|
|
958
|
+
} finally {
|
|
959
|
+
reader.releaseLock();
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async function readJsonResponse(response, maxBytes) {
|
|
963
|
+
const bytes = await readBoundedBody(response, maxBytes);
|
|
964
|
+
try {
|
|
965
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
966
|
+
} catch (error) {
|
|
967
|
+
throw new SyntaxError("Response is not valid JSON", { cause: error });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
861
971
|
// src/catalog.ts
|
|
862
972
|
var PROVIDER_ID = "opencode-go";
|
|
863
973
|
var DISPLAY_NAME = "OpenCode Go";
|
|
864
974
|
var DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
865
975
|
var MODELS_FETCH_TIMEOUT_MS = 1e4;
|
|
976
|
+
var MODEL_LISTING_MAX_BYTES = 1024 * 1024;
|
|
977
|
+
var MODEL_METADATA_MAX_BYTES = 16 * 1024 * 1024;
|
|
866
978
|
function builtinModels(baseURL) {
|
|
867
979
|
return new Map(getBuiltinModels("opencode-go").map((model) => [model.id, {
|
|
868
980
|
...model,
|
|
@@ -882,19 +994,27 @@ function readLiveModelIds(body) {
|
|
|
882
994
|
}
|
|
883
995
|
async function fetchLiveModelIds(baseURL) {
|
|
884
996
|
const url = `${baseURL.replace(/\/+$/, "")}/models`;
|
|
885
|
-
|
|
997
|
+
const endpoint = diagnosticURL(url);
|
|
998
|
+
let result;
|
|
886
999
|
try {
|
|
887
|
-
|
|
1000
|
+
result = await fetchJsonResponse(url, {
|
|
888
1001
|
method: "GET",
|
|
889
1002
|
cache: "no-cache",
|
|
890
|
-
headers: { accept: "application/json"
|
|
1003
|
+
headers: { ...attributionHeaders(), accept: "application/json" },
|
|
891
1004
|
signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
|
|
892
|
-
});
|
|
1005
|
+
}, MODEL_LISTING_MAX_BYTES);
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
const detail = error instanceof SyntaxError ? "invalid JSON response" : error instanceof RangeError ? `response exceeds the ${MODEL_LISTING_MAX_BYTES} byte limit` : transportFailure(error);
|
|
1008
|
+
const action = error instanceof SyntaxError || error instanceof RangeError ? "read" : "reach";
|
|
1009
|
+
throw new LlmError5(`could not ${action} ${endpoint}: ${detail}`, "DISCOVERY_FAILED", { cause: error });
|
|
1010
|
+
}
|
|
1011
|
+
const { response, body } = result;
|
|
1012
|
+
if (!response.ok) throw new LlmError5(`${endpoint} answered HTTP ${response.status}`, "DISCOVERY_FAILED");
|
|
1013
|
+
try {
|
|
1014
|
+
return readLiveModelIds(body);
|
|
893
1015
|
} catch (error) {
|
|
894
|
-
throw new LlmError5(
|
|
1016
|
+
throw new LlmError5(`${endpoint} returned an invalid model listing: expected a "data" array`, "DISCOVERY_FAILED", { cause: error });
|
|
895
1017
|
}
|
|
896
|
-
if (!response.ok) throw new LlmError5(`${url} answered ${response.status}`, "DISCOVERY_FAILED");
|
|
897
|
-
return readLiveModelIds(await response.json());
|
|
898
1018
|
}
|
|
899
1019
|
function harnessApiKeyAuth() {
|
|
900
1020
|
return { apiKey: {
|
|
@@ -941,18 +1061,18 @@ var OpencodeGoCatalog = class {
|
|
|
941
1061
|
}
|
|
942
1062
|
/** Conditional HTTP requests save bandwidth while still checking for updated metadata. */
|
|
943
1063
|
async refreshMetadata(builtin) {
|
|
944
|
-
const response = await
|
|
1064
|
+
const { response, body } = await fetchJsonResponse(MODEL_METADATA_URL, {
|
|
945
1065
|
headers: {
|
|
946
|
-
accept: "application/json",
|
|
947
1066
|
...attributionHeaders(),
|
|
1067
|
+
accept: "application/json",
|
|
948
1068
|
...this.metadataETag === void 0 ? {} : { "if-none-match": this.metadataETag }
|
|
949
1069
|
},
|
|
950
1070
|
cache: "no-cache",
|
|
951
1071
|
signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
|
|
952
|
-
});
|
|
1072
|
+
}, MODEL_METADATA_MAX_BYTES);
|
|
953
1073
|
if (response.status === 304 && this.metadata !== void 0) return this.metadata;
|
|
954
1074
|
if (!response.ok) throw new Error(`models.dev answered ${response.status}`);
|
|
955
|
-
const metadata = readModelMetadata(
|
|
1075
|
+
const metadata = readModelMetadata(body, this.baseURL, builtin);
|
|
956
1076
|
this.metadata = metadata;
|
|
957
1077
|
this.metadataETag = response.headers.get("etag") ?? void 0;
|
|
958
1078
|
return metadata;
|
|
@@ -978,7 +1098,8 @@ var OpencodeGoCatalog = class {
|
|
|
978
1098
|
unavailable: this.served?.unavailable ?? /* @__PURE__ */ new Map(),
|
|
979
1099
|
provider: buildProvider(this.baseURL, [...models2.values()]),
|
|
980
1100
|
live: false,
|
|
981
|
-
fetchedAtMs: Date.now()
|
|
1101
|
+
fetchedAtMs: Date.now(),
|
|
1102
|
+
listingFailure: listing.reason
|
|
982
1103
|
};
|
|
983
1104
|
}
|
|
984
1105
|
const models = /* @__PURE__ */ new Map();
|
|
@@ -987,7 +1108,7 @@ var OpencodeGoCatalog = class {
|
|
|
987
1108
|
const error = metadata?.errors.get(id);
|
|
988
1109
|
const model = known.get(id);
|
|
989
1110
|
if (error !== void 0 || model === void 0) {
|
|
990
|
-
unavailable.set(id, error ?? "
|
|
1111
|
+
unavailable.set(id, error ?? "no usable configuration was found for this OpenCode Go model");
|
|
991
1112
|
} else {
|
|
992
1113
|
models.set(id, model);
|
|
993
1114
|
}
|
|
@@ -1018,26 +1139,41 @@ var OpencodeGoCatalog = class {
|
|
|
1018
1139
|
};
|
|
1019
1140
|
async function discoverCatalogModels(catalog) {
|
|
1020
1141
|
const snapshot = await catalog.snapshot(true);
|
|
1021
|
-
|
|
1022
|
-
throw new LlmError5("llm-opencode-go: the live model listing is unreachable; try again later", "DISCOVERY_FAILED");
|
|
1023
|
-
}
|
|
1142
|
+
requireLiveListing(snapshot);
|
|
1024
1143
|
return describeCatalog(snapshot);
|
|
1025
1144
|
}
|
|
1145
|
+
function requireLiveListing(snapshot) {
|
|
1146
|
+
if (snapshot.live) return;
|
|
1147
|
+
throw listingError(snapshot);
|
|
1148
|
+
}
|
|
1149
|
+
function listingError(snapshot) {
|
|
1150
|
+
const detail = snapshot.listingFailure instanceof LlmError5 ? snapshot.listingFailure.message : "the live model listing is unreachable";
|
|
1151
|
+
return new LlmError5(`llm-opencode-go: ${detail}; refresh the model list to retry`, "DISCOVERY_FAILED", { cause: snapshot.listingFailure });
|
|
1152
|
+
}
|
|
1153
|
+
function describeConfiguredModels(snapshot) {
|
|
1154
|
+
return [...snapshot.models.values()].map((model) => ({
|
|
1155
|
+
id: model.id,
|
|
1156
|
+
name: model.name,
|
|
1157
|
+
contextWindow: model.contextWindow,
|
|
1158
|
+
maxTokens: model.maxTokens
|
|
1159
|
+
}));
|
|
1160
|
+
}
|
|
1026
1161
|
function describeCatalog(snapshot) {
|
|
1027
1162
|
return [
|
|
1028
|
-
...
|
|
1029
|
-
id: model.id,
|
|
1030
|
-
name: model.name,
|
|
1031
|
-
contextWindow: model.contextWindow,
|
|
1032
|
-
maxTokens: model.maxTokens
|
|
1033
|
-
})),
|
|
1163
|
+
...describeConfiguredModels(snapshot),
|
|
1034
1164
|
...[...snapshot.unavailable].map(([id, reason]) => ({ id, name: `${id} (metadata unavailable: ${reason})` }))
|
|
1035
1165
|
];
|
|
1036
1166
|
}
|
|
1037
1167
|
async function discoverSettingsModels(catalog) {
|
|
1038
1168
|
const snapshot = await catalog.snapshot(true);
|
|
1039
|
-
|
|
1040
|
-
|
|
1169
|
+
return {
|
|
1170
|
+
models: sortModels([
|
|
1171
|
+
...describeConfiguredModels(snapshot),
|
|
1172
|
+
...[...snapshot.unavailable.keys()].map((id) => ({ id, name: id, configurationMissing: true }))
|
|
1173
|
+
].map((model) => ({ ...model, ...snapshot.details.get(model.id) }))),
|
|
1174
|
+
stale: !snapshot.live,
|
|
1175
|
+
...snapshot.live ? {} : { error: listingError(snapshot).message }
|
|
1176
|
+
};
|
|
1041
1177
|
}
|
|
1042
1178
|
|
|
1043
1179
|
// src/config.ts
|
|
@@ -1048,7 +1184,7 @@ var DEFAULT_REFRESH_MINUTES = 60;
|
|
|
1048
1184
|
var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
1049
1185
|
var fields = {
|
|
1050
1186
|
enabled: z.boolean().default(true),
|
|
1051
|
-
|
|
1187
|
+
modelVisibility: z.dict(z.boolean().required()).default({}),
|
|
1052
1188
|
apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
1053
1189
|
baseURL: z.string().default(DEFAULT_BASE_URL),
|
|
1054
1190
|
refreshMinutes: z.number().step(1).min(1).max(7 * 24 * 60).default(DEFAULT_REFRESH_MINUTES),
|
|
@@ -1069,7 +1205,7 @@ var Config = z.object(Object.fromEntries(
|
|
|
1069
1205
|
Object.entries(fields).map(([key, schema]) => [key, schema.volatile()])
|
|
1070
1206
|
));
|
|
1071
1207
|
function readConfig(config) {
|
|
1072
|
-
return Object.fromEntries(Object.
|
|
1208
|
+
return Object.fromEntries(Object.keys(fields).map((key) => [key, config[key].get()]));
|
|
1073
1209
|
}
|
|
1074
1210
|
function assertBaseURL(raw) {
|
|
1075
1211
|
let url;
|
|
@@ -1088,6 +1224,7 @@ function assertBaseURL(raw) {
|
|
|
1088
1224
|
}
|
|
1089
1225
|
|
|
1090
1226
|
// src/adapter.ts
|
|
1227
|
+
var DISABLES_THINKING_WHEN_UNSET = /* @__PURE__ */ new Set(["deepseek", "zai", "qwen", "qwen-chat-template"]);
|
|
1091
1228
|
function withModelLimit(model, limits) {
|
|
1092
1229
|
const limit = limits[model.id];
|
|
1093
1230
|
if (limit == null) return model;
|
|
@@ -1143,8 +1280,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1143
1280
|
}
|
|
1144
1281
|
async listModels(_provider) {
|
|
1145
1282
|
const config = this.options.config();
|
|
1146
|
-
const snapshot = await this.catalogOf(config).snapshot(
|
|
1147
|
-
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) => ({
|
|
1148
1285
|
provider: PROVIDER_ID,
|
|
1149
1286
|
id: model.id,
|
|
1150
1287
|
name: model.name,
|
|
@@ -1165,11 +1302,14 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1165
1302
|
const reasoning = {};
|
|
1166
1303
|
const levels = model.reasoning ? getSupportedThinkingLevels(model) : [];
|
|
1167
1304
|
if (levels.length > 0) {
|
|
1305
|
+
const format = model.compat?.thinkingFormat;
|
|
1306
|
+
const fallback = format !== void 0 && DISABLES_THINKING_WHEN_UNSET.has(format) ? levels.includes("high") ? "high" : levels.findLast((level) => level !== "off") : void 0;
|
|
1168
1307
|
reasoning.reasoning = {
|
|
1169
1308
|
efforts: levels.map((level) => ({
|
|
1170
1309
|
id: ReasoningEffortId(level),
|
|
1171
1310
|
name: `${level.charAt(0).toUpperCase()}${level.slice(1)}`
|
|
1172
|
-
}))
|
|
1311
|
+
})),
|
|
1312
|
+
...fallback === void 0 ? {} : { defaultEffort: ReasoningEffortId(fallback) }
|
|
1173
1313
|
};
|
|
1174
1314
|
}
|
|
1175
1315
|
return {
|
|
@@ -1296,7 +1436,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1296
1436
|
};
|
|
1297
1437
|
|
|
1298
1438
|
// src/usage.ts
|
|
1299
|
-
import {
|
|
1439
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1440
|
+
import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
1300
1441
|
import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
|
|
1301
1442
|
|
|
1302
1443
|
// src/usage-contract.ts
|
|
@@ -1304,6 +1445,12 @@ function parseGoUsage(value) {
|
|
|
1304
1445
|
if (!value || typeof value !== "object") throw new Error("Invalid OpenCode Go usage response");
|
|
1305
1446
|
const source = value;
|
|
1306
1447
|
const result = {};
|
|
1448
|
+
if (source.source !== void 0) {
|
|
1449
|
+
if (typeof source.source !== "string" || source.source.length === 0 || source.source.length > 128) {
|
|
1450
|
+
throw new Error("Invalid OpenCode Go usage source");
|
|
1451
|
+
}
|
|
1452
|
+
result.source = source.source;
|
|
1453
|
+
}
|
|
1307
1454
|
for (const key of ["rolling", "weekly", "monthly"]) {
|
|
1308
1455
|
const row = source[key];
|
|
1309
1456
|
if (!row || row.status !== "ok" && row.status !== "rate-limited" || typeof row.percent !== "number" || !Number.isFinite(row.percent) || row.percent < 0 || typeof row.resetsAt !== "string" || !Number.isFinite(Date.parse(row.resetsAt))) {
|
|
@@ -1333,23 +1480,73 @@ var usageRemote = {
|
|
|
1333
1480
|
};
|
|
1334
1481
|
|
|
1335
1482
|
// src/usage.ts
|
|
1483
|
+
var USAGE_MAX_BYTES = 1024 * 1024;
|
|
1336
1484
|
var GoUsageService = class extends TypertRemoteService {
|
|
1337
1485
|
constructor(ctx, options) {
|
|
1338
1486
|
super(ctx, "opencodeGoUsage");
|
|
1339
1487
|
this.options = options;
|
|
1340
1488
|
}
|
|
1489
|
+
identity;
|
|
1341
1490
|
async read() {
|
|
1342
1491
|
const baseURL = assertBaseURL(this.options.baseURL()).replace(/\/$/, "");
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1492
|
+
let key;
|
|
1493
|
+
try {
|
|
1494
|
+
key = await this.options.resolveApiKey();
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
this.identity = void 0;
|
|
1497
|
+
const missing = error instanceof Error && "code" in error && error.code === "MISSING_CREDENTIAL";
|
|
1498
|
+
throw new RemoteError("opencode-go/usage-unavailable", missing ? "OpenCode Go API key is not configured" : "Could not resolve the OpenCode Go API key", {
|
|
1499
|
+
retryable: !missing,
|
|
1500
|
+
retainPrevious: false
|
|
1501
|
+
}, { cause: error });
|
|
1502
|
+
}
|
|
1503
|
+
if (!key) {
|
|
1504
|
+
this.identity = void 0;
|
|
1505
|
+
throw new RemoteError("opencode-go/usage-unavailable", "OpenCode Go API key is not configured", {
|
|
1506
|
+
retryable: false,
|
|
1507
|
+
retainPrevious: false
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
if (this.identity?.baseURL !== baseURL || this.identity.key !== key) {
|
|
1511
|
+
this.identity = { baseURL, key, source: randomUUID2() };
|
|
1512
|
+
}
|
|
1513
|
+
const { source } = this.identity;
|
|
1514
|
+
const endpoint = diagnosticURL(`${baseURL}/usage`);
|
|
1515
|
+
let result;
|
|
1516
|
+
try {
|
|
1517
|
+
result = await fetchJsonResponse(`${baseURL}/usage`, {
|
|
1518
|
+
headers: { ...attributionHeaders3(), Authorization: `Bearer ${key}`, Accept: "application/json" },
|
|
1519
|
+
signal: AbortSignal.timeout(1e4),
|
|
1520
|
+
redirect: "error"
|
|
1521
|
+
}, USAGE_MAX_BYTES);
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
const invalid = error instanceof SyntaxError || error instanceof RangeError;
|
|
1524
|
+
const detail = error instanceof SyntaxError ? "invalid JSON response" : error instanceof RangeError ? `response exceeds the ${USAGE_MAX_BYTES} byte limit` : transportFailure(error);
|
|
1525
|
+
throw new RemoteError("opencode-go/usage-unavailable", `Could not read ${endpoint}: ${detail}`, {
|
|
1526
|
+
retryable: true,
|
|
1527
|
+
retainPrevious: !invalid,
|
|
1528
|
+
source
|
|
1529
|
+
}, { cause: error });
|
|
1530
|
+
}
|
|
1531
|
+
const { response, body } = result;
|
|
1532
|
+
if (!response.ok) {
|
|
1533
|
+
const temporary = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
1534
|
+
throw new RemoteError("opencode-go/usage-unavailable", `OpenCode Go usage unavailable (HTTP ${response.status})`, {
|
|
1535
|
+
retryable: temporary,
|
|
1536
|
+
retainPrevious: temporary,
|
|
1537
|
+
source
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
try {
|
|
1541
|
+
const usage = parseGoUsage(body && typeof body === "object" ? body.usage : void 0);
|
|
1542
|
+
return { ...usage, source };
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
throw new RemoteError("opencode-go/usage-unavailable", "Invalid OpenCode Go usage response", {
|
|
1545
|
+
retryable: true,
|
|
1546
|
+
retainPrevious: false,
|
|
1547
|
+
source
|
|
1548
|
+
}, { cause: error });
|
|
1549
|
+
}
|
|
1353
1550
|
}
|
|
1354
1551
|
};
|
|
1355
1552
|
|
|
@@ -1360,8 +1557,12 @@ var GoModelsService = class extends TypertRemoteService2 {
|
|
|
1360
1557
|
super(ctx, "opencodeGoModels");
|
|
1361
1558
|
this.options = options;
|
|
1362
1559
|
}
|
|
1363
|
-
read() {
|
|
1364
|
-
|
|
1560
|
+
async read() {
|
|
1561
|
+
try {
|
|
1562
|
+
return await discoverSettingsModels(this.options.catalog());
|
|
1563
|
+
} finally {
|
|
1564
|
+
this.options.onRefresh?.();
|
|
1565
|
+
}
|
|
1365
1566
|
}
|
|
1366
1567
|
};
|
|
1367
1568
|
|
|
@@ -1430,9 +1631,15 @@ function apply(ctx, raw) {
|
|
|
1430
1631
|
ctx.logger.warn(`llm-opencode-go: unusable replay state on assistant history; sending provider-neutral content (${reason})`);
|
|
1431
1632
|
}
|
|
1432
1633
|
});
|
|
1433
|
-
ctx.plugin(GoModelsService, { catalog: () => adapter.catalogOf(current()) });
|
|
1434
|
-
let pickerVisibility = current().showDeprecatedModels;
|
|
1435
1634
|
let registration;
|
|
1635
|
+
ctx.plugin(GoModelsService, {
|
|
1636
|
+
catalog: () => adapter.catalogOf(current()),
|
|
1637
|
+
onRefresh: () => {
|
|
1638
|
+
registration?.replace([PROVIDER_ID]);
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
const pickerVisibilityOf = () => JSON.stringify(current().modelVisibility ?? {});
|
|
1642
|
+
let pickerVisibility = pickerVisibilityOf();
|
|
1436
1643
|
const applyRoute = (configured) => {
|
|
1437
1644
|
if (configured && current().enabled && registration === void 0) {
|
|
1438
1645
|
try {
|
|
@@ -1449,8 +1656,9 @@ function apply(ctx, raw) {
|
|
|
1449
1656
|
}
|
|
1450
1657
|
};
|
|
1451
1658
|
const syncRoute = () => {
|
|
1452
|
-
|
|
1453
|
-
|
|
1659
|
+
const visibility = pickerVisibilityOf();
|
|
1660
|
+
if (pickerVisibility !== visibility) {
|
|
1661
|
+
pickerVisibility = visibility;
|
|
1454
1662
|
registration?.replace([PROVIDER_ID]);
|
|
1455
1663
|
}
|
|
1456
1664
|
const credentials = ctx.get("credentials");
|
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;
|