@dan-ai-studio/dshopencodego 0.1.15 → 0.1.16
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 +1 -1
- package/README.md +1 -1
- package/lib/client.js +15 -2
- package/lib/index.js +203 -52
- package/lib/types/catalog/contract.js +7 -0
- package/lib/types/catalog/go-doc.d.ts +57 -0
- package/lib/types/catalog/go-doc.js +192 -0
- package/lib/types/catalog/index.d.ts +37 -7
- package/lib/types/catalog/index.js +50 -9
- package/lib/types/catalog/json-response.d.ts +6 -3
- package/lib/types/catalog/json-response.js +6 -5
- package/lib/types/catalog/protocol.d.ts +7 -2
- package/lib/types/catalog/reading.d.ts +5 -0
- package/lib/types/catalog/reading.js +3 -1
- package/lib/types/go-limits.d.ts +23 -11
- package/lib/types/go-limits.js +24 -12
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -48,15 +48,15 @@ var __callDispose = (stack, error, hasError) => {
|
|
|
48
48
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
49
49
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
50
50
|
import * as llm2 from "@deepseek-ai/dsh-llm";
|
|
51
|
-
import { LlmError as
|
|
51
|
+
import { LlmError as LlmError12 } from "@deepseek-ai/dsh-llm";
|
|
52
52
|
|
|
53
53
|
// src/adapter.ts
|
|
54
54
|
import { getSupportedThinkingLevels, normalizeContext } from "@earendil-works/pi-ai";
|
|
55
55
|
import {
|
|
56
|
-
attributionHeaders as
|
|
56
|
+
attributionHeaders as attributionHeaders5,
|
|
57
57
|
contentHasImage as contentHasImage2,
|
|
58
58
|
LlmAdapter,
|
|
59
|
-
LlmError as
|
|
59
|
+
LlmError as LlmError8,
|
|
60
60
|
ReasoningEffortId,
|
|
61
61
|
resolveRetryPolicy
|
|
62
62
|
} from "@deepseek-ai/dsh-llm";
|
|
@@ -68,7 +68,7 @@ import { getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
|
|
|
68
68
|
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
69
69
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
70
70
|
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
71
|
-
import { attributionHeaders as
|
|
71
|
+
import { attributionHeaders as attributionHeaders3, LlmError as LlmError4 } from "@deepseek-ai/dsh-llm";
|
|
72
72
|
|
|
73
73
|
// src/catalog/constants.ts
|
|
74
74
|
var PROVIDER_ID = "opencode-go";
|
|
@@ -172,6 +172,117 @@ async function fetchModelIds(baseURL, signal) {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// src/catalog/go-doc.ts
|
|
176
|
+
import { attributionHeaders as attributionHeaders2, LlmError as LlmError3 } from "@deepseek-ai/dsh-llm";
|
|
177
|
+
var GO_DOC_URLS = [
|
|
178
|
+
"https://raw.githubusercontent.com/anomalyco/opencode/dev/packages/web/src/content/docs/go.mdx",
|
|
179
|
+
"https://cdn.jsdelivr.net/gh/anomalyco/opencode@dev/packages/web/src/content/docs/go.mdx"
|
|
180
|
+
];
|
|
181
|
+
var GO_DOC_MAX_BYTES = 1024 * 1024;
|
|
182
|
+
var ENDPOINT_PROTOCOLS = {
|
|
183
|
+
"/chat/completions": "openai-completions",
|
|
184
|
+
"/messages": "anthropic-messages",
|
|
185
|
+
"/responses": "openai-responses"
|
|
186
|
+
};
|
|
187
|
+
function cell(value) {
|
|
188
|
+
return (value ?? "").replace(/[*`]/g, "").trim();
|
|
189
|
+
}
|
|
190
|
+
function baseName(name2) {
|
|
191
|
+
return name2.replace(/\s*\(.*\)\s*$/, "").trim();
|
|
192
|
+
}
|
|
193
|
+
function parseAllowance(value) {
|
|
194
|
+
if (/^unlimited\b/i.test(value) || value.startsWith("\u65E0\u9650\u5236")) return "unlimited";
|
|
195
|
+
const match = value.match(/\$([\d,]+(?:\.\d+)?)/);
|
|
196
|
+
if (match?.[1] === void 0) return void 0;
|
|
197
|
+
const amount = Number(match[1].replace(/,/g, ""));
|
|
198
|
+
return Number.isFinite(amount) ? amount : void 0;
|
|
199
|
+
}
|
|
200
|
+
function parseCount(value) {
|
|
201
|
+
if (/^unlimited\b/i.test(value) || value.startsWith("\u65E0\u9650\u5236")) return "unlimited";
|
|
202
|
+
const amount = Number(value.replace(/,/g, ""));
|
|
203
|
+
return value.length > 0 && Number.isFinite(amount) ? amount : void 0;
|
|
204
|
+
}
|
|
205
|
+
function protocolOfEndpoint(endpoint) {
|
|
206
|
+
for (const [suffix, protocol] of Object.entries(ENDPOINT_PROTOCOLS)) {
|
|
207
|
+
if (endpoint.endsWith(suffix)) return protocol;
|
|
208
|
+
}
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
function tableRows(lines, required) {
|
|
212
|
+
const start = lines.findIndex((line) => line.startsWith("|") && line.includes(required));
|
|
213
|
+
if (start < 0) return [];
|
|
214
|
+
const rows = [];
|
|
215
|
+
for (let index = start + 2; index < lines.length; index++) {
|
|
216
|
+
const line = lines[index] ?? "";
|
|
217
|
+
if (!line.startsWith("|")) break;
|
|
218
|
+
rows.push(line.split("|").slice(1, -1).map((entry) => entry.trim()));
|
|
219
|
+
}
|
|
220
|
+
return rows;
|
|
221
|
+
}
|
|
222
|
+
function parseGoDocument(markdown) {
|
|
223
|
+
const lines = markdown.split("\n");
|
|
224
|
+
const ids = /* @__PURE__ */ new Map();
|
|
225
|
+
const protocols = /* @__PURE__ */ new Map();
|
|
226
|
+
for (const row of tableRows(lines, "Model ID")) {
|
|
227
|
+
const name2 = cell(row[0]);
|
|
228
|
+
const id = cell(row[1]);
|
|
229
|
+
if (name2.length === 0 || id.length === 0) continue;
|
|
230
|
+
ids.set(name2, id);
|
|
231
|
+
const protocol = protocolOfEndpoint(cell(row[2]));
|
|
232
|
+
if (protocol !== void 0) protocols.set(id, protocol);
|
|
233
|
+
}
|
|
234
|
+
const quotas = /* @__PURE__ */ new Map();
|
|
235
|
+
for (const row of tableRows(lines, "Monthly limit")) {
|
|
236
|
+
const id = ids.get(baseName(cell(row[0])));
|
|
237
|
+
if (id === void 0) continue;
|
|
238
|
+
const monthlyUsd = parseAllowance(cell(row[5]));
|
|
239
|
+
if (monthlyUsd === void 0) continue;
|
|
240
|
+
const previous = quotas.get(id);
|
|
241
|
+
if (previous === void 0) quotas.set(id, { monthlyUsd });
|
|
242
|
+
else if (previous.monthlyUsd !== monthlyUsd) quotas.delete(id);
|
|
243
|
+
}
|
|
244
|
+
for (const row of tableRows(lines, "requests per month")) {
|
|
245
|
+
const id = ids.get(baseName(cell(row[0])));
|
|
246
|
+
if (id === void 0) continue;
|
|
247
|
+
const monthlyRequests = parseCount(cell(row[3]));
|
|
248
|
+
const quota = quotas.get(id);
|
|
249
|
+
if (monthlyRequests === void 0 || quota === void 0) continue;
|
|
250
|
+
quotas.set(id, { ...quota, monthlyRequests });
|
|
251
|
+
}
|
|
252
|
+
return { quotas, protocols };
|
|
253
|
+
}
|
|
254
|
+
async function fetchGoDocument(signal) {
|
|
255
|
+
const failures = [];
|
|
256
|
+
for (const url of GO_DOC_URLS) {
|
|
257
|
+
try {
|
|
258
|
+
const timeout = AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS);
|
|
259
|
+
const response = await fetch(url, {
|
|
260
|
+
redirect: "error",
|
|
261
|
+
headers: { ...attributionHeaders2(), accept: "text/plain", "cache-control": "no-cache" },
|
|
262
|
+
signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout])
|
|
263
|
+
});
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
await response.body?.cancel().catch(() => {
|
|
266
|
+
});
|
|
267
|
+
failures.push(`${url} answered HTTP ${response.status}`);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const document = parseGoDocument(await readBoundedText(response, GO_DOC_MAX_BYTES));
|
|
271
|
+
if (document.quotas.size === 0) {
|
|
272
|
+
failures.push(`${url} carried no allowance table`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
return document;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
failures.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
throw new LlmError3(
|
|
281
|
+
`could not read the Go documentation (${failures.join("; ")})`,
|
|
282
|
+
"DOCUMENT_UNAVAILABLE"
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
175
286
|
// src/catalog/protocol.ts
|
|
176
287
|
var WIRE_PROTOCOLS = [
|
|
177
288
|
"anthropic-messages",
|
|
@@ -420,6 +531,8 @@ var OpencodeGoCatalog = class {
|
|
|
420
531
|
metadataDocument;
|
|
421
532
|
metadataETag;
|
|
422
533
|
lastMetadata;
|
|
534
|
+
/** The last parsed documentation; retained across failed fetches. */
|
|
535
|
+
document;
|
|
423
536
|
constructor(options) {
|
|
424
537
|
this.options = options;
|
|
425
538
|
}
|
|
@@ -452,19 +565,21 @@ var OpencodeGoCatalog = class {
|
|
|
452
565
|
if (!snapshot.facts.has(id) && snapshot === cached) snapshot = await this.snapshot(true);
|
|
453
566
|
const reason = snapshot.unavailable.get(id);
|
|
454
567
|
if (reason !== void 0) {
|
|
455
|
-
throw new
|
|
568
|
+
throw new LlmError4(
|
|
456
569
|
`opencode-go model "${id}" is advertised but cannot be configured: ${reason}; refresh the model list to retry`,
|
|
457
570
|
"MODEL_METADATA_UNAVAILABLE"
|
|
458
571
|
);
|
|
459
572
|
}
|
|
460
573
|
return snapshot;
|
|
461
574
|
}
|
|
462
|
-
/** Revalidate
|
|
575
|
+
/** Revalidate every source, reusing the metadata document when it is unchanged. */
|
|
463
576
|
async build() {
|
|
464
577
|
const builtin = builtinModels(this.options.baseURL);
|
|
465
|
-
const
|
|
578
|
+
const readDocument = this.options.readDocument ?? fetchGoDocument;
|
|
579
|
+
const [listing, metadata, documentation] = await Promise.allSettled([
|
|
466
580
|
fetchModelIds(this.options.baseURL),
|
|
467
|
-
this.refreshMetadata(builtin)
|
|
581
|
+
this.refreshMetadata(builtin),
|
|
582
|
+
readDocument()
|
|
468
583
|
]);
|
|
469
584
|
if (metadata.status === "rejected") {
|
|
470
585
|
this.options.observers?.onFallback?.({
|
|
@@ -473,6 +588,15 @@ var OpencodeGoCatalog = class {
|
|
|
473
588
|
kept: this.lastMetadata?.models.size ?? 0
|
|
474
589
|
});
|
|
475
590
|
}
|
|
591
|
+
if (documentation.status === "fulfilled") {
|
|
592
|
+
this.document = documentation.value;
|
|
593
|
+
} else {
|
|
594
|
+
this.options.observers?.onFallback?.({
|
|
595
|
+
url: GO_DOC_URLS[0] ?? "the Go documentation",
|
|
596
|
+
error: documentation.reason,
|
|
597
|
+
kept: this.document?.quotas.size ?? 0
|
|
598
|
+
});
|
|
599
|
+
}
|
|
476
600
|
if (listing.status === "rejected") {
|
|
477
601
|
const previous = this.served;
|
|
478
602
|
this.options.observers?.onFallback?.({
|
|
@@ -487,7 +611,8 @@ var OpencodeGoCatalog = class {
|
|
|
487
611
|
provider: buildProvider(this.options.baseURL, [...facts.values()].map((fact) => toPiModel(fact, this.options.baseURL))),
|
|
488
612
|
live: false,
|
|
489
613
|
listingFailure: listing.reason,
|
|
490
|
-
fetchedAtMs: Date.now()
|
|
614
|
+
fetchedAtMs: Date.now(),
|
|
615
|
+
...this.documentQuotas()
|
|
491
616
|
};
|
|
492
617
|
}
|
|
493
618
|
return this.assemble(
|
|
@@ -511,7 +636,7 @@ var OpencodeGoCatalog = class {
|
|
|
511
636
|
unavailable.set(id, reason);
|
|
512
637
|
continue;
|
|
513
638
|
}
|
|
514
|
-
facts.set(id, this.inferredFacts(id, builtin));
|
|
639
|
+
facts.set(id, this.withDocumentedProtocol(this.inferredFacts(id, builtin)));
|
|
515
640
|
}
|
|
516
641
|
if (unavailable.size > 0) {
|
|
517
642
|
this.options.observers?.onUnconfigured?.(
|
|
@@ -523,9 +648,28 @@ var OpencodeGoCatalog = class {
|
|
|
523
648
|
unavailable,
|
|
524
649
|
provider: buildProvider(this.options.baseURL, [...facts.values()].map((fact) => toPiModel(fact, this.options.baseURL))),
|
|
525
650
|
live: true,
|
|
526
|
-
fetchedAtMs: Date.now()
|
|
651
|
+
fetchedAtMs: Date.now(),
|
|
652
|
+
...this.documentQuotas()
|
|
527
653
|
};
|
|
528
654
|
}
|
|
655
|
+
/** The document allowances as a snapshot field, absent until one fetch lands. */
|
|
656
|
+
documentQuotas() {
|
|
657
|
+
return this.document === void 0 ? {} : { documentQuotas: this.document.quotas };
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Prefer the documented protocol over anything below the installed catalog.
|
|
661
|
+
*
|
|
662
|
+
* The endpoint table is the provider's own statement, so it outranks both the
|
|
663
|
+
* family guess and models.dev's SDK hint — this turns an `inferred` badge into
|
|
664
|
+
* a known answer for ids the installed catalog misses. It never overrides a
|
|
665
|
+
* builtin entry or a configured override: those carry the wire quirks the
|
|
666
|
+
* adapter relies on.
|
|
667
|
+
*/
|
|
668
|
+
withDocumentedProtocol(facts) {
|
|
669
|
+
if (facts.protocolSource !== "inferred" && facts.protocolSource !== "online") return facts;
|
|
670
|
+
const documented = this.document?.protocols.get(facts.id);
|
|
671
|
+
return documented === void 0 ? facts : { ...facts, api: documented, protocolSource: "document" };
|
|
672
|
+
}
|
|
529
673
|
/** Facts for an id no source describes, from the ladder and the route defaults. */
|
|
530
674
|
inferredFacts(id, builtin) {
|
|
531
675
|
const exact = builtin.get(id);
|
|
@@ -556,7 +700,7 @@ var OpencodeGoCatalog = class {
|
|
|
556
700
|
const response = await fetch(MODEL_METADATA_URL, {
|
|
557
701
|
redirect: "error",
|
|
558
702
|
headers: {
|
|
559
|
-
...
|
|
703
|
+
...attributionHeaders3(),
|
|
560
704
|
accept: "application/json",
|
|
561
705
|
"cache-control": "no-cache",
|
|
562
706
|
...this.metadataETag === void 0 ? {} : { "if-none-match": this.metadataETag }
|
|
@@ -598,8 +742,8 @@ function inferFamily(id) {
|
|
|
598
742
|
async function discoverCatalogModels(catalog) {
|
|
599
743
|
const snapshot = await catalog.snapshot(true);
|
|
600
744
|
if (!snapshot.live) {
|
|
601
|
-
const detail = snapshot.listingFailure instanceof
|
|
602
|
-
throw new
|
|
745
|
+
const detail = snapshot.listingFailure instanceof LlmError4 ? snapshot.listingFailure.message : "the live model listing is unreachable";
|
|
746
|
+
throw new LlmError4(`dshopencodego: ${detail}; refresh the model list to retry`, "DISCOVERY_FAILED", {
|
|
603
747
|
cause: snapshot.listingFailure
|
|
604
748
|
});
|
|
605
749
|
}
|
|
@@ -670,11 +814,11 @@ function assertBaseURL(raw) {
|
|
|
670
814
|
// src/conversion/context.ts
|
|
671
815
|
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
672
816
|
import * as llm from "@deepseek-ai/dsh-llm";
|
|
673
|
-
import { contentHasImage, LlmError as
|
|
817
|
+
import { contentHasImage, LlmError as LlmError6, requestImageHandleText } from "@deepseek-ai/dsh-llm";
|
|
674
818
|
import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
|
|
675
819
|
|
|
676
820
|
// src/conversion/replay.ts
|
|
677
|
-
import { LlmError as
|
|
821
|
+
import { LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
|
|
678
822
|
function parseArguments(raw) {
|
|
679
823
|
try {
|
|
680
824
|
const parsed = JSON.parse(raw);
|
|
@@ -733,7 +877,7 @@ function toPiReplayState(message, requestedModel = message.model) {
|
|
|
733
877
|
};
|
|
734
878
|
}
|
|
735
879
|
function invalidReplay(message) {
|
|
736
|
-
throw new
|
|
880
|
+
throw new LlmError5(`invalid pi-ai replay state: ${message}`, "INVALID_REPLAY_STATE");
|
|
737
881
|
}
|
|
738
882
|
function readReplayState(value) {
|
|
739
883
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return invalidReplay("expected a replay envelope");
|
|
@@ -793,7 +937,7 @@ function foreignAssistant(message) {
|
|
|
793
937
|
});
|
|
794
938
|
break;
|
|
795
939
|
case "image":
|
|
796
|
-
throw new
|
|
940
|
+
throw new LlmError5("pi-ai chat history cannot represent structured assistant image output", "UNSUPPORTED_CONTENT");
|
|
797
941
|
default:
|
|
798
942
|
break;
|
|
799
943
|
}
|
|
@@ -867,7 +1011,7 @@ function toPiAssistant(message, onDegrade) {
|
|
|
867
1011
|
try {
|
|
868
1012
|
return replayedAssistant(message, source, source.replayState);
|
|
869
1013
|
} catch (error) {
|
|
870
|
-
if (!(error instanceof
|
|
1014
|
+
if (!(error instanceof LlmError5) || error.code !== "INVALID_REPLAY_STATE") throw error;
|
|
871
1015
|
onDegrade?.(error.message);
|
|
872
1016
|
return foreignAssistant(message);
|
|
873
1017
|
}
|
|
@@ -883,7 +1027,7 @@ function toolMessage(message) {
|
|
|
883
1027
|
function assertSupportedImageRoles(messages) {
|
|
884
1028
|
for (const message of messages) {
|
|
885
1029
|
if (message.role !== "user" && toolMessage(message) === void 0 && contentHasImage(message.content)) {
|
|
886
|
-
throw new
|
|
1030
|
+
throw new LlmError6(`pi-ai cannot represent an image in an in-history ${message.role} message`, "UNSUPPORTED_CONTENT");
|
|
887
1031
|
}
|
|
888
1032
|
}
|
|
889
1033
|
}
|
|
@@ -931,7 +1075,7 @@ async function userContent(blocks, requestImages, resolveImageAccess) {
|
|
|
931
1075
|
if (block.type === "image") {
|
|
932
1076
|
const version = requestImages.get(block.attachment.attachmentId);
|
|
933
1077
|
if (version === void 0) {
|
|
934
|
-
throw new
|
|
1078
|
+
throw new LlmError6(
|
|
935
1079
|
"dshopencodego: an image in this request has no prepared bytes; image input requires the attachment service",
|
|
936
1080
|
"UNSUPPORTED_CONTENT"
|
|
937
1081
|
);
|
|
@@ -955,7 +1099,7 @@ function projectImagesForRequest(messages, images, requestImages) {
|
|
|
955
1099
|
(block) => requestImages.get(block.attachment.attachmentId).bytes
|
|
956
1100
|
);
|
|
957
1101
|
if (over > 0) {
|
|
958
|
-
throw new
|
|
1102
|
+
throw new LlmError6(
|
|
959
1103
|
`request images exceed the ${images.maxRequestImageBytes}-byte base64 bound; ${over} more oldest occurrence(s) must be offloaded`,
|
|
960
1104
|
llm.IMAGE_OFFLOAD_REQUIRED_CODE,
|
|
961
1105
|
{ offloadImages: over }
|
|
@@ -1017,7 +1161,7 @@ import {
|
|
|
1017
1161
|
EMPTY_RESPONSE_CODE,
|
|
1018
1162
|
isContextWindowExceededError,
|
|
1019
1163
|
isQuotaExceededError,
|
|
1020
|
-
LlmError as
|
|
1164
|
+
LlmError as LlmError7
|
|
1021
1165
|
} from "@deepseek-ai/dsh-llm";
|
|
1022
1166
|
import { isContextOverflow } from "@earendil-works/pi-ai";
|
|
1023
1167
|
function mapUsage(usage) {
|
|
@@ -1170,11 +1314,11 @@ async function* toStreamChunks(events, contextWindow, callerSignal, requestedMod
|
|
|
1170
1314
|
return;
|
|
1171
1315
|
}
|
|
1172
1316
|
}
|
|
1173
|
-
throw new
|
|
1317
|
+
throw new LlmError7("pi-ai event stream ended without a terminal event", "STREAM_CLOSED");
|
|
1174
1318
|
}
|
|
1175
1319
|
|
|
1176
1320
|
// src/go-limits.ts
|
|
1177
|
-
var
|
|
1321
|
+
var SEED_QUOTAS = {
|
|
1178
1322
|
"glm-5.3-flash": { monthlyUsd: 60, monthlyRequests: 31580 },
|
|
1179
1323
|
"glm-5.3": { monthlyUsd: 15, monthlyRequests: 1080 },
|
|
1180
1324
|
"glm-5.2": { monthlyUsd: 60, monthlyRequests: 4300 },
|
|
@@ -1210,7 +1354,7 @@ var GO_QUOTAS = {
|
|
|
1210
1354
|
"gpt-5.6-luna": { monthlyUsd: 15, monthlyRequests: 10250 }
|
|
1211
1355
|
};
|
|
1212
1356
|
function goQuotaFor(id) {
|
|
1213
|
-
return Object.hasOwn(
|
|
1357
|
+
return Object.hasOwn(SEED_QUOTAS, id) ? SEED_QUOTAS[id] : void 0;
|
|
1214
1358
|
}
|
|
1215
1359
|
function monthlyRequestsRank(quota) {
|
|
1216
1360
|
if (quota === void 0) return -1;
|
|
@@ -1250,7 +1394,7 @@ function sortModels(models, now = Date.now()) {
|
|
|
1250
1394
|
|
|
1251
1395
|
// src/session-header.ts
|
|
1252
1396
|
import { randomUUID } from "node:crypto";
|
|
1253
|
-
import { attributionHeaders as
|
|
1397
|
+
import { attributionHeaders as attributionHeaders4 } from "@deepseek-ai/dsh-llm";
|
|
1254
1398
|
var SESSION_HEADER = "x-opencode-session";
|
|
1255
1399
|
function opencodeSessionValue(sessionId) {
|
|
1256
1400
|
return sessionId !== void 0 && sessionId.length > 0 ? sessionId : randomUUID();
|
|
@@ -1258,7 +1402,7 @@ function opencodeSessionValue(sessionId) {
|
|
|
1258
1402
|
function providerHeaders(sessionId) {
|
|
1259
1403
|
return {
|
|
1260
1404
|
[SESSION_HEADER]: opencodeSessionValue(sessionId),
|
|
1261
|
-
...
|
|
1405
|
+
...attributionHeaders4()
|
|
1262
1406
|
};
|
|
1263
1407
|
}
|
|
1264
1408
|
|
|
@@ -1323,7 +1467,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1323
1467
|
const config = this.options.config();
|
|
1324
1468
|
const snapshot = await this.catalogOf(config).forModel(model);
|
|
1325
1469
|
const facts = snapshot.facts.get(model);
|
|
1326
|
-
if (facts === void 0) throw new
|
|
1470
|
+
if (facts === void 0) throw new LlmError8(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
|
|
1327
1471
|
return this.modelInfo(withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits));
|
|
1328
1472
|
}
|
|
1329
1473
|
/** Describe one model: capacities plus the reasoning levels it really offers. */
|
|
@@ -1369,7 +1513,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1369
1513
|
if (effort === void 0 || effort === "off") return void 0;
|
|
1370
1514
|
const supported = getSupportedThinkingLevels(model);
|
|
1371
1515
|
if (supported.some((level) => level === effort)) return effort;
|
|
1372
|
-
throw new
|
|
1516
|
+
throw new LlmError8(
|
|
1373
1517
|
`opencode-go model "${model.id}" does not support reasoning effort "${effort}"`,
|
|
1374
1518
|
"UNSUPPORTED_REASONING_EFFORT"
|
|
1375
1519
|
);
|
|
@@ -1378,18 +1522,18 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1378
1522
|
var _stack = [];
|
|
1379
1523
|
try {
|
|
1380
1524
|
if (options.stop !== void 0) {
|
|
1381
|
-
throw new
|
|
1525
|
+
throw new LlmError8("dshopencodego does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
|
|
1382
1526
|
}
|
|
1383
1527
|
const config = this.options.config();
|
|
1384
1528
|
const snapshot = await this.catalogOf(config).forModel(options.model);
|
|
1385
1529
|
const facts = snapshot.facts.get(options.model);
|
|
1386
|
-
if (facts === void 0) throw new
|
|
1530
|
+
if (facts === void 0) throw new LlmError8(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
|
|
1387
1531
|
const model = withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits);
|
|
1388
1532
|
const ceiling = config.modelLimits[model.id]?.maxTokens;
|
|
1389
1533
|
const maxTokens = options.maxTokens === void 0 || ceiling === null || ceiling === void 0 ? options.maxTokens : Math.min(options.maxTokens, ceiling);
|
|
1390
1534
|
const apiKey = await this.options.resolveApiKey();
|
|
1391
1535
|
if (apiKey === void 0 || apiKey.length === 0) {
|
|
1392
|
-
throw new
|
|
1536
|
+
throw new LlmError8("dshopencodego: no credential resolved for the opencode-go route", "MISSING_CREDENTIAL");
|
|
1393
1537
|
}
|
|
1394
1538
|
const reasoning = this.resolveReasoningLevel(model, options.reasoningEffort);
|
|
1395
1539
|
const consumer = new AbortController();
|
|
@@ -1398,14 +1542,14 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1398
1542
|
try {
|
|
1399
1543
|
const containsImage = options.messages.some((message) => contentHasImage2(message.content));
|
|
1400
1544
|
if (containsImage && !model.input.includes("image")) {
|
|
1401
|
-
throw new
|
|
1545
|
+
throw new LlmError8(`opencode-go model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
|
|
1402
1546
|
}
|
|
1403
1547
|
let imageRequest;
|
|
1404
1548
|
if (containsImage) {
|
|
1405
1549
|
const access = this.options.imageAccess;
|
|
1406
1550
|
const store = access?.resolveAttachments();
|
|
1407
1551
|
if (access === void 0 || store === void 0) {
|
|
1408
|
-
throw new
|
|
1552
|
+
throw new LlmError8("dshopencodego image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1409
1553
|
}
|
|
1410
1554
|
imageRequest = {
|
|
1411
1555
|
attachments: store,
|
|
@@ -1435,7 +1579,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1435
1579
|
while (true) {
|
|
1436
1580
|
const result = await watchdog.next(iterator);
|
|
1437
1581
|
if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
|
|
1438
|
-
throw new
|
|
1582
|
+
throw new LlmError8("opencode-go stream idle timeout", "TIMEOUT");
|
|
1439
1583
|
}
|
|
1440
1584
|
if (result.done) {
|
|
1441
1585
|
exhausted = true;
|
|
@@ -1457,10 +1601,10 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1457
1601
|
}
|
|
1458
1602
|
} catch (error) {
|
|
1459
1603
|
if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
|
|
1460
|
-
throw new
|
|
1604
|
+
throw new LlmError8("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
|
|
1461
1605
|
}
|
|
1462
1606
|
if (options.signal?.aborted) {
|
|
1463
|
-
throw new
|
|
1607
|
+
throw new LlmError8("opencode-go request aborted by caller", "ABORTED", { cause: error });
|
|
1464
1608
|
}
|
|
1465
1609
|
throw error;
|
|
1466
1610
|
} finally {
|
|
@@ -1544,11 +1688,16 @@ function parseCatalogReading(value) {
|
|
|
1544
1688
|
if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 0) throw new Error(`invalid catalog count "${key}"`);
|
|
1545
1689
|
return entry;
|
|
1546
1690
|
};
|
|
1691
|
+
const quotaSource = row["quotaSource"];
|
|
1692
|
+
if (quotaSource !== void 0 && quotaSource !== "document" && quotaSource !== "seed") {
|
|
1693
|
+
throw new Error("invalid catalog quota source");
|
|
1694
|
+
}
|
|
1547
1695
|
return {
|
|
1548
1696
|
models: row["models"].map(parseModel),
|
|
1549
1697
|
stale: row["stale"],
|
|
1550
1698
|
...typeof row["error"] === "string" ? { error: row["error"] } : {},
|
|
1551
1699
|
fetchedAtMs: row["fetchedAtMs"],
|
|
1700
|
+
quotaSource: quotaSource ?? "seed",
|
|
1552
1701
|
counts: {
|
|
1553
1702
|
total: count("total"),
|
|
1554
1703
|
enabled: count("enabled"),
|
|
@@ -1691,13 +1840,14 @@ function registerRemotes(ctx) {
|
|
|
1691
1840
|
|
|
1692
1841
|
// src/catalog/service.ts
|
|
1693
1842
|
import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
1694
|
-
import { LlmError as
|
|
1843
|
+
import { LlmError as LlmError9 } from "@deepseek-ai/dsh-llm";
|
|
1695
1844
|
|
|
1696
1845
|
// src/catalog/reading.ts
|
|
1697
1846
|
function catalogReading(snapshot, visibility, listingFailure) {
|
|
1847
|
+
const documentQuotas = snapshot.documentQuotas;
|
|
1698
1848
|
const rows = [
|
|
1699
1849
|
...[...snapshot.facts.values()].map((fact) => {
|
|
1700
|
-
const quota = goQuotaFor(fact.id);
|
|
1850
|
+
const quota = documentQuotas === void 0 ? goQuotaFor(fact.id) : documentQuotas.get(fact.id);
|
|
1701
1851
|
const priced = fact.cost.input > 0 || fact.cost.output > 0;
|
|
1702
1852
|
return {
|
|
1703
1853
|
id: fact.id,
|
|
@@ -1731,6 +1881,7 @@ function catalogReading(snapshot, visibility, listingFailure) {
|
|
|
1731
1881
|
stale: !snapshot.live,
|
|
1732
1882
|
...!snapshot.live && listingFailure !== void 0 ? { error: listingFailure } : {},
|
|
1733
1883
|
fetchedAtMs: snapshot.fetchedAtMs,
|
|
1884
|
+
quotaSource: documentQuotas === void 0 ? "seed" : "document",
|
|
1734
1885
|
counts: {
|
|
1735
1886
|
total: models.length,
|
|
1736
1887
|
enabled,
|
|
@@ -1763,7 +1914,7 @@ var OpencodeGoCatalogService = class extends TypertRemoteService {
|
|
|
1763
1914
|
return catalogReading(
|
|
1764
1915
|
snapshot,
|
|
1765
1916
|
this.options.visibility(),
|
|
1766
|
-
failure === void 0 ? void 0 : failure instanceof
|
|
1917
|
+
failure === void 0 ? void 0 : failure instanceof LlmError9 || failure instanceof Error ? failure.message : String(failure)
|
|
1767
1918
|
);
|
|
1768
1919
|
}
|
|
1769
1920
|
};
|
|
@@ -1819,7 +1970,7 @@ var UsageMeter = class {
|
|
|
1819
1970
|
};
|
|
1820
1971
|
|
|
1821
1972
|
// src/usage/windows.ts
|
|
1822
|
-
import { attributionHeaders as
|
|
1973
|
+
import { attributionHeaders as attributionHeaders6, LlmError as LlmError10 } from "@deepseek-ai/dsh-llm";
|
|
1823
1974
|
var USAGE_MAX_BYTES = 1024 * 1024;
|
|
1824
1975
|
var USAGE_TIMEOUT_MS = 1e4;
|
|
1825
1976
|
function parseGoUsage(value) {
|
|
@@ -1837,7 +1988,7 @@ function parseGoUsage(value) {
|
|
|
1837
1988
|
}
|
|
1838
1989
|
async function readUsageWindows(options) {
|
|
1839
1990
|
if (options.apiKey === void 0 || options.apiKey.length === 0) {
|
|
1840
|
-
throw new
|
|
1991
|
+
throw new LlmError10("dshopencodego: no credential is configured for the opencode-go route", "USAGE_UNAVAILABLE");
|
|
1841
1992
|
}
|
|
1842
1993
|
const url = `${options.baseURL.replace(/\/+$/, "")}/usage`;
|
|
1843
1994
|
const timeout = AbortSignal.timeout(USAGE_TIMEOUT_MS);
|
|
@@ -1846,19 +1997,19 @@ async function readUsageWindows(options) {
|
|
|
1846
1997
|
response = await fetch(url, {
|
|
1847
1998
|
redirect: "error",
|
|
1848
1999
|
headers: {
|
|
1849
|
-
...
|
|
2000
|
+
...attributionHeaders6(),
|
|
1850
2001
|
accept: "application/json",
|
|
1851
2002
|
authorization: `Bearer ${options.apiKey}`
|
|
1852
2003
|
},
|
|
1853
2004
|
signal: options.signal === void 0 ? timeout : AbortSignal.any([options.signal, timeout])
|
|
1854
2005
|
});
|
|
1855
2006
|
} catch (error) {
|
|
1856
|
-
throw new
|
|
2007
|
+
throw new LlmError10(`could not reach ${url}`, "USAGE_UNAVAILABLE", { cause: error });
|
|
1857
2008
|
}
|
|
1858
2009
|
if (!response.ok) {
|
|
1859
2010
|
await response.body?.cancel().catch(() => {
|
|
1860
2011
|
});
|
|
1861
|
-
throw new
|
|
2012
|
+
throw new LlmError10(`${url} answered HTTP ${response.status}`, "USAGE_UNAVAILABLE");
|
|
1862
2013
|
}
|
|
1863
2014
|
const body = await readBoundedJson(response, url, USAGE_MAX_BYTES);
|
|
1864
2015
|
try {
|
|
@@ -1867,16 +2018,16 @@ async function readUsageWindows(options) {
|
|
|
1867
2018
|
...options.source === void 0 ? {} : { source: options.source }
|
|
1868
2019
|
};
|
|
1869
2020
|
} catch (error) {
|
|
1870
|
-
throw new
|
|
2021
|
+
throw new LlmError10(`${url} returned an invalid usage document`, "USAGE_UNAVAILABLE", { cause: error });
|
|
1871
2022
|
}
|
|
1872
2023
|
}
|
|
1873
2024
|
|
|
1874
2025
|
// src/usage/service.ts
|
|
1875
2026
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1876
2027
|
import { RemoteError as RemoteError2, TypertRemoteService as TypertRemoteService2 } from "@deepseek-ai/dsh-typert-protocol";
|
|
1877
|
-
import { LlmError as
|
|
2028
|
+
import { LlmError as LlmError11 } from "@deepseek-ai/dsh-llm";
|
|
1878
2029
|
function usageFailure(error, source) {
|
|
1879
|
-
const missing = error instanceof
|
|
2030
|
+
const missing = error instanceof LlmError11 && error.code === "MISSING_CREDENTIAL";
|
|
1880
2031
|
return new RemoteError2(
|
|
1881
2032
|
"dshopencodego/usage-unavailable",
|
|
1882
2033
|
error instanceof Error ? error.message : "OpenCode Go usage is unavailable",
|
|
@@ -1903,7 +2054,7 @@ var OpencodeGoUsageService = class extends TypertRemoteService2 {
|
|
|
1903
2054
|
}
|
|
1904
2055
|
if (key === void 0 || key.length === 0) {
|
|
1905
2056
|
this.identity = void 0;
|
|
1906
|
-
throw usageFailure(new
|
|
2057
|
+
throw usageFailure(new LlmError11("No OpenCode Go API key is configured", "MISSING_CREDENTIAL"), void 0);
|
|
1907
2058
|
}
|
|
1908
2059
|
if (this.identity?.baseURL !== baseURL || this.identity.key !== key) {
|
|
1909
2060
|
this.identity = { baseURL, key, source: randomUUID2() };
|
|
@@ -1937,7 +2088,7 @@ function apply(ctx, raw) {
|
|
|
1937
2088
|
const credentials = ctx.get("credentials");
|
|
1938
2089
|
const hit = credentials !== void 0 ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
|
|
1939
2090
|
if (hit !== void 0 && hit.length > 0) return llm2.assertUsableApiKey(hit, name, ref);
|
|
1940
|
-
throw new
|
|
2091
|
+
throw new LlmError12(
|
|
1941
2092
|
`dshopencodego: no credential; the profile resolves ${ref}, which is not set \u2014 store ${ref} through the credentials service (the Web Models page writes it) or export it`,
|
|
1942
2093
|
"MISSING_CREDENTIAL"
|
|
1943
2094
|
);
|
|
@@ -2018,7 +2169,7 @@ function apply(ctx, raw) {
|
|
|
2018
2169
|
syncRoute();
|
|
2019
2170
|
const undiscover = ctx.llm.registerModelDiscovery(name, async (request) => {
|
|
2020
2171
|
if (request.provider !== PROVIDER_ID && !(request.baseURL ?? "").includes("opencode.ai")) {
|
|
2021
|
-
throw new
|
|
2172
|
+
throw new LlmError12(
|
|
2022
2173
|
"dshopencodego discovers only OpenCode zen/go endpoints; enter this provider's models by hand",
|
|
2023
2174
|
"DISCOVERY_UNSUPPORTED"
|
|
2024
2175
|
);
|
|
@@ -93,11 +93,18 @@ export function parseCatalogReading(value) {
|
|
|
93
93
|
throw new Error(`invalid catalog count "${key}"`);
|
|
94
94
|
return entry;
|
|
95
95
|
};
|
|
96
|
+
const quotaSource = row['quotaSource'];
|
|
97
|
+
// Absent means the payload predates the field — a cached client bundle with a
|
|
98
|
+
// newer host — and the seed is what such a reading actually carries.
|
|
99
|
+
if (quotaSource !== undefined && quotaSource !== 'document' && quotaSource !== 'seed') {
|
|
100
|
+
throw new Error('invalid catalog quota source');
|
|
101
|
+
}
|
|
96
102
|
return {
|
|
97
103
|
models: row['models'].map(parseModel),
|
|
98
104
|
stale: row['stale'],
|
|
99
105
|
...typeof row['error'] === 'string' ? { error: row['error'] } : {},
|
|
100
106
|
fetchedAtMs: row['fetchedAtMs'],
|
|
107
|
+
quotaSource: quotaSource ?? 'seed',
|
|
101
108
|
counts: {
|
|
102
109
|
total: count('total'),
|
|
103
110
|
enabled: count('enabled'),
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider's own Go documentation as a data source.
|
|
3
|
+
*
|
|
4
|
+
* No API exposes the per-model allowances: the gateway's `/models` answers ids,
|
|
5
|
+
* and `/usage` answers account-wide percentages. The numbers live on a docs page
|
|
6
|
+
* whose source is a markdown file in the provider's repository, so this module
|
|
7
|
+
* fetches that file and parses its tables — allowances (with the provider's own
|
|
8
|
+
* request estimates) and the endpoint table that names each model's protocol.
|
|
9
|
+
*
|
|
10
|
+
* The document is the primary source. The transcribed table in `go-limits.ts`
|
|
11
|
+
* stays behind it as a startup seed and a permanent fallback for the day the
|
|
12
|
+
* URL moves, and its age is surfaced wherever it is used.
|
|
13
|
+
*
|
|
14
|
+
* @module @dan-ai-studio/dshopencodego/catalog/go-doc
|
|
15
|
+
*/
|
|
16
|
+
import type { GoQuota } from '../go-limits.ts';
|
|
17
|
+
import type { WireProtocol } from './protocol.ts';
|
|
18
|
+
/**
|
|
19
|
+
* Documentation sources, tried in order.
|
|
20
|
+
*
|
|
21
|
+
* The repository file is the source of truth; the jsDelivr entry caches the
|
|
22
|
+
* same path and is tried when GitHub itself is unreachable.
|
|
23
|
+
*/
|
|
24
|
+
export declare const GO_DOC_URLS: readonly string[];
|
|
25
|
+
/** The document is about 30 KB; the cap leaves room without inviting a flood. */
|
|
26
|
+
export declare const GO_DOC_MAX_BYTES: number;
|
|
27
|
+
/** What one parse of the document yields. */
|
|
28
|
+
export interface GoDoc {
|
|
29
|
+
/** Per-model allowances, keyed by the gateway's model id. */
|
|
30
|
+
readonly quotas: ReadonlyMap<string, GoQuota>;
|
|
31
|
+
/** Per-model protocol, straight from the provider's endpoint table. */
|
|
32
|
+
readonly protocols: ReadonlyMap<string, WireProtocol>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse the Go documentation into the two data sets this plugin consumes.
|
|
36
|
+
*
|
|
37
|
+
* Titles are the join key: the allowance tables name models the way the page
|
|
38
|
+
* presents them while the endpoint table carries the ids, so ids come from the
|
|
39
|
+
* endpoint table and the other two attach by normalised display name. A row
|
|
40
|
+
* that names no known model, or states no usable number, is skipped rather than
|
|
41
|
+
* guessed; a tiered model whose rows disagree is dropped entirely.
|
|
42
|
+
* @param markdown - the raw document text.
|
|
43
|
+
* @returns the parsed allowances and protocols; both may be empty when the
|
|
44
|
+
* document does not carry the expected tables.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseGoDocument(markdown: string): GoDoc;
|
|
47
|
+
/**
|
|
48
|
+
* Fetch and parse the documentation, trying each source in turn.
|
|
49
|
+
*
|
|
50
|
+
* A source that answers without a usable allowance table counts as a failure,
|
|
51
|
+
* not as an empty document: that is what a moved or restructured page looks
|
|
52
|
+
* like, and the caller must keep its previous data rather than blank the page.
|
|
53
|
+
* @param signal - caller cancellation, if any.
|
|
54
|
+
* @returns the parsed document from the first source that carried one.
|
|
55
|
+
* @throws {LlmError} `DOCUMENT_UNAVAILABLE` when every source failed.
|
|
56
|
+
*/
|
|
57
|
+
export declare function fetchGoDocument(signal?: AbortSignal): Promise<GoDoc>;
|