@dan-ai-studio/dshopencodego 0.1.43 → 0.1.44

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.
Files changed (44) hide show
  1. package/README.en.md +48 -28
  2. package/README.md +47 -27
  3. package/lib/client.js +47 -1
  4. package/lib/index.js +321 -123
  5. package/lib/types/adapter.d.ts +26 -3
  6. package/lib/types/catalog/go-doc.d.ts +9 -5
  7. package/lib/types/catalog/index.d.ts +22 -7
  8. package/lib/types/catalog/service.d.ts +44 -1
  9. package/lib/types/config.d.ts +4 -3
  10. package/lib/types/conversion/replay.d.ts +30 -1
  11. package/lib/types/models.d.ts +14 -0
  12. package/lib/types/usage/meter.d.ts +89 -2
  13. package/package.json +20 -21
  14. package/lib/types/adapter.js +0 -384
  15. package/lib/types/catalog/constants.js +0 -18
  16. package/lib/types/catalog/contract.js +0 -147
  17. package/lib/types/catalog/gateway.js +0 -59
  18. package/lib/types/catalog/go-doc.js +0 -192
  19. package/lib/types/catalog/index.js +0 -377
  20. package/lib/types/catalog/json-response.js +0 -73
  21. package/lib/types/catalog/metadata.js +0 -278
  22. package/lib/types/catalog/protocol.js +0 -87
  23. package/lib/types/catalog/reading.js +0 -73
  24. package/lib/types/catalog/service.js +0 -45
  25. package/lib/types/config.js +0 -91
  26. package/lib/types/conversion/context.js +0 -212
  27. package/lib/types/conversion/index.js +0 -7
  28. package/lib/types/conversion/replay.js +0 -242
  29. package/lib/types/conversion/stream.js +0 -221
  30. package/lib/types/go-limits.js +0 -91
  31. package/lib/types/index.js +0 -198
  32. package/lib/types/models.js +0 -86
  33. package/lib/types/remotes.js +0 -29
  34. package/lib/types/session-header.js +0 -49
  35. package/lib/types/update/check.js +0 -138
  36. package/lib/types/update/contract.js +0 -64
  37. package/lib/types/update/index.js +0 -10
  38. package/lib/types/update/service.js +0 -39
  39. package/lib/types/update/version.js +0 -16
  40. package/lib/types/usage/contract.js +0 -106
  41. package/lib/types/usage/index.js +0 -8
  42. package/lib/types/usage/meter.js +0 -65
  43. package/lib/types/usage/service.js +0 -74
  44. package/lib/types/usage/windows.js +0 -84
package/lib/index.js CHANGED
@@ -42702,31 +42702,32 @@ function parseGoDocument(markdown) {
42702
42702
  }
42703
42703
  return { quotas, protocols };
42704
42704
  }
