@dan-ai-studio/dshopencodego 0.1.14 → 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/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 LlmError11 } from "@deepseek-ai/dsh-llm";
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 attributionHeaders4,
56
+ attributionHeaders as attributionHeaders5,
57
57
  contentHasImage as contentHasImage2,
58
58
  LlmAdapter,
59
- LlmError as LlmError7,
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 attributionHeaders2, LlmError as LlmError3 } from "@deepseek-ai/dsh-llm";
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 LlmError3(
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 both sources, reusing the metadata document when it is unchanged. */
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 [listing, metadata] = await Promise.allSettled([
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
- ...attributionHeaders2(),
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 LlmError3 ? snapshot.listingFailure.message : "the live model listing is unreachable";
602
- throw new LlmError3(`dshopencodego: ${detail}; refresh the model list to retry`, "DISCOVERY_FAILED", {
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 LlmError5, requestImageHandleText } from "@deepseek-ai/dsh-llm";
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 LlmError4 } from "@deepseek-ai/dsh-llm";
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 LlmError4(`invalid pi-ai replay state: ${message}`, "INVALID_REPLAY_STATE");
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 LlmError4("pi-ai chat history cannot represent structured assistant image output", "UNSUPPORTED_CONTENT");
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 LlmError4) || error.code !== "INVALID_REPLAY_STATE") throw error;
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 LlmError5(`pi-ai cannot represent an image in an in-history ${message.role} message`, "UNSUPPORTED_CONTENT");
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 LlmError5(
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 LlmError5(
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 LlmError6
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 LlmError6("pi-ai event stream ended without a terminal event", "STREAM_CLOSED");
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 GO_QUOTAS = {
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(GO_QUOTAS, id) ? GO_QUOTAS[id] : void 0;
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 attributionHeaders3 } from "@deepseek-ai/dsh-llm";
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
- ...attributionHeaders3()
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 LlmError7(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
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 LlmError7(
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 LlmError7("dshopencodego does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
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 LlmError7(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
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 LlmError7("dshopencodego: no credential resolved for the opencode-go route", "MISSING_CREDENTIAL");
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 LlmError7(`opencode-go model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
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 LlmError7("dshopencodego image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
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 LlmError7("opencode-go stream idle timeout", "TIMEOUT");
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 LlmError7("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
1604
+ throw new LlmError8("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
1461
1605
  }
1462
1606
  if (options.signal?.aborted) {
1463
- throw new LlmError7("opencode-go request aborted by caller", "ABORTED", { cause: error });
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 LlmError8 } from "@deepseek-ai/dsh-llm";
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 LlmError8 || failure instanceof Error ? failure.message : String(failure)
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 attributionHeaders5, LlmError as LlmError9 } from "@deepseek-ai/dsh-llm";
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 LlmError9("dshopencodego: no credential is configured for the opencode-go route", "USAGE_UNAVAILABLE");
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
- ...attributionHeaders5(),
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 LlmError9(`could not reach ${url}`, "USAGE_UNAVAILABLE", { cause: error });
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 LlmError9(`${url} answered HTTP ${response.status}`, "USAGE_UNAVAILABLE");
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 LlmError9(`${url} returned an invalid usage document`, "USAGE_UNAVAILABLE", { cause: error });
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 LlmError10 } from "@deepseek-ai/dsh-llm";
2028
+ import { LlmError as LlmError11 } from "@deepseek-ai/dsh-llm";
1878
2029
  function usageFailure(error, source) {
1879
- const missing = error instanceof LlmError10 && error.code === "MISSING_CREDENTIAL";
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 LlmError10("No OpenCode Go API key is configured", "MISSING_CREDENTIAL"), void 0);
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 LlmError11(
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 LlmError11(
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>;