dsh-opencode-go 0.1.15 → 0.1.17
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 +37 -11
- package/README.md +37 -11
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/docs/development.md +74 -0
- package/docs/independent-package.md +3 -1
- package/docs/verification.md +118 -0
- package/examples/headless.patch.yml +1 -1
- package/lib/client.js +378 -286
- package/lib/index.js +228 -70
- package/lib/types/adapter.d.ts +2 -0
- package/lib/types/catalog.d.ts +14 -4
- package/lib/types/client/ModelEditor.d.ts +4 -1
- package/lib/types/client/Section.d.ts +6 -5
- package/lib/types/client/locales.d.ts +11 -2
- package/lib/types/config.d.ts +1 -1
- package/lib/types/conversion/context.d.ts +1 -1
- package/lib/types/conversion/replay.d.ts +6 -2
- package/lib/types/conversion/stream.d.ts +3 -2
- package/lib/types/index.d.ts +2 -2
- package/lib/types/json-response.d.ts +6 -3
- package/lib/types/metadata-cache.d.ts +12 -0
- package/lib/types/model-metadata.d.ts +4 -2
- package/lib/types/models-contract.d.ts +15 -0
- package/lib/types/provider-identity.d.ts +4 -0
- package/package.json +27 -18
package/lib/index.js
CHANGED
|
@@ -50,8 +50,8 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
50
50
|
import { LlmError as LlmError7, assertUsableApiKey, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
|
|
51
51
|
|
|
52
52
|
// src/adapter.ts
|
|
53
|
-
import { randomUUID } from "node:crypto";
|
|
54
|
-
import { getSupportedThinkingLevels } from "
|
|
53
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
54
|
+
import { getSupportedThinkingLevels, normalizeContext } from "opencode-go-pi-ai";
|
|
55
55
|
import {
|
|
56
56
|
LlmAdapter,
|
|
57
57
|
LlmError as LlmError6,
|
|
@@ -86,13 +86,14 @@ function emptyPiUsage() {
|
|
|
86
86
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
-
function toPiReplayState(message, requestedModel = message.model) {
|
|
89
|
+
function toPiReplayState(message, requestedModel = message.model, requestedProvider = message.provider) {
|
|
90
90
|
const responseModel = message.api === "anthropic-messages" && message.model !== requestedModel ? message.model : message.responseModel;
|
|
91
91
|
const response = {
|
|
92
92
|
kind: "pi-ai",
|
|
93
93
|
version: 2,
|
|
94
94
|
api: message.api,
|
|
95
|
-
provider:
|
|
95
|
+
provider: requestedProvider,
|
|
96
|
+
...requestedProvider === message.provider ? {} : { sdkProvider: message.provider },
|
|
96
97
|
model: requestedModel,
|
|
97
98
|
...responseModel === void 0 ? {} : { responseModel },
|
|
98
99
|
...message.responseId === void 0 ? {} : { responseId: message.responseId },
|
|
@@ -137,6 +138,9 @@ function readReplayState(value) {
|
|
|
137
138
|
for (const key of ["api", "provider", "model"]) {
|
|
138
139
|
if (typeof response[key] !== "string" || response[key].length === 0) return invalidReplay(`${key} must be a non-empty string`);
|
|
139
140
|
}
|
|
141
|
+
if (response["sdkProvider"] !== void 0 && (typeof response["sdkProvider"] !== "string" || response["sdkProvider"].length === 0)) {
|
|
142
|
+
return invalidReplay("sdkProvider must be a non-empty string");
|
|
143
|
+
}
|
|
140
144
|
if (!["stop", "length", "toolUse", "error", "aborted"].includes(String(response["stopReason"]))) {
|
|
141
145
|
return invalidReplay("unknown stopReason");
|
|
142
146
|
}
|
|
@@ -236,7 +240,7 @@ function replayedAssistant(message, source, rawState) {
|
|
|
236
240
|
role: "assistant",
|
|
237
241
|
content,
|
|
238
242
|
api: state.response.api,
|
|
239
|
-
provider: state.response.provider,
|
|
243
|
+
provider: state.response.sdkProvider ?? state.response.provider,
|
|
240
244
|
// Anthropic reports aliases and fallbacks as model, unlike Completions' informational responseModel.
|
|
241
245
|
model: state.response.api === "anthropic-messages" ? state.response.responseModel ?? state.response.model : state.response.model,
|
|
242
246
|
...state.response.responseModel === void 0 ? {} : { responseModel: state.response.responseModel },
|
|
@@ -551,7 +555,7 @@ async function toPiContextWithImages(options, images, onReplayDegrade) {
|
|
|
551
555
|
// src/conversion/stream.ts
|
|
552
556
|
import { brandString as brandString2 } from "@deepseek-ai/dsh-brand";
|
|
553
557
|
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError as LlmError4, QUOTA_EXCEEDED_CODE } from "@deepseek-ai/dsh-llm";
|
|
554
|
-
import { isContextOverflow } from "
|
|
558
|
+
import { isContextOverflow } from "opencode-go-pi-ai";
|
|
555
559
|
function mapUsage(usage) {
|
|
556
560
|
return {
|
|
557
561
|
inputTokens: usage.input,
|
|
@@ -624,7 +628,7 @@ function mapStopReason(message, contextWindow) {
|
|
|
624
628
|
}
|
|
625
629
|
}
|
|
626
630
|
}
|
|
627
|
-
async function* toStreamChunks(events, contextWindow, callerSignal, requestedModel) {
|
|
631
|
+
async function* toStreamChunks(events, contextWindow, callerSignal, requestedModel, requestedProvider) {
|
|
628
632
|
const toolIds = /* @__PURE__ */ new Map();
|
|
629
633
|
for await (const event of events) {
|
|
630
634
|
switch (event.type) {
|
|
@@ -686,7 +690,7 @@ async function* toStreamChunks(events, contextWindow, callerSignal, requestedMod
|
|
|
686
690
|
yield {
|
|
687
691
|
type: "finish",
|
|
688
692
|
reason: mapStopReason(event.message, contextWindow),
|
|
689
|
-
replayState: toPiReplayState(event.message, requestedModel)
|
|
693
|
+
replayState: toPiReplayState(event.message, requestedModel, requestedProvider)
|
|
690
694
|
};
|
|
691
695
|
return;
|
|
692
696
|
case "error":
|
|
@@ -708,14 +712,21 @@ async function* toStreamChunks(events, contextWindow, callerSignal, requestedMod
|
|
|
708
712
|
import { idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
|
|
709
713
|
|
|
710
714
|
// src/catalog.ts
|
|
711
|
-
import { createProvider } from "
|
|
712
|
-
import { getBuiltinModels } from "
|
|
713
|
-
import { anthropicMessagesApi } from "
|
|
714
|
-
import { openAICompletionsApi } from "
|
|
715
|
-
import { openAIResponsesApi } from "
|
|
715
|
+
import { createProvider } from "opencode-go-pi-ai";
|
|
716
|
+
import { getBuiltinModels } from "opencode-go-pi-ai/providers/all";
|
|
717
|
+
import { anthropicMessagesApi } from "opencode-go-pi-ai/api/anthropic-messages.lazy";
|
|
718
|
+
import { openAICompletionsApi } from "opencode-go-pi-ai/api/openai-completions.lazy";
|
|
719
|
+
import { openAIResponsesApi } from "opencode-go-pi-ai/api/openai-responses.lazy";
|
|
716
720
|
import { attributionHeaders, LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
|
|
717
721
|
|
|
718
722
|
// src/models-contract.ts
|
|
723
|
+
var INPUT_MODALITIES = ["text", "image", "audio", "video", "pdf"];
|
|
724
|
+
function normalizeInputModalities(value) {
|
|
725
|
+
if (!Array.isArray(value)) return void 0;
|
|
726
|
+
const declared = new Set(value.filter((item) => typeof item === "string").map((item) => item.toLowerCase()));
|
|
727
|
+
const ordered = INPUT_MODALITIES.filter((modality) => declared.has(modality));
|
|
728
|
+
return ordered.length === 0 ? void 0 : ordered;
|
|
729
|
+
}
|
|
719
730
|
function isModelEnabled(model, modelVisibility) {
|
|
720
731
|
if (model.configurationMissing) return false;
|
|
721
732
|
const enabled = modelVisibility && Object.hasOwn(modelVisibility, model.id) ? modelVisibility[model.id] : void 0;
|
|
@@ -747,6 +758,8 @@ function parseGoModels(value) {
|
|
|
747
758
|
if (typeof row.deprecated === "boolean") model.deprecated = row.deprecated;
|
|
748
759
|
if (typeof row.configurationMissing === "boolean") model.configurationMissing = row.configurationMissing;
|
|
749
760
|
if (validReleaseDate(row.releaseDate)) model.releaseDate = row.releaseDate;
|
|
761
|
+
const modalities = normalizeInputModalities(row.inputModalities);
|
|
762
|
+
if (modalities !== void 0) model.inputModalities = modalities;
|
|
750
763
|
return model;
|
|
751
764
|
});
|
|
752
765
|
}
|
|
@@ -852,9 +865,11 @@ function readModelMetadata(body, baseURL, builtin) {
|
|
|
852
865
|
const details = /* @__PURE__ */ new Map();
|
|
853
866
|
for (const [id, value] of Object.entries(entries)) {
|
|
854
867
|
const data = record(value);
|
|
868
|
+
const inputModalities = normalizeInputModalities(record(data.modalities).input);
|
|
855
869
|
details.set(id, {
|
|
856
870
|
deprecated: data.status === "deprecated",
|
|
857
|
-
...validReleaseDate(data.release_date) ? { releaseDate: data.release_date } : {}
|
|
871
|
+
...validReleaseDate(data.release_date) ? { releaseDate: data.release_date } : {},
|
|
872
|
+
...inputModalities === void 0 ? {} : { inputModalities }
|
|
858
873
|
});
|
|
859
874
|
try {
|
|
860
875
|
const metadata = record(value);
|
|
@@ -906,6 +921,9 @@ function readModelMetadata(body, baseURL, builtin) {
|
|
|
906
921
|
|
|
907
922
|
// src/json-response.ts
|
|
908
923
|
import { setTimeout as delay } from "node:timers/promises";
|
|
924
|
+
import { Readable, Writable } from "node:stream";
|
|
925
|
+
import { pipeline } from "node:stream/promises";
|
|
926
|
+
import { createGunzip } from "node:zlib";
|
|
909
927
|
var RETRYABLE_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "EPIPE", "UND_ERR_SOCKET", "EAI_AGAIN"]);
|
|
910
928
|
function errorCauses(error) {
|
|
911
929
|
const causes = [];
|
|
@@ -943,10 +961,10 @@ function diagnosticURL(raw) {
|
|
|
943
961
|
url.hash = "";
|
|
944
962
|
return url.href;
|
|
945
963
|
}
|
|
946
|
-
async function fetchJsonResponse(url, init, maxBytes) {
|
|
964
|
+
async function fetchJsonResponse(url, init, maxBytes, encoding = "identity") {
|
|
947
965
|
if ((init.method ?? "GET").toUpperCase() !== "GET") throw new TypeError("JSON requests must use GET");
|
|
948
966
|
const headers = new Headers(init.headers);
|
|
949
|
-
headers.set("accept-encoding",
|
|
967
|
+
headers.set("accept-encoding", encoding);
|
|
950
968
|
const options = { ...init, method: "GET", headers };
|
|
951
969
|
for (let attempt = 0; ; attempt += 1) {
|
|
952
970
|
init.signal?.throwIfAborted();
|
|
@@ -957,7 +975,7 @@ async function fetchJsonResponse(url, init, maxBytes) {
|
|
|
957
975
|
});
|
|
958
976
|
return { response, body: void 0 };
|
|
959
977
|
}
|
|
960
|
-
return { response, body: await readJsonResponse(response, maxBytes) };
|
|
978
|
+
return { response, body: await readJsonResponse(response, maxBytes, { gzip: encoding === "gzip", signal: init.signal }) };
|
|
961
979
|
} catch (error) {
|
|
962
980
|
init.signal?.throwIfAborted();
|
|
963
981
|
if (attempt > 0 || !retryableTransportFailure(error)) throw error;
|
|
@@ -987,8 +1005,33 @@ async function readBoundedBody(response, maxBytes) {
|
|
|
987
1005
|
reader.releaseLock();
|
|
988
1006
|
}
|
|
989
1007
|
}
|
|
990
|
-
async function
|
|
991
|
-
const
|
|
1008
|
+
async function readGzipBody(bytes, maxBytes, signal) {
|
|
1009
|
+
const chunks = [];
|
|
1010
|
+
let size = 0;
|
|
1011
|
+
await pipeline(Readable.from([bytes]), createGunzip(), new Writable({
|
|
1012
|
+
write(chunk, _encoding, done) {
|
|
1013
|
+
if (chunk.length > maxBytes - size) {
|
|
1014
|
+
done(new RangeError(`Response body exceeds ${maxBytes} byte limit`));
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
size += chunk.length;
|
|
1018
|
+
chunks.push(chunk);
|
|
1019
|
+
done();
|
|
1020
|
+
}
|
|
1021
|
+
}), { signal: signal ?? void 0 });
|
|
1022
|
+
return Buffer.concat(chunks, size);
|
|
1023
|
+
}
|
|
1024
|
+
async function readJsonResponse(response, maxBytes, options = {}) {
|
|
1025
|
+
let bytes = await readBoundedBody(response, maxBytes);
|
|
1026
|
+
if (options.gzip && bytes[0] === 31 && bytes[1] === 139) {
|
|
1027
|
+
try {
|
|
1028
|
+
bytes = await readGzipBody(bytes, maxBytes, options.signal);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
options.signal?.throwIfAborted();
|
|
1031
|
+
if (error instanceof RangeError) throw error;
|
|
1032
|
+
throw new SyntaxError("Response is not valid gzip", { cause: error });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
992
1035
|
try {
|
|
993
1036
|
return JSON.parse(new TextDecoder().decode(bytes));
|
|
994
1037
|
} catch (error) {
|
|
@@ -996,27 +1039,82 @@ async function readJsonResponse(response, maxBytes) {
|
|
|
996
1039
|
}
|
|
997
1040
|
}
|
|
998
1041
|
|
|
1042
|
+
// src/metadata-cache.ts
|
|
1043
|
+
import { randomUUID } from "node:crypto";
|
|
1044
|
+
import { createReadStream } from "node:fs";
|
|
1045
|
+
import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
1046
|
+
import { homedir } from "node:os";
|
|
1047
|
+
import { dirname, join, resolve } from "node:path";
|
|
1048
|
+
var MODEL_METADATA_MAX_BYTES = 16 * 1024 * 1024;
|
|
1049
|
+
var CACHE_MAX_BYTES = MODEL_METADATA_MAX_BYTES + 4096;
|
|
1050
|
+
function metadataCachePath() {
|
|
1051
|
+
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
1052
|
+
return resolve(home, "cache", "dsh-opencode-go", "models.dev.api.json");
|
|
1053
|
+
}
|
|
1054
|
+
function metadataETag(value) {
|
|
1055
|
+
return typeof value === "string" && value.length <= 1024 && /^(W\/)?"[\x21\x23-\x7e\x80-\xff]*"$/.test(value) ? value : void 0;
|
|
1056
|
+
}
|
|
1057
|
+
async function readMetadataCache(path) {
|
|
1058
|
+
try {
|
|
1059
|
+
const info = await stat(path);
|
|
1060
|
+
if (!info.isFile() || info.size > CACHE_MAX_BYTES) return void 0;
|
|
1061
|
+
const chunks = [];
|
|
1062
|
+
let size = 0;
|
|
1063
|
+
for await (const chunk of createReadStream(path)) {
|
|
1064
|
+
const bytes = chunk;
|
|
1065
|
+
size += bytes.length;
|
|
1066
|
+
if (size > CACHE_MAX_BYTES) return void 0;
|
|
1067
|
+
chunks.push(bytes);
|
|
1068
|
+
}
|
|
1069
|
+
const value = JSON.parse(Buffer.concat(chunks, size).toString("utf8"));
|
|
1070
|
+
if (!value || value.version !== 1 || value.sourceURL !== MODEL_METADATA_URL || typeof value.savedAtMs !== "number" || !Number.isSafeInteger(value.savedAtMs) || value.savedAtMs < 0 || value.savedAtMs > Date.now() || value.etag !== void 0 && metadataETag(value.etag) === void 0) return void 0;
|
|
1071
|
+
return { body: value.body, savedAtMs: value.savedAtMs, etag: metadataETag(value.etag) };
|
|
1072
|
+
} catch {
|
|
1073
|
+
return void 0;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
async function writeMetadataCache(path, value) {
|
|
1077
|
+
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
1078
|
+
try {
|
|
1079
|
+
const content = JSON.stringify({ version: 1, sourceURL: MODEL_METADATA_URL, ...value });
|
|
1080
|
+
if (Buffer.byteLength(content) > CACHE_MAX_BYTES) return;
|
|
1081
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1082
|
+
await writeFile(temporary, content, { flag: "wx", mode: 384 });
|
|
1083
|
+
await rename(temporary, path);
|
|
1084
|
+
} catch {
|
|
1085
|
+
} finally {
|
|
1086
|
+
await rm(temporary, { force: true }).catch(() => {
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// src/provider-identity.ts
|
|
1092
|
+
var PROVIDER_ID = "dsh-opencode-go";
|
|
1093
|
+
var DISPLAY_NAME = "DSH OpenCode Go";
|
|
1094
|
+
var SDK_PROVIDER_ID = "opencode-go";
|
|
1095
|
+
|
|
999
1096
|
// src/catalog.ts
|
|
1000
|
-
var PROVIDER_ID = "opencode-go";
|
|
1001
|
-
var DISPLAY_NAME = "OpenCode Go";
|
|
1002
1097
|
var DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
1003
1098
|
var MODELS_FETCH_TIMEOUT_MS = 1e4;
|
|
1004
1099
|
var METADATA_FETCH_TIMEOUT_MS = 3e4;
|
|
1005
1100
|
var STALE_WHILE_REVALIDATE_MS = 5 * 6e4;
|
|
1006
1101
|
var MODEL_LISTING_MAX_BYTES = 1024 * 1024;
|
|
1007
|
-
var MODEL_METADATA_MAX_BYTES = 16 * 1024 * 1024;
|
|
1008
1102
|
var RETRY_MIN_MS = 5e3;
|
|
1009
1103
|
var RETRY_MAX_MS = 6e4;
|
|
1104
|
+
async function settled(promise) {
|
|
1105
|
+
const [result] = await Promise.allSettled([promise]);
|
|
1106
|
+
return result;
|
|
1107
|
+
}
|
|
1010
1108
|
function waitForSnapshot(pending, signal) {
|
|
1011
1109
|
if (signal === void 0) return pending;
|
|
1012
|
-
return new Promise((
|
|
1110
|
+
return new Promise((resolve2, reject) => {
|
|
1013
1111
|
const abort = () => {
|
|
1014
1112
|
signal.removeEventListener("abort", abort);
|
|
1015
1113
|
reject(new LlmError5("opencode-go catalog request aborted by caller", "ABORTED", { cause: signal.reason }));
|
|
1016
1114
|
};
|
|
1017
1115
|
pending.then((value) => {
|
|
1018
1116
|
signal.removeEventListener("abort", abort);
|
|
1019
|
-
|
|
1117
|
+
resolve2(value);
|
|
1020
1118
|
}, (error) => {
|
|
1021
1119
|
signal.removeEventListener("abort", abort);
|
|
1022
1120
|
reject(error);
|
|
@@ -1026,9 +1124,9 @@ function waitForSnapshot(pending, signal) {
|
|
|
1026
1124
|
});
|
|
1027
1125
|
}
|
|
1028
1126
|
function builtinModels(baseURL) {
|
|
1029
|
-
return new Map(getBuiltinModels(
|
|
1127
|
+
return new Map(getBuiltinModels(SDK_PROVIDER_ID).map((model) => [model.id, {
|
|
1030
1128
|
...model,
|
|
1031
|
-
provider:
|
|
1129
|
+
provider: SDK_PROVIDER_ID,
|
|
1032
1130
|
baseUrl: modelBaseURL(model.api, baseURL)
|
|
1033
1131
|
}]));
|
|
1034
1132
|
}
|
|
@@ -1074,7 +1172,7 @@ function harnessApiKeyAuth() {
|
|
|
1074
1172
|
}
|
|
1075
1173
|
function buildProvider(baseURL, models) {
|
|
1076
1174
|
return createProvider({
|
|
1077
|
-
id:
|
|
1175
|
+
id: SDK_PROVIDER_ID,
|
|
1078
1176
|
name: DISPLAY_NAME,
|
|
1079
1177
|
baseUrl: baseURL,
|
|
1080
1178
|
auth: harnessApiKeyAuth(),
|
|
@@ -1087,15 +1185,22 @@ function buildProvider(baseURL, models) {
|
|
|
1087
1185
|
});
|
|
1088
1186
|
}
|
|
1089
1187
|
var OpencodeGoCatalog = class {
|
|
1090
|
-
constructor(baseURL, refreshMs, onFallback, onOmitted) {
|
|
1188
|
+
constructor(baseURL, refreshMs, onFallback, onOmitted, onRefresh = () => {
|
|
1189
|
+
}) {
|
|
1091
1190
|
this.baseURL = baseURL;
|
|
1092
1191
|
this.refreshMs = refreshMs;
|
|
1093
1192
|
this.onFallback = onFallback;
|
|
1094
1193
|
this.onOmitted = onOmitted;
|
|
1194
|
+
this.onRefresh = onRefresh;
|
|
1095
1195
|
}
|
|
1096
1196
|
served;
|
|
1097
1197
|
pending;
|
|
1198
|
+
/** A cold runtime read can use disk metadata while the shared online refresh runs. */
|
|
1199
|
+
cachedPending;
|
|
1200
|
+
initialized;
|
|
1201
|
+
cachePath = metadataCachePath();
|
|
1098
1202
|
metadata;
|
|
1203
|
+
metadataBody;
|
|
1099
1204
|
metadataETag;
|
|
1100
1205
|
metadataUpdatedAtMs;
|
|
1101
1206
|
failures = 0;
|
|
@@ -1104,19 +1209,63 @@ var OpencodeGoCatalog = class {
|
|
|
1104
1209
|
if (signal?.aborted) {
|
|
1105
1210
|
return Promise.reject(new LlmError5("opencode-go catalog request aborted by caller", "ABORTED", { cause: signal.reason }));
|
|
1106
1211
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1212
|
+
return waitForSnapshot(this.served === void 0 ? this.readSnapshot(force) : this.currentSnapshot(force), signal);
|
|
1213
|
+
}
|
|
1214
|
+
async restoreMetadata() {
|
|
1215
|
+
const saved = await readMetadataCache(this.cachePath);
|
|
1216
|
+
if (saved === void 0) return;
|
|
1217
|
+
try {
|
|
1218
|
+
this.metadata = readModelMetadata(saved.body, this.baseURL, builtinModels(this.baseURL));
|
|
1219
|
+
this.metadataBody = saved.body;
|
|
1220
|
+
this.metadataETag = saved.etag;
|
|
1221
|
+
this.metadataUpdatedAtMs = saved.savedAtMs;
|
|
1222
|
+
} catch {
|
|
1109
1223
|
}
|
|
1110
|
-
|
|
1224
|
+
}
|
|
1225
|
+
async readSnapshot(force) {
|
|
1226
|
+
await (this.initialized ??= this.restoreMetadata());
|
|
1227
|
+
return this.currentSnapshot(force);
|
|
1228
|
+
}
|
|
1229
|
+
currentSnapshot(force) {
|
|
1230
|
+
if (!force && this.served !== void 0 && Date.now() < this.refreshAtMs) return Promise.resolve(this.served);
|
|
1231
|
+
if (this.pending === void 0) this.startRefresh(force);
|
|
1232
|
+
return !force && this.cachedPending !== void 0 ? this.cachedPending : this.pending;
|
|
1233
|
+
}
|
|
1234
|
+
startRefresh(force) {
|
|
1235
|
+
const builtin = builtinModels(this.baseURL);
|
|
1236
|
+
const listing = settled(fetchLiveModelIds(this.baseURL).then((ids) => ({ ids, updatedAtMs: Date.now() })));
|
|
1237
|
+
const restored = this.metadata;
|
|
1238
|
+
if (!force && this.served === void 0 && restored !== void 0) {
|
|
1239
|
+
const status = { metadataLive: false, metadataUpdatedAtMs: this.metadataUpdatedAtMs };
|
|
1240
|
+
this.cachedPending = listing.then((result) => {
|
|
1241
|
+
const snapshot = this.build(builtin, result, restored, status);
|
|
1242
|
+
this.served = snapshot;
|
|
1243
|
+
return snapshot;
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
const notify = this.cachedPending !== void 0 || !force && this.served !== void 0;
|
|
1247
|
+
this.pending = Promise.all([listing, settled(this.refreshMetadata(builtin)), this.cachedPending]).then(([listing2, result]) => this.build(
|
|
1248
|
+
builtin,
|
|
1249
|
+
listing2,
|
|
1250
|
+
result.status === "fulfilled" ? result.value : this.metadata,
|
|
1251
|
+
{
|
|
1252
|
+
metadataLive: result.status === "fulfilled",
|
|
1253
|
+
...this.metadataUpdatedAtMs === void 0 ? {} : { metadataUpdatedAtMs: this.metadataUpdatedAtMs },
|
|
1254
|
+
...result.status === "rejected" ? { metadataFailure: result.reason } : {}
|
|
1255
|
+
}
|
|
1256
|
+
)).then((snapshot) => {
|
|
1111
1257
|
this.served = snapshot;
|
|
1112
1258
|
this.failures = snapshot.live && snapshot.metadataLive ? 0 : Math.min(this.failures + 1, 5);
|
|
1113
1259
|
const lifetime = this.failures === 0 ? this.refreshMs : Math.min(this.refreshMs, RETRY_MIN_MS * 2 ** (this.failures - 1), RETRY_MAX_MS);
|
|
1114
1260
|
this.refreshAtMs = snapshot.fetchedAtMs + lifetime;
|
|
1261
|
+
if (notify) this.onRefresh();
|
|
1115
1262
|
return snapshot;
|
|
1116
1263
|
}).finally(() => {
|
|
1117
1264
|
this.pending = void 0;
|
|
1265
|
+
this.cachedPending = void 0;
|
|
1266
|
+
});
|
|
1267
|
+
void this.pending.catch(() => {
|
|
1118
1268
|
});
|
|
1119
|
-
return waitForSnapshot(this.pending, signal);
|
|
1120
1269
|
}
|
|
1121
1270
|
/** Conditional HTTP requests save bandwidth while still checking for updated metadata. */
|
|
1122
1271
|
async refreshMetadata(builtin) {
|
|
@@ -1128,9 +1277,11 @@ var OpencodeGoCatalog = class {
|
|
|
1128
1277
|
},
|
|
1129
1278
|
cache: "no-cache",
|
|
1130
1279
|
signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS)
|
|
1131
|
-
}, MODEL_METADATA_MAX_BYTES);
|
|
1280
|
+
}, MODEL_METADATA_MAX_BYTES, "gzip");
|
|
1132
1281
|
if (response.status === 304 && this.metadata !== void 0) {
|
|
1133
1282
|
this.metadataUpdatedAtMs = Date.now();
|
|
1283
|
+
this.metadataETag = metadataETag(response.headers.get("etag")) ?? this.metadataETag;
|
|
1284
|
+
await this.persistMetadata();
|
|
1134
1285
|
return this.metadata;
|
|
1135
1286
|
}
|
|
1136
1287
|
if (!response.ok) throw new LlmError5(`${MODEL_METADATA_URL} answered HTTP ${response.status}`, "DISCOVERY_FAILED");
|
|
@@ -1141,26 +1292,24 @@ var OpencodeGoCatalog = class {
|
|
|
1141
1292
|
throw new LlmError5(`${MODEL_METADATA_URL} returned invalid model configuration`, "DISCOVERY_FAILED", { cause: error });
|
|
1142
1293
|
}
|
|
1143
1294
|
this.metadata = metadata;
|
|
1144
|
-
this.
|
|
1295
|
+
this.metadataBody = body;
|
|
1296
|
+
this.metadataETag = metadataETag(response.headers.get("etag"));
|
|
1145
1297
|
this.metadataUpdatedAtMs = Date.now();
|
|
1298
|
+
await this.persistMetadata();
|
|
1146
1299
|
return metadata;
|
|
1147
1300
|
}
|
|
1301
|
+
async persistMetadata() {
|
|
1302
|
+
await writeMetadataCache(this.cachePath, {
|
|
1303
|
+
body: this.metadataBody,
|
|
1304
|
+
etag: this.metadataETag,
|
|
1305
|
+
savedAtMs: this.metadataUpdatedAtMs
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1148
1308
|
/** Gateway ids decide membership; online metadata decides how to call each model. */
|
|
1149
|
-
|
|
1150
|
-
const builtin = builtinModels(this.baseURL);
|
|
1151
|
-
const [listing, metadataResult] = await Promise.allSettled([
|
|
1152
|
-
fetchLiveModelIds(this.baseURL).then((ids) => ({ ids, updatedAtMs: Date.now() })),
|
|
1153
|
-
this.refreshMetadata(builtin)
|
|
1154
|
-
]);
|
|
1155
|
-
const metadata = metadataResult.status === "fulfilled" ? metadataResult.value : this.metadata;
|
|
1156
|
-
const metadataStatus = {
|
|
1157
|
-
metadataLive: metadataResult.status === "fulfilled",
|
|
1158
|
-
...this.metadataUpdatedAtMs === void 0 ? {} : { metadataUpdatedAtMs: this.metadataUpdatedAtMs },
|
|
1159
|
-
...metadataResult.status === "rejected" ? { metadataFailure: metadataResult.reason } : {}
|
|
1160
|
-
};
|
|
1309
|
+
build(builtin, listing, metadata, metadataStatus) {
|
|
1161
1310
|
const known = new Map([...builtin, ...this.served?.models ?? [], ...metadata?.models ?? []]);
|
|
1162
|
-
if (
|
|
1163
|
-
this.onFallback({ url: MODEL_METADATA_URL, error:
|
|
1311
|
+
if (metadataStatus.metadataFailure !== void 0) {
|
|
1312
|
+
this.onFallback({ url: MODEL_METADATA_URL, error: metadataStatus.metadataFailure, kept: known.size });
|
|
1164
1313
|
}
|
|
1165
1314
|
if (listing.status === "rejected") {
|
|
1166
1315
|
const models2 = this.served?.models ?? /* @__PURE__ */ new Map();
|
|
@@ -1188,7 +1337,9 @@ var OpencodeGoCatalog = class {
|
|
|
1188
1337
|
models.set(id, model);
|
|
1189
1338
|
}
|
|
1190
1339
|
}
|
|
1191
|
-
if (unavailable.size > 0
|
|
1340
|
+
if (unavailable.size > 0 && (metadataStatus.metadataLive || metadataStatus.metadataFailure !== void 0)) {
|
|
1341
|
+
this.onOmitted([...unavailable.keys()]);
|
|
1342
|
+
}
|
|
1192
1343
|
return {
|
|
1193
1344
|
details: new Map(listing.value.ids.map((id) => [id, metadata?.details.get(id) ?? {}])),
|
|
1194
1345
|
models,
|
|
@@ -1210,7 +1361,9 @@ var OpencodeGoCatalog = class {
|
|
|
1210
1361
|
return cached;
|
|
1211
1362
|
}
|
|
1212
1363
|
let snapshot = await this.snapshot(false, signal);
|
|
1213
|
-
if (!snapshot.models.has(id) && snapshot === cached
|
|
1364
|
+
if (!snapshot.models.has(id) && (snapshot === cached || this.cachedPending !== void 0)) {
|
|
1365
|
+
snapshot = await this.snapshot(true, signal);
|
|
1366
|
+
}
|
|
1214
1367
|
if (signal?.aborted) throw new LlmError5("opencode-go catalog request aborted by caller", "ABORTED", { cause: signal.reason });
|
|
1215
1368
|
if (snapshot.unavailable.has(id)) {
|
|
1216
1369
|
throw new LlmError5(
|
|
@@ -1336,7 +1489,7 @@ function withModelLimit(model, limits) {
|
|
|
1336
1489
|
};
|
|
1337
1490
|
}
|
|
1338
1491
|
function opencodeSessionValue(sessionId) {
|
|
1339
|
-
return sessionId !== void 0 && sessionId.length > 0 ? sessionId :
|
|
1492
|
+
return sessionId !== void 0 && sessionId.length > 0 ? sessionId : randomUUID2();
|
|
1340
1493
|
}
|
|
1341
1494
|
var OpencodeGoAdapter = class extends LlmAdapter {
|
|
1342
1495
|
constructor(options) {
|
|
@@ -1360,19 +1513,20 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1360
1513
|
catalogOf(config) {
|
|
1361
1514
|
const key = `${config.baseURL}|${String(config.refreshMinutes)}`;
|
|
1362
1515
|
if (this.catalogCache?.key !== key) {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1516
|
+
const catalog = new OpencodeGoCatalog(
|
|
1517
|
+
assertBaseURL(config.baseURL),
|
|
1518
|
+
config.refreshMinutes * 6e4,
|
|
1519
|
+
/* v8 ignore next -- the plugin always passes both observers; the defaults exist for direct construction */
|
|
1520
|
+
this.options.onFallback ?? (() => {
|
|
1521
|
+
}),
|
|
1522
|
+
/* v8 ignore next -- the plugin always passes both observers; the defaults exist for direct construction */
|
|
1523
|
+
this.options.onOmitted ?? (() => {
|
|
1524
|
+
}),
|
|
1525
|
+
() => {
|
|
1526
|
+
if (this.catalogCache?.catalog === catalog) this.options.onCatalogRefresh?.();
|
|
1527
|
+
}
|
|
1528
|
+
);
|
|
1529
|
+
this.catalogCache = { key, catalog };
|
|
1376
1530
|
}
|
|
1377
1531
|
return this.catalogCache.catalog;
|
|
1378
1532
|
}
|
|
@@ -1489,7 +1643,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1489
1643
|
};
|
|
1490
1644
|
}
|
|
1491
1645
|
const context = imageRequest === void 0 ? toPiContext(options, void 0, this.options.onReplayDegrade) : await toPiContext({ ...options, signal: watchdog.signal }, imageRequest, this.options.onReplayDegrade);
|
|
1492
|
-
const events = snapshot.provider.streamSimple(model, context, {
|
|
1646
|
+
const events = snapshot.provider.streamSimple(model, normalizeContext(context), {
|
|
1493
1647
|
apiKey,
|
|
1494
1648
|
...reasoning === void 0 || reasoning === "off" ? {} : { reasoning },
|
|
1495
1649
|
...options.temperature === void 0 ? {} : { temperature: options.temperature },
|
|
@@ -1507,7 +1661,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1507
1661
|
// one SDK attempt.
|
|
1508
1662
|
maxRetries: 0
|
|
1509
1663
|
});
|
|
1510
|
-
const iterator = toStreamChunks(events, model.contextWindow, options.signal, model.id)[Symbol.asyncIterator]();
|
|
1664
|
+
const iterator = toStreamChunks(events, model.contextWindow, options.signal, model.id, options.provider)[Symbol.asyncIterator]();
|
|
1511
1665
|
let exhausted = false;
|
|
1512
1666
|
try {
|
|
1513
1667
|
while (true) {
|
|
@@ -1548,7 +1702,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1548
1702
|
};
|
|
1549
1703
|
|
|
1550
1704
|
// src/usage.ts
|
|
1551
|
-
import { randomUUID as
|
|
1705
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1552
1706
|
import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
1553
1707
|
import { attributionHeaders as attributionHeaders3 } from "@deepseek-ai/dsh-llm";
|
|
1554
1708
|
|
|
@@ -1620,7 +1774,7 @@ var GoUsageService = class extends TypertRemoteService {
|
|
|
1620
1774
|
});
|
|
1621
1775
|
}
|
|
1622
1776
|
if (this.identity?.baseURL !== baseURL || this.identity.key !== key) {
|
|
1623
|
-
this.identity = { baseURL, key, source:
|
|
1777
|
+
this.identity = { baseURL, key, source: randomUUID3() };
|
|
1624
1778
|
}
|
|
1625
1779
|
const { source } = this.identity;
|
|
1626
1780
|
const endpoint = diagnosticURL(`${baseURL}/usage`);
|
|
@@ -1726,6 +1880,7 @@ function apply(ctx, raw) {
|
|
|
1726
1880
|
ctx.logger.warn(`llm-opencode-go: gateway models awaiting usable online metadata: ${ids.join(", ")}`);
|
|
1727
1881
|
}
|
|
1728
1882
|
};
|
|
1883
|
+
let registration;
|
|
1729
1884
|
const adapter = new OpencodeGoAdapter({
|
|
1730
1885
|
config: () => current(),
|
|
1731
1886
|
resolveApiKey,
|
|
@@ -1739,11 +1894,13 @@ function apply(ctx, raw) {
|
|
|
1739
1894
|
},
|
|
1740
1895
|
onFallback: logger.fallback,
|
|
1741
1896
|
onOmitted: logger.omitted,
|
|
1897
|
+
onCatalogRefresh: () => {
|
|
1898
|
+
registration?.replace([PROVIDER_ID]);
|
|
1899
|
+
},
|
|
1742
1900
|
onReplayDegrade: (reason) => {
|
|
1743
1901
|
ctx.logger.warn(`llm-opencode-go: unusable replay state on assistant history; sending provider-neutral content (${reason})`);
|
|
1744
1902
|
}
|
|
1745
1903
|
});
|
|
1746
|
-
let registration;
|
|
1747
1904
|
ctx.plugin(GoModelsService, {
|
|
1748
1905
|
catalog: () => adapter.catalogOf(current()),
|
|
1749
1906
|
onRefresh: () => {
|
|
@@ -1756,6 +1913,7 @@ function apply(ctx, raw) {
|
|
|
1756
1913
|
if (configured && current().enabled && registration === void 0) {
|
|
1757
1914
|
try {
|
|
1758
1915
|
registration = ctx.llm.registerAdapter([PROVIDER_ID], adapter);
|
|
1916
|
+
ctx.logger.info(`llm-opencode-go: route "${PROVIDER_ID}" registered as ${DISPLAY_NAME}`);
|
|
1759
1917
|
} catch (error) {
|
|
1760
1918
|
ctx.logger.error(`llm-opencode-go: not registering the "${PROVIDER_ID}" route (${String(error)})`);
|
|
1761
1919
|
}
|
|
@@ -1796,6 +1954,7 @@ function apply(ctx, raw) {
|
|
|
1796
1954
|
});
|
|
1797
1955
|
ctx.effect(() => () => {
|
|
1798
1956
|
registration?.();
|
|
1957
|
+
registration = void 0;
|
|
1799
1958
|
undiscover();
|
|
1800
1959
|
});
|
|
1801
1960
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
@@ -1828,7 +1987,6 @@ function apply(ctx, raw) {
|
|
|
1828
1987
|
});
|
|
1829
1988
|
syncRoute();
|
|
1830
1989
|
});
|
|
1831
|
-
ctx.logger.info(`llm-opencode-go: route "${PROVIDER_ID}" registered as ${DISPLAY_NAME}`);
|
|
1832
1990
|
}
|
|
1833
1991
|
export {
|
|
1834
1992
|
Config,
|
package/lib/types/adapter.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface OpencodeGoAdapterOptions {
|
|
|
62
62
|
onOmitted?: (ids: readonly string[]) => void;
|
|
63
63
|
/** Observe assistant history degrading to provider-neutral conversion. */
|
|
64
64
|
onReplayDegrade?: (reason: string) => void;
|
|
65
|
+
/** Re-read picker models after a background catalog refresh commits. */
|
|
66
|
+
onCatalogRefresh?: () => void;
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
67
69
|
* The single route's adapter. The catalog snapshot freezes at each operation,
|
package/lib/types/catalog.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { Api, Model, Provider } from '
|
|
1
|
+
import type { Api, Model, Provider } from 'opencode-go-pi-ai';
|
|
2
2
|
import type { LlmDiscoveredModel } from '@deepseek-ai/dsh-llm';
|
|
3
3
|
import { type GoModelCatalog } from './models-contract.ts';
|
|
4
4
|
import type { ModelMetadata } from './model-metadata.ts';
|
|
5
|
-
export
|
|
6
|
-
export declare const DISPLAY_NAME = "OpenCode Go";
|
|
5
|
+
export { PROVIDER_ID, DISPLAY_NAME } from './provider-identity.ts';
|
|
7
6
|
export declare const DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
8
7
|
export interface CatalogSnapshot {
|
|
9
8
|
readonly details: ModelMetadata['details'];
|
|
@@ -30,9 +29,15 @@ export declare class OpencodeGoCatalog {
|
|
|
30
29
|
private readonly onFallback;
|
|
31
30
|
/** Kept for API compatibility; now reports unconfigured ids rather than hiding them. */
|
|
32
31
|
private readonly onOmitted;
|
|
32
|
+
private readonly onRefresh;
|
|
33
33
|
private served;
|
|
34
34
|
private pending;
|
|
35
|
+
/** A cold runtime read can use disk metadata while the shared online refresh runs. */
|
|
36
|
+
private cachedPending;
|
|
37
|
+
private initialized;
|
|
38
|
+
private readonly cachePath;
|
|
35
39
|
private metadata;
|
|
40
|
+
private metadataBody;
|
|
36
41
|
private metadataETag;
|
|
37
42
|
private metadataUpdatedAtMs;
|
|
38
43
|
private failures;
|
|
@@ -43,10 +48,15 @@ export declare class OpencodeGoCatalog {
|
|
|
43
48
|
kept: number;
|
|
44
49
|
}) => void,
|
|
45
50
|
/** Kept for API compatibility; now reports unconfigured ids rather than hiding them. */
|
|
46
|
-
onOmitted: (ids: readonly string[]) => void);
|
|
51
|
+
onOmitted: (ids: readonly string[]) => void, onRefresh?: () => void);
|
|
47
52
|
snapshot(force?: boolean, signal?: AbortSignal): Promise<CatalogSnapshot>;
|
|
53
|
+
private restoreMetadata;
|
|
54
|
+
private readSnapshot;
|
|
55
|
+
private currentSnapshot;
|
|
56
|
+
private startRefresh;
|
|
48
57
|
/** Conditional HTTP requests save bandwidth while still checking for updated metadata. */
|
|
49
58
|
private refreshMetadata;
|
|
59
|
+
private persistMetadata;
|
|
50
60
|
/** Gateway ids decide membership; online metadata decides how to call each model. */
|
|
51
61
|
private build;
|
|
52
62
|
/** New or previously unconfigured ids get a fresh lookup even during the runtime TTL. */
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
1
2
|
import type { OpencodeGoModelLimit, OpencodeGoModelLimits, OpencodeGoModels } from './section-controller.ts';
|
|
2
3
|
import type { en } from './locales.ts';
|
|
3
4
|
type Translate = (key: keyof typeof en, params?: Record<string, unknown>) => string;
|
|
4
5
|
export declare function hasCapacityOverride(limit: OpencodeGoModelLimit | null | undefined): boolean;
|
|
5
6
|
/** 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
|
+
export declare function ModelEditor({ models, draft, modelVisibility, t, locale, disabled, visibilitySaving, filterAction, onEdit, onModelEnabled }: {
|
|
7
8
|
models: OpencodeGoModels;
|
|
8
9
|
draft: OpencodeGoModelLimits;
|
|
9
10
|
modelVisibility: Readonly<Record<string, boolean>>;
|
|
@@ -11,6 +12,8 @@ export declare function ModelEditor({ models, draft, modelVisibility, t, locale,
|
|
|
11
12
|
locale?: string;
|
|
12
13
|
disabled: boolean;
|
|
13
14
|
visibilitySaving: boolean;
|
|
15
|
+
/** Optional control pinned to the right of the filter row, such as a refresh. */
|
|
16
|
+
filterAction?: ReactNode;
|
|
14
17
|
onEdit: (next: OpencodeGoModelLimits) => void;
|
|
15
18
|
onModelEnabled: (id: string, enabled: boolean) => void;
|
|
16
19
|
}): import("react").JSX.Element;
|