42705
- async function fetchGoDocument(signal) {
42706
- const failures = [];
42707
- for (const url of GO_DOC_URLS) {
42708
- try {
42709
- const timeout = AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS);
42710
- const response = await fetch(url, {
42711
- redirect: "error",
42712
- headers: { ...attributionHeaders2(), accept: "text/plain", "cache-control": "no-cache" },
42713
- signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout])
42705
+ async function readSource(url, signal) {
42706
+ const timeout = AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS);
42707
+ try {
42708
+ const response = await fetch(url, {
42709
+ redirect: "error",
42710
+ headers: { ...attributionHeaders2(), accept: "text/plain", "cache-control": "no-cache" },
42711
+ signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout])
42712
+ });
42713
+ if (!response.ok) {
42714
+ await response.body?.cancel().catch(() => {
42714
42715
  });
42715
- if (!response.ok) {
42716
- await response.body?.cancel().catch(() => {
42717
- });
42718
- failures.push(`${url} answered HTTP ${response.status}`);
42719
- continue;
42720
- }
42721
- const document = parseGoDocument(await readBoundedText(response, GO_DOC_MAX_BYTES));
42722
- if (document.quotas.size === 0) {
42723
- failures.push(`${url} carried no allowance table`);
42724
- continue;
42725
- }
42726
- return document;
42727
- } catch (error) {
42728
- failures.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
42716
+ return { failure: `${url} answered HTTP ${response.status}` };
42729
42717
  }
42718
+ const document = parseGoDocument(await readBoundedText(response, GO_DOC_MAX_BYTES));
42719
+ if (document.quotas.size === 0) return { failure: `${url} carried no allowance table` };
42720
+ return { document };
42721
+ } catch (error) {
42722
+ return { failure: `${url}: ${error instanceof Error ? error.message : String(error)}` };
42723
+ }
42724
+ }
42725
+ async function fetchGoDocument(signal) {
42726
+ const attempts = await Promise.all(GO_DOC_URLS.map((url) => readSource(url, signal)));
42727
+ const failures = [];
42728
+ for (const attempt of attempts) {
42729
+ if ("document" in attempt) return attempt.document;
42730
+ failures.push(attempt.failure);
42730
42731
  }
42731
42732
  throw new LlmError3(
42732
42733
  `could not read the Go documentation (${failures.join("; ")})`,
@@ -42996,12 +42997,19 @@ var OpencodeGoCatalog = class {
42996
42997
  constructor(options) {
42997
42998
  this.options = options;
42998
42999
  }
42999
- /** The cached snapshot, refreshed when it is older than the configured TTL. */
43000
- snapshot(force = false) {
43000
+ /**
43001
+ * The cached snapshot, refreshed when it is older than the configured TTL.
43002
+ * @param force - revalidate every source, ignoring the cache.
43003
+ * @param signal - caller cancellation, forwarded to the documentation read,
43004
+ * the slowest source and the only one a caller can cancel; a build already
43005
+ * in flight keeps the signal of the caller that started it, because one
43006
+ * shared snapshot cannot carry two different cancellations.
43007
+ */
43008
+ snapshot(force = false, signal) {
43001
43009
  if (!force && this.served !== void 0 && Date.now() - this.served.fetchedAtMs < this.options.refreshMs) {
43002
43010
  return Promise.resolve(this.served);
43003
43011
  }
43004
- this.pending ??= this.build().then((snapshot2) => {
43012
+ this.pending ??= this.build(signal).then((snapshot2) => {
43005
43013
  this.served = snapshot2;
43006
43014
  return snapshot2;
43007
43015
  }).finally(() => {
@@ -43015,14 +43023,15 @@ var OpencodeGoCatalog = class {
43015
43023
  * A model this build has never seen forces one revalidation even inside the
43016
43024
  * TTL, because "unknown" and "newly added" look identical from here.
43017
43025
  * @param id - the requested model id.
43026
+ * @param signal - caller cancellation, forwarded to a forced revalidation.
43018
43027
  * @returns the snapshot holding the model.
43019
43028
  * @throws {LlmError} `MODEL_METADATA_UNAVAILABLE` when the gateway advertises
43020
43029
  * the id but this build cannot configure it, naming the reason.
43021
43030
  */
43022
- async forModel(id) {
43031
+ async forModel(id, signal) {
43023
43032
  let snapshot2 = await this.snapshot();
43024
43033
  if (!snapshot2.facts.has(id) && !snapshot2.unavailable.has(id)) {
43025
- snapshot2 = await this.snapshot(true);
43034
+ snapshot2 = await this.snapshot(true, signal);
43026
43035
  }
43027
43036
  const reason = snapshot2.unavailable.get(id);
43028
43037
  if (reason !== void 0) {
@@ -43033,14 +43042,17 @@ var OpencodeGoCatalog = class {
43033
43042
  }
43034
43043
  return snapshot2;
43035
43044
  }
43036
- /** Revalidate every source, reusing the metadata document when it is unchanged. */
43037
- async build() {
43045
+ /**
43046
+ * Revalidate every source, reusing the metadata document when it is unchanged.
43047
+ * @param signal - caller cancellation, forwarded to the documentation read.
43048
+ */
43049
+ async build(signal) {
43038
43050
  const builtin = builtinModels(this.options.baseURL);
43039
43051
  const readDocument = this.options.readDocument ?? fetchGoDocument;
43040
43052
  const [listing, metadata, documentation] = await Promise.allSettled([
43041
43053
  fetchModelIds(this.options.baseURL),
43042
43054
  this.refreshMetadata(builtin),
43043
- readDocument()
43055
+ readDocument(signal)
43044
43056
  ]);
43045
43057
  if (metadata.status === "rejected") {
43046
43058
  this.options.observers?.onFallback?.({
@@ -43206,8 +43218,8 @@ function inferFamily(id) {
43206
43218
  const lower = id.toLowerCase();
43207
43219
  return ["grok", "gpt", "muse"].some((family) => lower.startsWith(family)) ? "openai-responses" : "openai-completions";
43208
43220
  }
43209
- async function discoverCatalogModels(catalog) {
43210
- const snapshot2 = await catalog.snapshot(true);
43221
+ async function discoverCatalogModels(catalog, signal) {
43222
+ const snapshot2 = await catalog.snapshot(true, signal);
43211
43223
  if (!snapshot2.live) {
43212
43224
  const detail = snapshot2.listingFailure instanceof LlmError4 ? snapshot2.listingFailure.message : "the live model listing is unreachable";
43213
43225
  throw new LlmError4(`dshopencodego: ${detail}; refresh the model list to retry`, "DISCOVERY_FAILED", {
@@ -44129,6 +44141,7 @@ function readConfig(config) {
44129
44141
  return field?.get === void 0 ? [] : [[key, field.get()]];
44130
44142
  }));
44131
44143
  }
44144
+ var PLAIN_HTTP_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
44132
44145
  function assertBaseURL(raw) {
44133
44146
  let url;
44134
44147
  try {
@@ -44139,6 +44152,11 @@ function assertBaseURL(raw) {
44139
44152
  if (url.protocol !== "https:" && url.protocol !== "http:") {
44140
44153
  throw new Error(`dshopencodego: baseURL "${raw}" must be http or https`);
44141
44154
  }
44155
+ if (url.protocol === "http:" && !PLAIN_HTTP_HOSTS.has(url.hostname)) {
44156
+ throw new Error(
44157
+ `dshopencodego: baseURL "${raw}" must use https outside localhost \u2014 a plain-http endpoint would send the API key in clear text`
44158
+ );
44159
+ }
44142
44160
  if (url.search.length > 0 || url.hash.length > 0) {
44143
44161
  throw new Error(`dshopencodego: baseURL "${raw}" must not carry a query or fragment`);
44144
44162
  }
@@ -44153,6 +44171,10 @@ import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
44153
44171
 
44154
44172
  // src/conversion/replay.ts
44155
44173
  import { LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
44174
+ var REPLAYABLE_STOP_REASONS = ["stop", "length", "toolUse", "error", "aborted"];
44175
+ function replayableStopReason(stopReason) {
44176
+ return REPLAYABLE_STOP_REASONS.includes(stopReason) ? stopReason : "error";
44177
+ }
44156
44178
  function parseArguments(raw) {
44157
44179
  try {
44158
44180
  const parsed = JSON.parse(raw);
@@ -44184,7 +44206,7 @@ function toPiReplayState(message, requestedModel = message.model) {
44184
44206
  ...responseModel === void 0 ? {} : { responseModel },
44185
44207
  ...message.responseId === void 0 ? {} : { responseId: message.responseId },
44186
44208
  ...message.providerThinkingLevel === void 0 ? {} : { providerThinkingLevel: message.providerThinkingLevel },
44187
- stopReason: message.stopReason
44209
+ stopReason: replayableStopReason(message.stopReason)
44188
44210
  };
44189
44211
  return {
44190
44212
  response,
@@ -44226,7 +44248,7 @@ function readReplayState(value) {
44226
44248
  for (const key of ["api", "provider", "model"]) {
44227
44249
  if (typeof response[key] !== "string" || response[key].length === 0) return invalidReplay(`${key} must be a non-empty string`);
44228
44250
  }
44229
- if (!["stop", "length", "toolUse", "error", "aborted"].includes(String(response["stopReason"]))) {
44251
+ if (!REPLAYABLE_STOP_REASONS.includes(String(response["stopReason"]))) {
44230
44252
  return invalidReplay("unknown stopReason");
44231
44253
  }
44232
44254
  for (const key of ["responseModel", "responseId", "providerThinkingLevel"]) {
@@ -44804,6 +44826,10 @@ function isModelEnabled(model, visibility) {
44804
44826
  if (typeof explicit === "boolean") return explicit;
44805
44827
  return typeof model.recommended === "boolean" ? model.recommended : model.deprecated !== true;
44806
44828
  }
44829
+ function unadvertisedVisibilityIds(visibility, advertised) {
44830
+ const known = new Set(advertised);
44831
+ return Object.keys(visibility).filter((id) => !known.has(id)).sort();
44832
+ }
44807
44833
  function validReleaseDate2(value) {
44808
44834
  return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
44809
44835
  }
@@ -44834,6 +44860,85 @@ function providerHeaders(sessionId) {
44834
44860
  };
44835
44861
  }
44836
44862
 
44863
+ // src/usage/meter.ts
44864
+ function usableRates(cost) {
44865
+ if (cost === void 0) return void 0;
44866
+ if (!(cost.input > 0) && !(cost.output > 0)) return void 0;
44867
+ return {
44868
+ input: cost.input,
44869
+ output: cost.output,
44870
+ ...cost.cacheRead > 0 ? { cacheRead: cost.cacheRead } : {},
44871
+ ...cost.cacheWrite > 0 ? { cacheWrite: cost.cacheWrite } : {}
44872
+ };
44873
+ }
44874
+ var EMPTY3 = {
44875
+ calls: 0,
44876
+ inputTokens: 0,
44877
+ outputTokens: 0,
44878
+ cacheReadTokens: 0,
44879
+ cacheWriteTokens: 0,
44880
+ totalTokens: 0
44881
+ };
44882
+ function spendOf(usage, rates2) {
44883
+ const fallback = rates2.input > 0 ? rates2.input : rates2.output;
44884
+ const cacheRead = usage.cacheReadTokens ?? 0;
44885
+ const cacheWrite = usage.cacheWriteTokens ?? 0;
44886
+ return (usage.inputTokens * rates2.input + usage.outputTokens * rates2.output + cacheRead * (rates2.cacheRead ?? fallback) + cacheWrite * (rates2.cacheWrite ?? fallback)) / 1e6;
44887
+ }
44888
+ function add(totals, usage) {
44889
+ const inputTokens = totals.inputTokens + usage.inputTokens;
44890
+ const outputTokens = totals.outputTokens + usage.outputTokens;
44891
+ const cacheReadTokens = totals.cacheReadTokens + (usage.cacheReadTokens ?? 0);
44892
+ const cacheWriteTokens = totals.cacheWriteTokens + (usage.cacheWriteTokens ?? 0);
44893
+ const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
44894
+ const reasoningTokens = (totals.reasoningTokens ?? 0) + (usage.reasoningTokens ?? 0);
44895
+ const rates2 = usage.rates;
44896
+ const billedTokens = usage.inputTokens + usage.outputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
44897
+ const unpricedTokens = (totals.unpricedTokens ?? 0) + (rates2 === void 0 ? billedTokens : 0);
44898
+ const estimatedUSD = rates2 === void 0 ? totals.estimatedUSD : (totals.estimatedUSD ?? 0) + spendOf(usage, rates2);
44899
+ return {
44900
+ calls: totals.calls + 1,
44901
+ inputTokens,
44902
+ outputTokens,
44903
+ cacheReadTokens,
44904
+ cacheWriteTokens,
44905
+ // The four counts are disjoint (uncached input, cached input, output), so
44906
+ // their sum is the billed total. The provider's own `totalTokens` is not
44907
+ // used: it is optional, and mixing the two would double-count a cache read.
44908
+ totalTokens,
44909
+ ...reasoningTokens > 0 ? { reasoningTokens } : {},
44910
+ ...estimatedUSD === void 0 ? {} : { estimatedUSD },
44911
+ // Only meaningful next to an estimate: a reading with no priced call at all
44912
+ // says so by omitting `estimatedUSD`, and naming every token as "unpriced"
44913
+ // there would just restate the total.
44914
+ ...estimatedUSD === void 0 || unpricedTokens === 0 ? {} : { unpricedTokens }
44915
+ };
44916
+ }
44917
+ var UsageMeter = class {
44918
+ sinceMs = Date.now();
44919
+ totals = EMPTY3;
44920
+ byModel = /* @__PURE__ */ new Map();
44921
+ /**
44922
+ * Record one completed call.
44923
+ * @param model - the model id the call was made with.
44924
+ * @param usage - the provider's own usage for that call, with the rates that
44925
+ * were published for the model at the time it ran.
44926
+ */
44927
+ record(model, usage) {
44928
+ this.totals = add(this.totals, usage);
44929
+ this.byModel.set(model, add(this.byModel.get(model) ?? EMPTY3, usage));
44930
+ }
44931
+ /** The current reading, busiest model first. */
44932
+ snapshot(now = Date.now()) {
44933
+ return {
44934
+ sinceMs: this.sinceMs,
44935
+ atMs: now,
44936
+ totals: this.totals,
44937
+ models: [...this.byModel.entries()].map(([model, totals]) => ({ model, ...totals })).sort((left, right) => right.totalTokens - left.totalTokens || left.model.localeCompare(right.model))
44938
+ };
44939
+ }
44940
+ };
44941
+
44837
44942
  // src/adapter.ts
44838
44943
  var DISABLES_THINKING_WHEN_UNSET = /* @__PURE__ */ new Set(["deepseek", "zai", "qwen", "qwen-chat-template"]);
44839
44944
  function withModelLimit(model, limits) {
@@ -44853,6 +44958,13 @@ function stopSequencesFor(model, stop) {
44853
44958
  "UNSUPPORTED_OPTION"
44854
44959
  );
44855
44960
  }
44961
+ async function* observingTerminalUsage(events, observe) {
44962
+ for await (const event of events) {
44963
+ if (event.type === "done") observe(event.message.usage);
44964
+ else if (event.type === "error") observe(event.error.usage);
44965
+ yield event;
44966
+ }
44967
+ }
44856
44968
  var OpencodeGoAdapter = class extends LlmAdapter {
44857
44969
  cache;
44858
44970
  options;
@@ -44888,6 +45000,28 @@ var OpencodeGoAdapter = class extends LlmAdapter {
44888
45000
  providerInfo(provider) {
44889
45001
  return { id: provider, name: DISPLAY_NAME };
44890
45002
  }
45003
+ /**
45004
+ * This route declares no request-image pricing, and that is a decision
45005
+ * rather than an omission.
45006
+ *
45007
+ * `priceImages` must answer with the visual tokens the wire actually carries
45008
+ * for every image occurrence, and nothing available to this plugin can
45009
+ * answer that. The route fronts several model families behind three
45010
+ * protocols whose vision accounting differs and is not published by the
45011
+ * gateway; the catalog carries text rates only (`ModelCost` is input,
45012
+ * output, cache read and cache write) and states no vision rule; and the one
45013
+ * implementation shipped upstream anchors on a single provider's own
45014
+ * published accounting (`dsh-llm-deepseek`). An invented number would not
45015
+ * fail loudly: the token meter sums these values into its surface estimate
45016
+ * and the retention policy spends real budget against them, so a wrong
45017
+ * answer is worse than the contract's own default — declaring none, which
45018
+ * leaves every consumer on its neutral estimate. Revisit this once the
45019
+ * gateway's per-family visual accounting is known and checked against the
45020
+ * usage it reports.
45021
+ */
45022
+ imageRequestPricing(_provider, _model) {
45023
+ return void 0;
45024
+ }
44891
45025
  async listModels(_provider) {
44892
45026
  const config = this.options.config();
44893
45027
  const snapshot2 = await this.catalogOf(config).snapshot();
@@ -44977,6 +45111,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
44977
45111
  const facts = snapshot2.facts.get(options.model);
44978
45112
  if (facts === void 0) throw new LlmError8(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
44979
45113
  const model = withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits);
45114
+ const rates2 = usableRates(facts.cost);
44980
45115
  const stop = stopSequencesFor(model, options.stop);
44981
45116
  const ceiling = config.modelLimits[model.id]?.maxTokens;
44982
45117
  const maxTokens = options.maxTokens === void 0 || ceiling === null || ceiling === void 0 ? options.maxTokens : Math.min(options.maxTokens, ceiling);
@@ -44988,6 +45123,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
44988
45123
  const consumer = new AbortController();
44989
45124
  const upstream = options.signal === void 0 ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]);
44990
45125
  const watchdog = __using(_stack, idleWatchdog(upstream, config.streamIdleTimeoutMs, "LLM_STREAM_IDLE_TIMEOUT"));
45126
+ let timedOut = false;
44991
45127
  try {
44992
45128
  const containsImage = options.messages.some((message) => contentHasImage2(message.content));
44993
45129
  if (containsImage && !model.input.includes("image")) {
@@ -45008,7 +45144,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
45008
45144
  };
45009
45145
  }
45010
45146
  const context = imageRequest === void 0 ? await toPiContext(options, void 0, this.options.onReplayDegrade) : await toPiContext({ ...options, signal: watchdog.signal }, imageRequest, this.options.onReplayDegrade);
45011
- const events = snapshot2.provider.streamSimple(model, normalizeContext(context), {
45147
+ let reportedReasoning;
45148
+ const events = observingTerminalUsage(snapshot2.provider.streamSimple(model, normalizeContext(context), {
45012
45149
  apiKey,
45013
45150
  ...reasoning === void 0 || reasoning === "off" ? {} : { reasoning },
45014
45151
  ...options.temperature === void 0 ? {} : { temperature: options.temperature },
@@ -45025,6 +45162,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
45025
45162
  // The agent recovery layer owns visible attempts; one adapter call is
45026
45163
  // one SDK attempt.
45027
45164
  maxRetries: 0
45165
+ }), (usage) => {
45166
+ reportedReasoning = usage.reasoning;
45028
45167
  });
45029
45168
  const iterator = toStreamChunks(events, model.contextWindow, options.signal, model.id)[Symbol.asyncIterator]();
45030
45169
  let exhausted = false;
@@ -45039,21 +45178,32 @@ var OpencodeGoAdapter = class extends LlmAdapter {
45039
45178
  return;
45040
45179
  }
45041
45180
  if (result.value.type === "usage") {
45042
- this.options.onUsage?.({ model: options.model, usage: result.value.usage });
45181
+ this.options.onUsage?.({
45182
+ model: options.model,
45183
+ usage: {
45184
+ ...result.value.usage,
45185
+ // pi-ai reports this as part of `output`, and `mapUsage` drops
45186
+ // it; the meter records it without adding it to a total.
45187
+ ...reportedReasoning === void 0 || reportedReasoning <= 0 ? {} : { reasoningTokens: reportedReasoning },
45188
+ ...rates2 === void 0 ? {} : { rates: rates2 }
45189
+ }
45190
+ });
45043
45191
  }
45044
45192
  yield result.value;
45045
45193
  }
45046
45194
  } finally {
45047
45195
  if (!exhausted) {
45196
+ timedOut = timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0;
45048
45197
  consumer.abort("opencode-go stream consumer stopped");
45049
45198
  try {
45050
- await iterator.return(void 0);
45199
+ void Promise.resolve(iterator.return(void 0)).catch(() => {
45200
+ });
45051
45201
  } catch (_abortedSdkTeardown) {
45052
45202
  }
45053
45203
  }
45054
45204
  }
45055
45205
  } catch (error) {
45056
- if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
45206
+ if (timedOut || timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
45057
45207
  throw new LlmError8("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
45058
45208
  }
45059
45209
  if (options.signal?.aborted) {
@@ -45266,13 +45416,20 @@ function parseTotals(value) {
45266
45416
  if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 0) throw new Error(`invalid meter field "${key}"`);
45267
45417
  return entry;
45268
45418
  };
45419
+ const optionalCount = (key) => row[key] === void 0 ? void 0 : count(key);
45420
+ const reasoningTokens = optionalCount("reasoningTokens");
45421
+ const estimatedUSD = optionalCount("estimatedUSD");
45422
+ const unpricedTokens = optionalCount("unpricedTokens");
45269
45423
  return {
45270
45424
  calls: count("calls"),
45271
45425
  inputTokens: count("inputTokens"),
45272
45426
  outputTokens: count("outputTokens"),
45273
45427
  cacheReadTokens: count("cacheReadTokens"),
45274
45428
  cacheWriteTokens: count("cacheWriteTokens"),
45275
- totalTokens: count("totalTokens")
45429
+ totalTokens: count("totalTokens"),
45430
+ ...reasoningTokens === void 0 ? {} : { reasoningTokens },
45431
+ ...estimatedUSD === void 0 ? {} : { estimatedUSD },
45432
+ ...unpricedTokens === void 0 ? {} : { unpricedTokens }
45276
45433
  };
45277
45434
  }
45278
45435
  function parseGoMeter(value) {
@@ -45397,6 +45554,8 @@ function catalogReading(snapshot2, visibility, listingFailure, now) {
45397
45554
  // src/catalog/service.ts
45398
45555
  var OpencodeGoCatalogService = class extends TypertRemoteService {
45399
45556
  options;
45557
+ /** The last reported set of unadvertised switches, as a comparable fact. */
45558
+ reportedUnadvertised;
45400
45559
  constructor(ctx, options) {
45401
45560
  super(ctx, "opencodeGoCatalog");
45402
45561
  this.options = options;
@@ -45411,15 +45570,49 @@ var OpencodeGoCatalogService = class extends TypertRemoteService {
45411
45570
  }
45412
45571
  async project(force) {
45413
45572
  const catalog = this.options.catalog();
45414
- const snapshot2 = await catalog.snapshot(force);
45415
- const failure = snapshot2.listingFailure;
45416
- return catalogReading(
45417
- snapshot2,
45418
- this.options.visibility(),
45419
- failure === void 0 ? void 0 : failure instanceof LlmError9 || failure instanceof Error ? failure.message : String(failure)
45420
- );
45573
+ try {
45574
+ const snapshot2 = await catalog.snapshot(force);
45575
+ const failure = snapshot2.listingFailure;
45576
+ const visibility = this.options.visibility();
45577
+ const reading = catalogReading(
45578
+ snapshot2,
45579
+ visibility,
45580
+ failure === void 0 ? void 0 : failure instanceof LlmError9 || failure instanceof Error ? failure.message : String(failure)
45581
+ );
45582
+ this.reportUnadvertised(snapshot2, visibility);
45583
+ return reading;
45584
+ } catch (error) {
45585
+ throw catalogFailure(error);
45586
+ }
45587
+ }
45588
+ /**
45589
+ * Report switches for ids the gateway never advertised.
45590
+ *
45591
+ * `modelVisibility` is per-id configuration, and an entry naming a model the
45592
+ * gateway does not serve has no effect anywhere — the picker cannot show a
45593
+ * model that is not in the listing, so without this the switch is silent.
45594
+ * (The reverse case, advertised but unbuildable, is `onUnconfigured`.)
45595
+ *
45596
+ * Only a live listing is evidence of absence: a retained snapshot after an
45597
+ * outage would accuse models that merely could not be re-read. The set is
45598
+ * compared as a fact, so re-reading the page does not repeat the warning.
45599
+ * @param snapshot - the snapshot the reading was projected from.
45600
+ * @param visibility - the switches in force for this read.
45601
+ */
45602
+ reportUnadvertised(snapshot2, visibility) {
45603
+ const missing = snapshot2.live && visibility !== void 0 ? unadvertisedVisibilityIds(visibility, [...snapshot2.facts.keys(), ...snapshot2.unavailable.keys()]) : [];
45604
+ const facts = JSON.stringify(missing);
45605
+ if (facts === this.reportedUnadvertised) return;
45606
+ this.reportedUnadvertised = facts;
45607
+ if (missing.length > 0) this.options.onUnadvertised?.(missing);
45421
45608
  }
45422
45609
  };
45610
+ function catalogFailure(error) {
45611
+ return new RemoteError("dshopencodego/catalog-unavailable", error instanceof Error ? error.message : "The OpenCode Go catalog is unavailable", {
45612
+ retryable: true,
45613
+ retainPrevious: true
45614
+ }, { cause: error });
45615
+ }
45423
45616
 
45424
45617
  // src/update/check.ts
45425
45618
  var PACKAGE_NAME = "@dan-ai-studio/dshopencodego";
@@ -45435,6 +45628,14 @@ function parseVersion(value) {
45435
45628
  prerelease: prerelease === void 0 ? void 0 : prerelease.split(".")
45436
45629
  };
45437
45630
  }
45631
+ function comparePrereleaseIdentifier(left, right) {
45632
+ const leftNumber = /^\d+$/.test(left) ? Number(left) : void 0;
45633
+ const rightNumber = /^\d+$/.test(right) ? Number(right) : void 0;
45634
+ if (leftNumber !== void 0 && rightNumber !== void 0) return leftNumber - rightNumber;
45635
+ if (leftNumber !== void 0) return -1;
45636
+ if (rightNumber !== void 0) return 1;
45637
+ return left < right ? -1 : left > right ? 1 : 0;
45638
+ }
45438
45639
  function compareVersions(left, right) {
45439
45640
  const ours = parseVersion(left);
45440
45641
  const theirs = parseVersion(right);
@@ -45452,7 +45653,8 @@ function compareVersions(left, right) {
45452
45653
  const theirsPart = theirs.prerelease[index];
45453
45654
  if (oursPart === void 0) return -1;
45454
45655
  if (theirsPart === void 0) return 1;
45455
- if (oursPart !== theirsPart) return oursPart < theirsPart ? -1 : 1;
45656
+ const difference = comparePrereleaseIdentifier(oursPart, theirsPart);
45657
+ if (difference !== 0) return difference;
45456
45658
  }
45457
45659
  return 0;
45458
45660
  }
@@ -45505,7 +45707,7 @@ function createUpdateChecker(deps) {
45505
45707
  }
45506
45708
 
45507
45709
  // src/update/version.ts
45508
- var PLUGIN_VERSION = true ? "0.1.43" : "0.0.0-dev";
45710
+ var PLUGIN_VERSION = true ? "0.1.44" : "0.0.0-dev";
45509
45711
 
45510
45712
  // src/update/service.ts
45511
45713
  import { TypertRemoteService as TypertRemoteService2 } from "@deepseek-ai/dsh-typert-protocol";
@@ -45533,56 +45735,6 @@ var OpencodeGoUpdateService = class extends TypertRemoteService2 {
45533
45735
  }
45534
45736
  };
45535
45737
 
45536
- // src/usage/meter.ts
45537
- var EMPTY3 = {
45538
- calls: 0,
45539
- inputTokens: 0,
45540
- outputTokens: 0,
45541
- cacheReadTokens: 0,
45542
- cacheWriteTokens: 0,
45543
- totalTokens: 0
45544
- };
45545
- function add(totals, usage) {
45546
- const inputTokens = totals.inputTokens + usage.inputTokens;
45547
- const outputTokens = totals.outputTokens + usage.outputTokens;
45548
- const cacheReadTokens = totals.cacheReadTokens + (usage.cacheReadTokens ?? 0);
45549
- const cacheWriteTokens = totals.cacheWriteTokens + (usage.cacheWriteTokens ?? 0);
45550
- return {
45551
- calls: totals.calls + 1,
45552
- inputTokens,
45553
- outputTokens,
45554
- cacheReadTokens,
45555
- cacheWriteTokens,
45556
- // The four counts are disjoint (uncached input, cached input, output), so
45557
- // their sum is the billed total. The provider's own `totalTokens` is not
45558
- // used: it is optional, and mixing the two would double-count a cache read.
45559
- totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens
45560
- };
45561
- }
45562
- var UsageMeter = class {
45563
- sinceMs = Date.now();
45564
- totals = EMPTY3;
45565
- byModel = /* @__PURE__ */ new Map();
45566
- /**
45567
- * Record one completed call.
45568
- * @param model - the model id the call was made with.
45569
- * @param usage - the provider's own usage for that call.
45570
- */
45571
- record(model, usage) {
45572
- this.totals = add(this.totals, usage);
45573
- this.byModel.set(model, add(this.byModel.get(model) ?? EMPTY3, usage));
45574
- }
45575
- /** The current reading, busiest model first. */
45576
- snapshot(now = Date.now()) {
45577
- return {
45578
- sinceMs: this.sinceMs,
45579
- atMs: now,
45580
- totals: this.totals,
45581
- models: [...this.byModel.entries()].map(([model, totals]) => ({ model, ...totals })).sort((left, right) => right.totalTokens - left.totalTokens || left.model.localeCompare(right.model))
45582
- };
45583
- }
45584
- };
45585
-
45586
45738
  // src/usage/windows.ts
45587
45739
  import { attributionHeaders as attributionHeaders6, LlmError as LlmError10 } from "@deepseek-ai/dsh-llm";
45588
45740
  var USAGE_MAX_BYTES = 1024 * 1024;
@@ -45697,6 +45849,37 @@ function apply(ctx, raw) {
45697
45849
  const constant = live === void 0 ? PlainConfig(raw ?? {}) : void 0;
45698
45850
  const current = () => live === void 0 ? constant : readConfig(live);
45699
45851
  assertBaseURL(current().baseURL);
45852
+ const settingsNs = ctx.fiber?.entry?.options.id ?? name;
45853
+ let directory;
45854
+ let directoryError;
45855
+ let directoryRetired = false;
45856
+ const directoryEntry = (error) => ({
45857
+ provider: PROVIDER_ID,
45858
+ displayName: DISPLAY_NAME,
45859
+ settingsNs,
45860
+ // This plugin's profile is the entry's whole `config:` mapping — the
45861
+ // bundled `cordis.patch.yml` inserts `id: dshopencodego` with the route's
45862
+ // fields directly under `config`, and `src/config.ts` owns exactly those
45863
+ // fields — so there is no path from the section root to a nested profile.
45864
+ settingsPath: [],
45865
+ defaultBaseURL: DEFAULT_BASE_URL,
45866
+ ...error === void 0 ? {} : { error }
45867
+ });
45868
+ const publishDirectory = (error) => {
45869
+ if (directoryRetired || directory !== void 0 && error === directoryError) return;
45870
+ directoryError = error;
45871
+ const entry = directoryEntry(error);
45872
+ try {
45873
+ if (directory === void 0) directory = ctx.llm.registerConfigurableProviders([entry]);
45874
+ else directory.replace([entry]);
45875
+ } catch (failure) {
45876
+ directoryRetired = true;
45877
+ ctx.logger.error(
45878
+ `dshopencodego: could not publish the "${PROVIDER_ID}" configurable-provider entry; the Models page will not offer this route's model discovery (${String(failure)})`
45879
+ );
45880
+ }
45881
+ };
45882
+ publishDirectory(void 0);
45700
45883
  const resolveApiKey = async () => {
45701
45884
  const ref = current().apiKeyEnv;
45702
45885
  const credentials = ctx.get("credentials");
@@ -45740,7 +45923,10 @@ function apply(ctx, raw) {
45740
45923
  });
45741
45924
  ctx.plugin(OpencodeGoCatalogService, {
45742
45925
  catalog: () => adapter.catalogOf(current()),
45743
- visibility: () => current().modelVisibility
45926
+ visibility: () => current().modelVisibility,
45927
+ onUnadvertised: (ids) => {
45928
+ ctx.logger.warn(`dshopencodego: modelVisibility configures models the gateway does not advertise: ${ids.join(", ")}; those switches have no effect until the gateway publishes them`);
45929
+ }
45744
45930
  });
45745
45931
  ctx.plugin(OpencodeGoUpdateService, { currentVersion: PLUGIN_VERSION });
45746
45932
  let registration;
@@ -45762,38 +45948,50 @@ function apply(ctx, raw) {
45762
45948
  ctx.logger.info("dshopencodego: disabled by configuration; the route and its models are withdrawn");
45763
45949
  }
45764
45950
  }
45951
+ publishDirectory(enabled && !credentialConfigured ? `the route is withheld until ${current().apiKeyEnv} is stored through the credentials service` : void 0);
45765
45952
  };
45766
45953
  const syncRoute = () => {
45767
- const visibility = JSON.stringify(current().modelVisibility);
45768
- if (visibility !== visibilityFacts) {
45769
- visibilityFacts = visibility;
45770
- registration?.replace([PROVIDER_ID]);
45771
- }
45772
- const credentials = ctx.get("credentials");
45773
- const ref = current().apiKeyEnv;
45774
- if (credentials === void 0) {
45775
- applyRoute(launchEnvironmentOf(ctx).get(ref)?.value !== void 0);
45776
- return;
45954
+ try {
45955
+ const visibility = JSON.stringify(current().modelVisibility);
45956
+ if (visibility !== visibilityFacts) {
45957
+ visibilityFacts = visibility;
45958
+ registration?.replace([PROVIDER_ID]);
45959
+ }
45960
+ const credentials = ctx.get("credentials");
45961
+ const ref = current().apiKeyEnv;
45962
+ if (credentials === void 0) {
45963
+ applyRoute(launchEnvironmentOf(ctx).get(ref)?.value !== void 0);
45964
+ return;
45965
+ }
45966
+ void credentials.describe(credentialRef(ref)).then((info) => {
45967
+ if (ref === current().apiKeyEnv) applyRoute(info.configured);
45968
+ }).catch((error) => {
45969
+ ctx.logger.error(`dshopencodego: credential describe failed; keeping the previous route state (${String(error)})`);
45970
+ });
45971
+ } catch (error) {
45972
+ ctx.logger.error(
45973
+ `dshopencodego: could not re-evaluate the route after a configuration change; keeping the previous route state (${String(error)})`
45974
+ );
45777
45975
  }
45778
- void credentials.describe(credentialRef(ref)).then((info) => {
45779
- if (ref === current().apiKeyEnv) applyRoute(info.configured);
45780
- }).catch((error) => {
45781
- ctx.logger.error(`dshopencodego: credential describe failed; keeping the previous route state (${String(error)})`);
45782
- });
45783
45976
  };
45784
45977
  syncRoute();
45785
- const undiscover = ctx.llm.registerModelDiscovery(name, async (request) => {
45786
- if (request.provider !== PROVIDER_ID && !(request.baseURL ?? "").includes("opencode.ai")) {
45787
- throw new LlmError12(
45788
- "dshopencodego discovers only OpenCode Go/Console endpoints; enter this provider's models by hand",
45789
- "DISCOVERY_UNSUPPORTED"
45790
- );
45978
+ const undiscover = ctx.llm.registerModelDiscovery(
45979
+ settingsNs,
45980
+ async (request, signal) => {
45981
+ if (request.provider !== PROVIDER_ID && !(request.baseURL ?? "").includes("opencode.ai")) {
45982
+ throw new LlmError12(
45983
+ "dshopencodego discovers only OpenCode Go/Console endpoints; enter this provider's models by hand",
45984
+ "DISCOVERY_UNSUPPORTED"
45985
+ );
45986
+ }
45987
+ return discoverCatalogModels(adapter.catalogOf(current()), signal);
45791
45988
  }
45792
- return discoverCatalogModels(adapter.catalogOf(current()));
45793
- });
45989
+ );
45794
45990
  ctx.effect(() => () => {
45795
45991
  registration?.();
45796
45992
  undiscover();
45993
+ directory?.();
45994
+ directoryRetired = true;
45797
45995
  });
45798
45996
  ctx.on("internal/config", function(_raw, next) {
45799
45997
  const value = next();