@blockrun/llm 3.21.0 → 3.22.0

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/dist/index.js CHANGED
@@ -313,6 +313,122 @@ var ApiKeyAuth = class {
313
313
  }
314
314
  };
315
315
 
316
+ // src/search-wire.ts
317
+ function put(out, key, value) {
318
+ if (value !== void 0) out[key] = value;
319
+ }
320
+ function handles(list) {
321
+ return list?.map((h) => h.trim().replace(/^@+/, ""));
322
+ }
323
+ function sourceToWire(source) {
324
+ const raw = source;
325
+ const out = { ...raw };
326
+ for (const key of Object.keys(out)) if (/[A-Z]/.test(key)) delete out[key];
327
+ switch (source.type) {
328
+ case "x":
329
+ put(out, "included_x_handles", handles(source.includedXHandles) ?? out.included_x_handles);
330
+ put(out, "excluded_x_handles", handles(source.excludedXHandles) ?? out.excluded_x_handles);
331
+ put(out, "post_favorite_count", source.postFavoriteCount);
332
+ put(out, "post_view_count", source.postViewCount);
333
+ put(out, "enable_image_understanding", source.enableImageUnderstanding);
334
+ put(out, "enable_video_understanding", source.enableVideoUnderstanding);
335
+ break;
336
+ case "web":
337
+ put(out, "country", source.country);
338
+ put(out, "allowed_websites", source.allowedWebsites);
339
+ put(out, "excluded_websites", source.excludedWebsites);
340
+ put(out, "safe_search", source.safeSearch);
341
+ put(out, "enable_image_understanding", source.enableImageUnderstanding);
342
+ break;
343
+ case "news":
344
+ put(out, "country", source.country);
345
+ put(out, "allowed_websites", source.allowedWebsites);
346
+ put(out, "excluded_websites", source.excludedWebsites);
347
+ put(out, "safe_search", source.safeSearch);
348
+ break;
349
+ case "rss":
350
+ put(out, "links", source.links);
351
+ break;
352
+ }
353
+ return out;
354
+ }
355
+ function toWireSearchParameters(sp) {
356
+ const raw = sp;
357
+ const out = {};
358
+ for (const [key, value] of Object.entries(raw)) if (!/[A-Z]/.test(key) && key !== "sources") out[key] = value;
359
+ put(out, "mode", sp.mode);
360
+ if (sp.sources) out.sources = sp.sources.map(sourceToWire);
361
+ put(out, "return_citations", sp.returnCitations);
362
+ put(out, "from_date", sp.fromDate);
363
+ put(out, "to_date", sp.toDate);
364
+ put(out, "max_search_results", sp.maxSearchResults);
365
+ put(out, "max_turns", sp.maxTurns);
366
+ return out;
367
+ }
368
+ function toWireTools(tools) {
369
+ return tools.map((tool) => {
370
+ if (tool.type === "x_search") {
371
+ const out = { type: "x_search" };
372
+ put(out, "allowed_x_handles", handles(tool.allowedXHandles));
373
+ put(out, "excluded_x_handles", handles(tool.excludedXHandles));
374
+ put(out, "from_date", tool.fromDate);
375
+ put(out, "to_date", tool.toDate);
376
+ put(out, "enable_image_understanding", tool.enableImageUnderstanding);
377
+ put(out, "enable_video_understanding", tool.enableVideoUnderstanding);
378
+ return out;
379
+ }
380
+ if (tool.type === "web_search") {
381
+ const out = { type: "web_search" };
382
+ put(out, "allowed_domains", tool.allowedDomains);
383
+ put(out, "excluded_domains", tool.excludedDomains);
384
+ put(out, "enable_image_understanding", tool.enableImageUnderstanding);
385
+ return out;
386
+ }
387
+ return tool;
388
+ });
389
+ }
390
+
391
+ // src/receipt.ts
392
+ function atomicToUsd(amount) {
393
+ return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
394
+ }
395
+ function receiptChargedUsd(receipt) {
396
+ return atomicToUsd(receipt?.extra?.chargedAmount) ?? atomicToUsd(receipt?.amount);
397
+ }
398
+ function decode(header) {
399
+ try {
400
+ const json = typeof Buffer !== "undefined" ? Buffer.from(header, "base64").toString("utf8") : atob(header);
401
+ const parsed = JSON.parse(json);
402
+ return parsed && typeof parsed === "object" ? parsed : void 0;
403
+ } catch {
404
+ return void 0;
405
+ }
406
+ }
407
+ function readSettlement(response, quotedUsd, scheme) {
408
+ const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
409
+ const receipt = header ? decode(header) : void 0;
410
+ const extra = receipt?.extra;
411
+ const costUsd = receiptChargedUsd({
412
+ amount: receipt?.amount,
413
+ extra: extra && typeof extra === "object" ? extra : void 0
414
+ }) ?? quotedUsd;
415
+ const receiptTx = typeof receipt?.transaction === "string" && receipt.transaction ? receipt.transaction : void 0;
416
+ const transaction = receiptTx ?? (response.headers.get("X-Payment-Receipt")?.trim() || void 0);
417
+ const settlement = {
418
+ scheme,
419
+ quotedUsd,
420
+ ...transaction ? { transaction } : {},
421
+ ...typeof receipt?.network === "string" ? { network: receipt.network } : {},
422
+ ...receipt?.success === false ? { success: false } : {}
423
+ };
424
+ return { costUsd, settlement };
425
+ }
426
+ function withCost(body, response, bookedUsd, scheme) {
427
+ body.costUsd = bookedUsd;
428
+ body.settlement = readSettlement(response, bookedUsd, scheme).settlement;
429
+ return body;
430
+ }
431
+
316
432
  // src/client.ts
317
433
  import { privateKeyToAccount } from "viem/accounts";
318
434
 
@@ -4099,6 +4215,11 @@ function errSummary(err) {
4099
4215
  }
4100
4216
  return String(err).slice(0, 100);
4101
4217
  }
4218
+ function requestsSearch(options) {
4219
+ if (options.tools?.some((tool) => tool.type === "x_search" || tool.type === "web_search")) return true;
4220
+ if (options.searchParameters !== void 0) return options.searchParameters.mode !== "off";
4221
+ return options.search === true;
4222
+ }
4102
4223
  function routingProfileForModel(model) {
4103
4224
  return AUTO_ROUTING_PROFILES[model.toLowerCase()];
4104
4225
  }
@@ -4126,7 +4247,7 @@ function routingText(messages) {
4126
4247
  };
4127
4248
  }
4128
4249
  function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, options = {}) {
4129
- const tools = options.tools ?? [];
4250
+ const tools = (options.tools ?? []).filter((tool) => tool.type === "function");
4130
4251
  const requiresTools = options.toolChoice === "none" ? false : options.toolChoice === "required" || typeof options.toolChoice === "object" ? true : void 0;
4131
4252
  const decision = route(prompt, systemPrompt, maxOutputTokens, {
4132
4253
  config: DEFAULT_ROUTING_CONFIG,
@@ -4146,6 +4267,15 @@ function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, o
4146
4267
  const resolved = !id.startsWith("free/") ? id : modelPricing.has(`nvidia/${id.slice(5)}`) ? `nvidia/${id.slice(5)}` : null;
4147
4268
  if (resolved && !callable.includes(resolved)) callable.push(resolved);
4148
4269
  }
4270
+ if (requestsSearch(options)) {
4271
+ const searchable = callable.filter((id) => id.startsWith("xai/"));
4272
+ if (searchable.length === 0) {
4273
+ throw new Error(
4274
+ `Search (x_search / web_search tools or searchParameters) runs on xai/* models only, and the ${options.routingProfile ?? "auto"} route has no xai/* candidate for this request. Call an xai/* model directly (e.g. xai/grok-4.3) or drop the search options.`
4275
+ );
4276
+ }
4277
+ callable.splice(0, callable.length, ...searchable);
4278
+ }
4149
4279
  const estimatedInputTokens = Math.ceil(
4150
4280
  Math.max(options.conversationChars ?? 0, `${systemPrompt ?? ""} ${prompt}`.length) / 4
4151
4281
  );
@@ -4564,7 +4694,7 @@ function getCostSummary() {
4564
4694
  }
4565
4695
 
4566
4696
  // src/version.ts
4567
- var SDK_VERSION = "3.21.0";
4697
+ var SDK_VERSION = "3.22.0";
4568
4698
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4569
4699
 
4570
4700
  // src/client.ts
@@ -4710,7 +4840,9 @@ var LLMClient = class _LLMClient {
4710
4840
  options?.maxOutputTokens ?? options?.maxTokens ?? DEFAULT_MAX_TOKENS,
4711
4841
  {
4712
4842
  routingProfile: options?.routingProfile,
4713
- requiresStructuredOutput: options?.responseFormat !== void 0
4843
+ requiresStructuredOutput: options?.responseFormat !== void 0,
4844
+ search: options?.search,
4845
+ searchParameters: options?.searchParameters
4714
4846
  }
4715
4847
  );
4716
4848
  }
@@ -4777,6 +4909,8 @@ var LLMClient = class _LLMClient {
4777
4909
  requiresStructuredOutput: options.responseFormat !== void 0,
4778
4910
  tools: options.tools,
4779
4911
  toolChoice: options.toolChoice,
4912
+ search: options.search,
4913
+ searchParameters: options.searchParameters,
4780
4914
  conversationChars,
4781
4915
  hasVision
4782
4916
  }
@@ -4791,6 +4925,7 @@ var LLMClient = class _LLMClient {
4791
4925
  toolChoice: options.toolChoice,
4792
4926
  responseFormat: options.responseFormat,
4793
4927
  stop: options.stop,
4928
+ timeout: options.timeout,
4794
4929
  // An explicit caller-supplied chain wins over the routed one.
4795
4930
  fallbackModels: options.fallbackModels ?? decision.fallbacks
4796
4931
  });
@@ -4867,11 +5002,11 @@ var LLMClient = class _LLMClient {
4867
5002
  if (options?.temperature !== void 0) body.temperature = options.temperature;
4868
5003
  if (options?.topP !== void 0) body.top_p = options.topP;
4869
5004
  if (options?.searchParameters !== void 0) {
4870
- body.search_parameters = options.searchParameters;
5005
+ body.search_parameters = toWireSearchParameters(options.searchParameters);
4871
5006
  } else if (options?.search === true) {
4872
5007
  body.search_parameters = { mode: "on" };
4873
5008
  }
4874
- if (options?.tools !== void 0) body.tools = options.tools;
5009
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
4875
5010
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
4876
5011
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
4877
5012
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -4882,7 +5017,7 @@ var LLMClient = class _LLMClient {
4882
5017
  for (let i = 0; i < chain.length; i++) {
4883
5018
  const candidate = chain[i];
4884
5019
  try {
4885
- return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
5020
+ return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
4886
5021
  } catch (err) {
4887
5022
  lastErr = err;
4888
5023
  const next = chain[i + 1];
@@ -4953,19 +5088,19 @@ var LLMClient = class _LLMClient {
4953
5088
  * is `"paid-or-in-doubt"`. With an API key the request itself is billed, so
4954
5089
  * only the account API's explicit 4xx refusal is `"unpaid"`.
4955
5090
  */
4956
- async requestWithPayment(endpoint, body) {
5091
+ async requestWithPayment(endpoint, body, timeoutMs) {
4957
5092
  const url = `${this.apiUrl}${endpoint}`;
4958
5093
  const unpaid = {
4959
5094
  method: "POST",
4960
5095
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
4961
5096
  body: JSON.stringify(body)
4962
5097
  };
4963
- const response = await this.sendUnpaid(url, unpaid);
5098
+ const response = await this.sendUnpaid(url, unpaid, timeoutMs);
4964
5099
  if (response.status === 502 || response.status === 503) {
4965
5100
  await new Promise((r) => setTimeout(r, 1e3));
4966
- const retryResp = await this.sendUnpaid(url, unpaid);
5101
+ const retryResp = await this.sendUnpaid(url, unpaid, timeoutMs);
4967
5102
  if (retryResp.status !== 502 && retryResp.status !== 503) {
4968
- if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp);
5103
+ if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp, timeoutMs);
4969
5104
  if (!retryResp.ok) {
4970
5105
  let errorBody;
4971
5106
  try {
@@ -4982,7 +5117,7 @@ var LLMClient = class _LLMClient {
4982
5117
  }
4983
5118
  }
4984
5119
  if (response.status === 402) {
4985
- return this.handlePaymentAndRetry(url, body, response);
5120
+ return this.handlePaymentAndRetry(url, body, response, timeoutMs);
4986
5121
  }
4987
5122
  if (!response.ok) {
4988
5123
  let errorBody;
@@ -5007,9 +5142,9 @@ var LLMClient = class _LLMClient {
5007
5142
  * {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
5008
5143
  * {@link accountErrorDisposition} (the request itself is billed).
5009
5144
  */
5010
- async sendUnpaid(url, init) {
5145
+ async sendUnpaid(url, init, timeoutMs) {
5011
5146
  try {
5012
- return await this.fetchWithTimeout(url, init);
5147
+ return await this.fetchWithTimeout(url, init, timeoutMs);
5013
5148
  } catch (err) {
5014
5149
  throw withDisposition(err, this.apiAuth ? accountErrorDisposition(err) : "unpaid");
5015
5150
  }
@@ -5017,13 +5152,22 @@ var LLMClient = class _LLMClient {
5017
5152
  /**
5018
5153
  * Parse a response to a request that carried no payment: the free tier,
5019
5154
  * or, with an API key, the billed request itself.
5155
+ *
5156
+ * With a wallet nothing was paid, so `costUsd` is 0 (a free model). With an
5157
+ * API key the call is billed to the account and `costUsd` stays undefined.
5020
5158
  */
5021
5159
  async parseUnpaidChatResponse(response) {
5160
+ let served;
5022
5161
  try {
5023
- return await this.parseChatResponse(response);
5162
+ served = await this.parseChatResponse(response);
5024
5163
  } catch (err) {
5025
5164
  throw withDisposition(err, this.apiAuth ? "paid-or-in-doubt" : "unpaid");
5026
5165
  }
5166
+ if (!this.apiAuth) {
5167
+ served.costUsd = 0;
5168
+ served.settlement = { scheme: "free", quotedUsd: 0 };
5169
+ }
5170
+ return served;
5027
5171
  }
5028
5172
  /**
5029
5173
  * Handle 402 response: parse requirements, sign payment, retry.
@@ -5031,7 +5175,7 @@ var LLMClient = class _LLMClient {
5031
5175
  * Anything thrown before the signed payment is sent is `"unpaid"`; anything
5032
5176
  * after it, `"paid-or-in-doubt"` (see {@link RetryDisposition}).
5033
5177
  */
5034
- async handlePaymentAndRetry(url, body, response) {
5178
+ async handlePaymentAndRetry(url, body, response, timeoutMs) {
5035
5179
  let signed;
5036
5180
  try {
5037
5181
  signed = await this.signFrom402(url, response);
@@ -5039,7 +5183,7 @@ var LLMClient = class _LLMClient {
5039
5183
  throw withDisposition(err, "unpaid");
5040
5184
  }
5041
5185
  try {
5042
- return await this.sendPaid(url, body, signed.paymentPayload, signed.details);
5186
+ return await this.sendPaid(url, body, signed.paymentPayload, signed.details, timeoutMs);
5043
5187
  } catch (err) {
5044
5188
  throw withDisposition(err, "paid-or-in-doubt");
5045
5189
  }
@@ -5084,7 +5228,7 @@ var LLMClient = class _LLMClient {
5084
5228
  return { paymentPayload, details };
5085
5229
  }
5086
5230
  /** Send the signed payment and read the answer. The payment may settle from here on. */
5087
- async sendPaid(url, body, paymentPayload, details) {
5231
+ async sendPaid(url, body, paymentPayload, details, timeoutMs) {
5088
5232
  const retryResponse = await this.fetchWithTimeout(url, {
5089
5233
  method: "POST",
5090
5234
  headers: {
@@ -5093,7 +5237,7 @@ var LLMClient = class _LLMClient {
5093
5237
  "PAYMENT-SIGNATURE": paymentPayload
5094
5238
  },
5095
5239
  body: JSON.stringify(body)
5096
- });
5240
+ }, timeoutMs);
5097
5241
  if (retryResponse.status === 502 || retryResponse.status === 503) {
5098
5242
  await new Promise((r) => setTimeout(r, 1e3));
5099
5243
  const retryResp2 = await this.fetchWithTimeout(url, {
@@ -5104,7 +5248,7 @@ var LLMClient = class _LLMClient {
5104
5248
  "PAYMENT-SIGNATURE": paymentPayload
5105
5249
  },
5106
5250
  body: JSON.stringify(body)
5107
- });
5251
+ }, timeoutMs);
5108
5252
  if (retryResp2.status !== 502 && retryResp2.status !== 503) {
5109
5253
  if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
5110
5254
  if (!retryResp2.ok) {
@@ -5120,7 +5264,7 @@ var LLMClient = class _LLMClient {
5120
5264
  this.sessionCalls += 1;
5121
5265
  this.sessionTotalUsd += costUsd2;
5122
5266
  this.recordCost(url, costUsd2, { body, network: details.network });
5123
- return this.parseChatResponse(retryResp2);
5267
+ return withCost(await this.parseChatResponse(retryResp2), retryResp2, costUsd2, "exact");
5124
5268
  }
5125
5269
  }
5126
5270
  if (retryResponse.status === 402) {
@@ -5143,7 +5287,7 @@ var LLMClient = class _LLMClient {
5143
5287
  this.sessionCalls += 1;
5144
5288
  this.sessionTotalUsd += costUsd;
5145
5289
  this.recordCost(url, costUsd, { body, network: details.network });
5146
- return this.parseChatResponse(retryResponse);
5290
+ return withCost(await this.parseChatResponse(retryResponse), retryResponse, costUsd, "exact");
5147
5291
  }
5148
5292
  /**
5149
5293
  * Sign a payment header and return the PAYMENT-SIGNATURE value.
@@ -5213,7 +5357,7 @@ var LLMClient = class _LLMClient {
5213
5357
  };
5214
5358
  if (options?.temperature !== void 0) body.temperature = options.temperature;
5215
5359
  if (options?.topP !== void 0) body.top_p = options.topP;
5216
- if (options?.tools !== void 0) body.tools = options.tools;
5360
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
5217
5361
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
5218
5362
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
5219
5363
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -5611,11 +5755,11 @@ var LLMClient = class _LLMClient {
5611
5755
  return retryResponse.json();
5612
5756
  }
5613
5757
  /**
5614
- * Fetch with timeout.
5758
+ * Fetch with timeout: `timeoutMs` (a per-call override), else the client's `timeout`.
5615
5759
  */
5616
- async fetchWithTimeout(url, options) {
5760
+ async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
5617
5761
  const controller = new AbortController();
5618
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
5762
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
5619
5763
  try {
5620
5764
  const response = await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, {
5621
5765
  ...options,
@@ -9600,6 +9744,7 @@ import bs582 from "bs58";
9600
9744
  var BATCH_SCHEME = "batch-settlement";
9601
9745
  var BLOCKRUN_SOL_OPERATOR = "5YKPQUFjw5WQqhSUkEGKNNfYYVqnRRNbpYyL71qQ1vm3";
9602
9746
  var DEFAULT_BATCH_RATE_LIMIT = { maxAttempts: 3, maxWaitMs: 6e4 };
9747
+ var DEFAULT_BATCH_IN_DOUBT = { maxWaitMs: 3e5 };
9603
9748
  var BatchPaymentUnresolvedError = class extends PaymentError {
9604
9749
  /** The {@link SolanaBatchEvent.reason} of the `unresolved` event reported with it. */
9605
9750
  reason;
@@ -10276,6 +10421,46 @@ function withoutClientSignedBatch(paymentRequired) {
10276
10421
  const accepts = paymentRequired.accepts ?? [];
10277
10422
  return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
10278
10423
  }
10424
+ var PAYMENT_IDENTIFIER = "payment-identifier";
10425
+ function withPaymentIdentifier(paymentRequired) {
10426
+ const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
10427
+ if (typeof declared?.info?.required !== "boolean") return paymentRequired;
10428
+ const id = `pay_${randomUUID().replace(/-/g, "")}`;
10429
+ return {
10430
+ ...paymentRequired,
10431
+ extensions: { ...paymentRequired.extensions, [PAYMENT_IDENTIFIER]: { ...declared, info: { ...declared.info, id } } }
10432
+ };
10433
+ }
10434
+ function peekReceipt(http, getHeader) {
10435
+ try {
10436
+ return http.getPaymentSettleResponse(getHeader);
10437
+ } catch {
10438
+ return void 0;
10439
+ }
10440
+ }
10441
+ function receiptSucceeded(http, answer) {
10442
+ return peekReceipt(http, (name) => answer.headers.get(name))?.success === true;
10443
+ }
10444
+ function hasPaymentIdentifier(payload) {
10445
+ const info = payload.extensions?.[PAYMENT_IDENTIFIER]?.info;
10446
+ return typeof info?.id === "string";
10447
+ }
10448
+ var IN_DOUBT_MIN_WAIT_MS = 1e3;
10449
+ var IN_DOUBT_MAX_WAIT_MS = 3e4;
10450
+ function replayMayResolve(doubt) {
10451
+ if (doubt.unanswered) return true;
10452
+ const answer = doubt.held;
10453
+ if (!answer) return false;
10454
+ return answer.status >= 500 || answer.status === 429 || answer.status === 409 && Boolean(answer.headers.get("retry-after")?.trim());
10455
+ }
10456
+ function inDoubtWait(doubt, replay) {
10457
+ const asked = doubt.held ? parseRetryAfter(doubt.held.headers.get("retry-after")) : void 0;
10458
+ return Math.min(IN_DOUBT_MAX_WAIT_MS, Math.max(IN_DOUBT_MIN_WAIT_MS, asked ?? backoffDelay(replay)));
10459
+ }
10460
+ function replayDoubt(outcome) {
10461
+ if (outcome.kind === "charged" || outcome.kind === "in_doubt") return outcome;
10462
+ return { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
10463
+ }
10279
10464
  function trustedBatchAccepts(paymentRequired, operators) {
10280
10465
  return (paymentRequired.accepts ?? []).filter((candidate) => {
10281
10466
  const extra = candidate.extra;
@@ -10285,9 +10470,6 @@ function trustedBatchAccepts(paymentRequired, operators) {
10285
10470
  function trustedBatchAccept(paymentRequired, operators) {
10286
10471
  return trustedBatchAccepts(paymentRequired, operators)[0];
10287
10472
  }
10288
- function atomicToUsd(amount) {
10289
- return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
10290
- }
10291
10473
  function chargedUsd(getReceipt) {
10292
10474
  let receipt;
10293
10475
  try {
@@ -10295,7 +10477,7 @@ function chargedUsd(getReceipt) {
10295
10477
  } catch {
10296
10478
  return 0;
10297
10479
  }
10298
- return atomicToUsd(receipt?.extra?.chargedAmount) ?? atomicToUsd(receipt?.amount) ?? 0;
10480
+ return receiptChargedUsd(receipt) ?? 0;
10299
10481
  }
10300
10482
  function rebuiltCharge(settled, payload) {
10301
10483
  if (settled?.success !== true) return 0n;
@@ -10415,13 +10597,42 @@ function parseRetryAfter(value, now = Date.now()) {
10415
10597
  }
10416
10598
  return Number.isFinite(ms) && ms <= MAX_RETRY_AFTER_MS ? ms : void 0;
10417
10599
  }
10418
- var NOT_BROADCAST_429 = /* @__PURE__ */ new Set([
10600
+ var NOT_BROADCAST_REFUSALS = /* @__PURE__ */ new Set([
10601
+ // 429
10419
10602
  "batch_account_channel_capacity_exhausted",
10420
10603
  "batch_channel_capacity_exhausted",
10421
- "batch_deposit_rate_limited"
10604
+ "batch_deposit_rate_limited",
10605
+ "batch_deposit_ip_rate_limited",
10606
+ // 503
10607
+ "batch_signer_funding_headroom_exhausted",
10608
+ "batch_new_channels_disabled",
10609
+ "batch_public_new_channels_disabled",
10610
+ // 403
10611
+ "batch_account_admission_paused",
10612
+ "batch_channel_owner_mismatch",
10613
+ "batch_fee_payer_unavailable",
10614
+ "batch_fee_payer_not_dedicated",
10615
+ "batch_fee_payer_lane_not_authorized",
10616
+ "batch_transaction_fee_payer_mismatch",
10617
+ // 400
10618
+ "batch_initial_deposit_out_of_range",
10619
+ "batch_deposit_asset_not_supported",
10620
+ "batch_withdraw_delay_out_of_range"
10422
10621
  ]);
10423
10622
  function provesNothingBroadcast(receipt) {
10424
- return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_429.has(receipt.errorReason);
10623
+ return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_REFUSALS.has(receipt.errorReason);
10624
+ }
10625
+ var CONFIG_REFUSALS = /* @__PURE__ */ new Set([
10626
+ "batch_initial_deposit_out_of_range",
10627
+ "batch_deposit_asset_not_supported",
10628
+ "batch_withdraw_delay_out_of_range"
10629
+ ]);
10630
+ var REFUSAL_COOLDOWN_MS = 6e4;
10631
+ var CONFIG_REFUSAL_COOLDOWN_MS = 10 * 6e4;
10632
+ var REFUSAL_COOLDOWN_MAX_MS = 15 * 6e4;
10633
+ function refusalCooldownMs(errorReason, retryAfterMs2) {
10634
+ if (retryAfterMs2 !== void 0) return Math.min(retryAfterMs2, REFUSAL_COOLDOWN_MAX_MS);
10635
+ return errorReason !== void 0 && CONFIG_REFUSALS.has(errorReason) ? CONFIG_REFUSAL_COOLDOWN_MS : REFUSAL_COOLDOWN_MS;
10425
10636
  }
10426
10637
  function backoffDelay(attempt) {
10427
10638
  const base = 1e3 * 2 ** Math.max(0, attempt - 1);
@@ -10554,7 +10765,7 @@ function intentTarget(intent) {
10554
10765
  var SolanaBatchPayer = class {
10555
10766
  constructor(init) {
10556
10767
  this.init = init;
10557
- const { operators, rateLimit } = init.options;
10768
+ const { operators, rateLimit, inDoubt } = init.options;
10558
10769
  if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
10559
10770
  throw new Error("batch.operators must list at least one base58 operator public key");
10560
10771
  }
@@ -10566,11 +10777,16 @@ var SolanaBatchPayer = class {
10566
10777
  if (!Number.isFinite(this.maxWaitMs) || this.maxWaitMs < 0) {
10567
10778
  throw new Error("batch.rateLimit.maxWaitMs must be a finite number of ms >= 0");
10568
10779
  }
10780
+ this.inDoubtMaxWaitMs = inDoubt?.maxWaitMs ?? DEFAULT_BATCH_IN_DOUBT.maxWaitMs;
10781
+ if (!Number.isFinite(this.inDoubtMaxWaitMs) || this.inDoubtMaxWaitMs < 0) {
10782
+ throw new Error("batch.inDoubt.maxWaitMs must be a finite number of ms >= 0");
10783
+ }
10569
10784
  }
10570
10785
  init;
10571
10786
  state;
10572
10787
  maxAttempts;
10573
10788
  maxWaitMs;
10789
+ inDoubtMaxWaitMs;
10574
10790
  counters = {
10575
10791
  fallbacks: 0,
10576
10792
  fallbacksByReason: {},
@@ -10939,14 +11155,19 @@ var SolanaBatchPayer = class {
10939
11155
  * the call's result (`served`, nothing paid); one that fails otherwise
10940
11156
  * is the call's error (`failed`, `"unpaid"`), never an exact payment
10941
11157
  * against the stale challenge;
10942
- * - in doubt: ends only with a definitive success receipt from the one
10943
- * replay a receipt-less 429 is allowed ({@link resolveInDoubt}), or a
10944
- * thrown {@link BatchPaymentUnresolvedError}. No new authorization, no
10945
- * new deposit, no exact payment and no fallback model is ever paid for a
10946
- * call in doubt.
11158
+ * - in doubt: ends only with a definitive success receipt from a replay
11159
+ * of the identical payment ({@link resolveInDoubt}: the one replay a
11160
+ * receipt-less 429 is allowed, or the replays of a payment carrying a
11161
+ * payment identifier while the gateway says one can succeed, within
11162
+ * `inDoubt.maxWaitMs`), or a thrown {@link BatchPaymentUnresolvedError}.
11163
+ * No new authorization, no new deposit, no exact payment and no fallback
11164
+ * model is ever paid for a call in doubt.
10947
11165
  *
10948
11166
  * The wallet stays in cooldown after a 429, so its other calls wait
10949
- * instead of sending opens of their own.
11167
+ * instead of sending opens of their own. After a deposit the facilitator
11168
+ * refused before broadcasting it (a 402 with a failed `batch_*` admission
11169
+ * receipt), its calls do not wait: they pay exact at once
11170
+ * (`admission_refused`) until the refusal cooldown ends.
10950
11171
  *
10951
11172
  * @param closesAtStart - {@link closeFence}, taken before the call's first
10952
11173
  * (unpaid) request. If a close completed since, the call pays exact
@@ -11010,6 +11231,15 @@ var SolanaBatchPayer = class {
11010
11231
  try {
11011
11232
  for (; ; ) {
11012
11233
  const now = Date.now();
11234
+ if ((wallet.refusedUntil ?? 0) > now) {
11235
+ return fallback({
11236
+ reason: "admission_refused",
11237
+ errorReason: wallet.refusedFor,
11238
+ retryAfterMs: (wallet.refusedUntil ?? 0) - now,
11239
+ attempt: budget.attempt,
11240
+ detail: `the facilitator refused this wallet's last batch deposit (${wallet.refusedFor ?? "unknown"}); batch is skipped until the refusal cooldown ends`
11241
+ });
11242
+ }
11013
11243
  if (wallet.cooldownUntil > now && wallet.cooldownUntil !== waitedUntil) {
11014
11244
  const wait = wallet.cooldownUntil - now;
11015
11245
  if (budget.waited + wait > this.maxWaitMs) {
@@ -11099,12 +11329,23 @@ var SolanaBatchPayer = class {
11099
11329
  if (built.kind !== "authorization") {
11100
11330
  if (outcome.depositSafe) this.forgetIntent(wallet, built.key);
11101
11331
  else this.distrust(wallet, built, "deposit_refused");
11332
+ if (outcome.refusedForMs !== void 0) {
11333
+ const until = Date.now() + outcome.refusedForMs;
11334
+ if (until > (wallet.refusedUntil ?? 0)) {
11335
+ wallet.refusedUntil = until;
11336
+ wallet.refusedFor = outcome.errorReason;
11337
+ }
11338
+ }
11102
11339
  }
11103
11340
  sent = void 0;
11104
11341
  release();
11105
11342
  if (!outcome.rateLimited) {
11106
- const { kind: _kind, rateLimited: _limited, depositSafe: _safe, ...event } = outcome;
11107
- return fallback({ ...event, attempt: budget.attempt });
11343
+ const { kind: _kind, rateLimited: _limited, depositSafe: _safe, refusedForMs, ...event } = outcome;
11344
+ return fallback({
11345
+ ...event,
11346
+ ...refusedForMs !== void 0 && built.kind !== "authorization" ? { retryAfterMs: refusedForMs } : {},
11347
+ attempt: budget.attempt
11348
+ });
11108
11349
  }
11109
11350
  const wait = this.coolDown(wallet, outcome.rateLimited.retryAfterMs ?? backoffDelay(budget.attempt));
11110
11351
  if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
@@ -11150,7 +11391,7 @@ var SolanaBatchPayer = class {
11150
11391
  if (built.kind !== "authorization" && !(built.key && wallet.resyncs.has(built.key))) this.forgetIntent(wallet, built.key);
11151
11392
  sent = void 0;
11152
11393
  release();
11153
- await this.report({ type: "recovered", reason: "rate_limited", attempt: budget.attempt });
11394
+ await this.report({ type: "recovered", reason: outcome.status === 429 ? "rate_limited" : "in_doubt", attempt: budget.attempt });
11154
11395
  return { kind: "paid", response: resolved.response, chargedUsd: resolved.chargedUsd };
11155
11396
  }
11156
11397
  return await this.raiseUnresolved(wallet, built, resolved, budget.attempt);
@@ -11199,21 +11440,24 @@ var SolanaBatchPayer = class {
11199
11440
  /**
11200
11441
  * Try to end a payment in doubt with the gateway's own word.
11201
11442
  *
11202
- * The one source of evidence this SDK accepts today is a definitive
11203
- * success receipt on the single byte-identical replay a 429 without a
11204
- * receipt is allowed, after its backoff (the owner's bounded safety
11205
- * attempt). Any other answer to it (a 402 or `duplicate_settlement`, which
11206
- * mean the original reached the gateway; another 429; a 5xx; a timeout)
11207
- * leaves it in doubt. Every other in-doubt outcome is never replayed.
11443
+ * The one source of evidence this SDK accepts is a definitive success
11444
+ * receipt on a byte-identical replay of the payment. Any other answer to a
11445
+ * replay (a 402 or `duplicate_settlement`, which mean the original reached
11446
+ * the gateway; a 409; another 429; a 5xx; a timeout) leaves it in doubt.
11447
+ *
11448
+ * - A payment carrying a payment identifier is replayed for as long as
11449
+ * the gateway says a later replay can succeed ({@link replayInDoubt}).
11450
+ * - Any other payment gets a replay only after a 429 without a receipt:
11451
+ * one, after its backoff (the owner's bounded safety attempt), within
11452
+ * `rateLimit`. Every other in-doubt outcome is never replayed.
11208
11453
  *
11209
11454
  * Chain state is not evidence here: it can settle a deposit (see
11210
- * `resync`), never whether the gateway charged. Gateway-side evidence (a
11211
- * receipt on every response, a request-status endpoint with a fence)
11212
- * plugs in here, before the caller raises.
11455
+ * `resync`), never whether the gateway charged.
11213
11456
  *
11214
11457
  * @returns the charged replay, or the doubt to raise.
11215
11458
  */
11216
11459
  async resolveInDoubt(wallet, sent, doubt, send, budget) {
11460
+ if (hasPaymentIdentifier(sent.payload)) return this.replayInDoubt(wallet, sent, doubt, send, budget.attempt);
11217
11461
  if (!doubt.replayable) return doubt;
11218
11462
  const wait = this.coolDown(wallet, doubt.retryAfterMs ?? backoffDelay(budget.attempt));
11219
11463
  if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
@@ -11233,9 +11477,94 @@ var SolanaBatchPayer = class {
11233
11477
  budget.waited += wait;
11234
11478
  await sleep2(wait);
11235
11479
  budget.attempt += 1;
11236
- const replayed = await this.sendOnce(wallet, sent, send, true, true);
11237
- if (replayed.kind === "charged") return replayed;
11238
- return replayed.kind === "in_doubt" ? replayed : { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
11480
+ return replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
11481
+ }
11482
+ /**
11483
+ * Replay a payment in doubt that carries a payment identifier until the
11484
+ * gateway's answer settles it.
11485
+ *
11486
+ * A gateway that declares `payment-identifier` stores each paid response
11487
+ * under its identifier and answers a byte-identical replay of a completed
11488
+ * request with it, the original receipt included; while the original is
11489
+ * still running, it answers 409 with a `Retry-After`. So the identical
11490
+ * payment (the same `PAYMENT-SIGNATURE` header and the same body) is
11491
+ * replayed for as long as the last answer says a later one can succeed
11492
+ * ({@link replayMayResolve}), after {@link inDoubtWait} each time, as long
11493
+ * as the wait fits in `inDoubt.maxWaitMs`, counted from the first doubt:
11494
+ * every wait (as long as it really took) and every replay's round trip. A
11495
+ * replay already sent is not cut short. These replays do not count
11496
+ * against `rateLimit`; a 429 among them still puts the wallet in cooldown.
11497
+ *
11498
+ * Every answer that does not settle it is held back from the scheme
11499
+ * ({@link InDoubt.held}) and handed over only once no replay follows, so
11500
+ * the scheme rolls the payment back once, or takes the receipt that answer
11501
+ * carries. That is the last answer, unless an earlier one carried a
11502
+ * success receipt and the later ones do not: a success receipt is the
11503
+ * gateway's word that it charged, and handing the scheme a receipt-less
11504
+ * refusal of a replay instead would roll a real charge back and desync the
11505
+ * channel's cumulative.
11506
+ *
11507
+ * @returns the charged replay, or the doubt to raise.
11508
+ */
11509
+ async replayInDoubt(wallet, sent, first, send, attempt) {
11510
+ let doubt = first;
11511
+ let held = first.held;
11512
+ let heldSucceeded = held !== void 0 && receiptSucceeded(sent.http, held);
11513
+ let replays = 0;
11514
+ let spent = 0;
11515
+ const stop = async (note) => {
11516
+ if (!doubt.settled && held) {
11517
+ const answer = held;
11518
+ await this.settle(sent.http, sent.payload, (name) => answer.headers.get(name), answer.status);
11519
+ }
11520
+ const notes = [replays > 0 ? `${replays} replay${replays === 1 ? "" : "s"} did not resolve it` : "", note ?? ""];
11521
+ return {
11522
+ ...doubt,
11523
+ settled: doubt.settled || held !== void 0,
11524
+ held: void 0,
11525
+ firstReason: first.reason,
11526
+ detail: [doubt.detail, ...notes].filter(Boolean).join("; ")
11527
+ };
11528
+ };
11529
+ for (; ; ) {
11530
+ if (!replayMayResolve(doubt)) {
11531
+ return stop(doubt.held?.status === 409 ? "the gateway sent no Retry-After, so no replay can resolve it" : void 0);
11532
+ }
11533
+ const wait = inDoubtWait(doubt, replays + 1);
11534
+ const status2 = doubt.held?.status;
11535
+ if (status2 === 429) this.coolDown(wallet, (doubt.held && parseRetryAfter(doubt.held.headers.get("retry-after"))) ?? wait);
11536
+ if (spent + wait > this.inDoubtMaxWaitMs) {
11537
+ return stop(`batch.inDoubt.maxWaitMs (${this.inDoubtMaxWaitMs} ms) leaves no room for ${replays > 0 ? "another replay" : "a replay"}`);
11538
+ }
11539
+ await this.report({
11540
+ type: "backoff",
11541
+ reason: status2 === 429 ? "rate_limited" : "in_doubt",
11542
+ status: status2,
11543
+ errorReason: doubt.errorReason,
11544
+ retryAfterMs: wait,
11545
+ attempt,
11546
+ detail: `replay ${replays + 1} of the payment in doubt`
11547
+ });
11548
+ const sleptFrom = Date.now();
11549
+ await sleep2(wait);
11550
+ spent += Math.max(wait, Date.now() - sleptFrom);
11551
+ replays += 1;
11552
+ const startedAt = Date.now();
11553
+ const replayed = replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
11554
+ spent += Math.max(0, Date.now() - startedAt);
11555
+ if (replayed.kind === "charged") return replayed;
11556
+ doubt = replayed;
11557
+ if (doubt.held) {
11558
+ const succeeded = receiptSucceeded(sent.http, doubt.held);
11559
+ if (succeeded || !heldSucceeded) {
11560
+ held = doubt.held;
11561
+ heldSucceeded = succeeded;
11562
+ }
11563
+ } else if (doubt.settled) {
11564
+ held = void 0;
11565
+ heldSucceeded = false;
11566
+ }
11567
+ }
11239
11568
  }
11240
11569
  /**
11241
11570
  * Raise a payment in doubt: release the scheme's hold on it (the channel
@@ -11246,10 +11575,11 @@ var SolanaBatchPayer = class {
11246
11575
  async raiseUnresolved(wallet, sent, doubt, attempt) {
11247
11576
  if (!doubt.settled) await this.settle(sent.http, sent.payload, () => null, 0);
11248
11577
  if (sent.kind !== "authorization") {
11578
+ const reason = doubt.firstReason ?? doubt.reason;
11249
11579
  this.distrust(
11250
11580
  wallet,
11251
11581
  sent,
11252
- doubt.reason === "no_response" ? "deposit_unanswered" : doubt.reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
11582
+ reason === "no_response" ? "deposit_unanswered" : reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
11253
11583
  );
11254
11584
  }
11255
11585
  await this.report({
@@ -11336,8 +11666,8 @@ var SolanaBatchPayer = class {
11336
11666
  * here, and only for a first send.
11337
11667
  *
11338
11668
  * @param retry - count this send in `retries` (it follows this call's own 429).
11339
- * @param replay - this is the one replay of a receipt-less 429: only a 2xx
11340
- * with a success receipt counts, everything else stays in doubt.
11669
+ * @param replay - this is a replay of a payment in doubt: only a 2xx with
11670
+ * a success receipt counts, everything else stays in doubt.
11341
11671
  */
11342
11672
  async sendOnce(wallet, sent, send, retry, replay) {
11343
11673
  if (retry) this.counters.retries += 1;
@@ -11351,6 +11681,7 @@ var SolanaBatchPayer = class {
11351
11681
  reason: replay ? "replay_unresolved" : "no_response",
11352
11682
  replayable: false,
11353
11683
  settled: false,
11684
+ unanswered: true,
11354
11685
  detail: `${replay ? "the replay" : "the payment"} got no answer (${err instanceof Error ? `${err.name}: ${err.message}` : String(err)})`,
11355
11686
  cause: err
11356
11687
  };
@@ -11375,6 +11706,7 @@ var SolanaBatchPayer = class {
11375
11706
  const getHeader = (name) => response.headers.get(name);
11376
11707
  const receipted = Boolean(getHeader("PAYMENT-RESPONSE") || getHeader("X-PAYMENT-RESPONSE"));
11377
11708
  const status2 = response.status;
11709
+ const hold = hasPaymentIdentifier(payload) && (!response.ok || replay && peekReceipt(http, getHeader)?.success !== true);
11378
11710
  if (status2 === 429 && !receipted) {
11379
11711
  return {
11380
11712
  kind: "in_doubt",
@@ -11384,12 +11716,27 @@ var SolanaBatchPayer = class {
11384
11716
  status: status2,
11385
11717
  retryAfterMs: parseRetryAfter(getHeader("retry-after")),
11386
11718
  errorReason: await rateLimitReason(response, void 0),
11387
- detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt"
11719
+ detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt",
11720
+ ...hold ? { held: response } : {}
11388
11721
  };
11389
11722
  }
11723
+ const outcome = await this.classifyAnswer(wallet, sent, response, replay, hold);
11724
+ if (!hold) return outcome;
11725
+ if (outcome.kind === "in_doubt") return { ...outcome, settled: false, held: response };
11726
+ await this.settle(http, payload, getHeader, status2);
11727
+ return outcome;
11728
+ }
11729
+ /**
11730
+ * {@link classify}, past a receipt-less 429. With `hold`, the answer is not
11731
+ * handed to the scheme: its receipt is only read.
11732
+ */
11733
+ async classifyAnswer(wallet, sent, response, replay, hold) {
11734
+ const { http, payload } = sent;
11735
+ const getHeader = (name) => response.headers.get(name);
11736
+ const status2 = response.status;
11390
11737
  const channelId = payloadChannelId(payload);
11391
11738
  const before = channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0;
11392
- const reconciled = await this.settle(http, payload, getHeader, status2);
11739
+ const reconciled = hold ? { settleResponse: peekReceipt(http, getHeader), ok: false } : await this.settle(http, payload, getHeader, status2);
11393
11740
  const receipt = reconciled.settleResponse;
11394
11741
  const definitive = receipt?.success === true && reconciled.ok && recordAdvanced(before, channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0, payload, receipt);
11395
11742
  if (!response.ok && !replay && sent.kind === "authorization" && cancelledReceipt(receipt)) {
@@ -11452,7 +11799,15 @@ var SolanaBatchPayer = class {
11452
11799
  cause: await afterPaymentError(response)
11453
11800
  };
11454
11801
  }
11455
- return { kind: "not_charged", reason: "payment_required", status: status2, errorReason: receipt?.errorReason, depositSafe: provesNothingBroadcast(receipt) };
11802
+ const refused = provesNothingBroadcast(receipt);
11803
+ return {
11804
+ kind: "not_charged",
11805
+ reason: "payment_required",
11806
+ status: status2,
11807
+ errorReason: receipt?.errorReason,
11808
+ depositSafe: refused,
11809
+ ...refused ? { refusedForMs: refusalCooldownMs(receipt?.errorReason, parseRetryAfter(getHeader("retry-after"))) } : {}
11810
+ };
11456
11811
  }
11457
11812
  const refusal = await batchRefusal(response);
11458
11813
  if (refusal) {
@@ -11483,7 +11838,7 @@ var SolanaBatchPayer = class {
11483
11838
  const http = (await this.build(wallet)).http;
11484
11839
  try {
11485
11840
  await this.prepareTopUp(wallet, paymentRequired);
11486
- return { http, payload: await http.createPaymentPayload(withoutClientSignedBatch(paymentRequired)) };
11841
+ return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(withoutClientSignedBatch(paymentRequired))) };
11487
11842
  } catch (err) {
11488
11843
  if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
11489
11844
  } finally {
@@ -11576,13 +11931,7 @@ var SolanaBatchPayer = class {
11576
11931
  console.error(
11577
11932
  `[@blockrun/llm] batch-settlement receipt not reconciled: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
11578
11933
  );
11579
- let settleResponse;
11580
- try {
11581
- settleResponse = http.getPaymentSettleResponse(getHeader);
11582
- } catch {
11583
- settleResponse = void 0;
11584
- }
11585
- return { settleResponse, ok: false };
11934
+ return { settleResponse: peekReceipt(http, getHeader), ok: false };
11586
11935
  }
11587
11936
  }
11588
11937
  /**
@@ -11965,9 +12314,9 @@ var SolanaLLMClient = class {
11965
12314
  };
11966
12315
  if (options?.temperature !== void 0) body.temperature = options.temperature;
11967
12316
  if (options?.topP !== void 0) body.top_p = options.topP;
11968
- if (options?.searchParameters !== void 0) body.search_parameters = options.searchParameters;
12317
+ if (options?.searchParameters !== void 0) body.search_parameters = toWireSearchParameters(options.searchParameters);
11969
12318
  else if (options?.search === true) body.search_parameters = { mode: "on" };
11970
- if (options?.tools !== void 0) body.tools = options.tools;
12319
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
11971
12320
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
11972
12321
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
11973
12322
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -11978,7 +12327,7 @@ var SolanaLLMClient = class {
11978
12327
  for (let index = 0; index < chain.length; index += 1) {
11979
12328
  const candidate = chain[index];
11980
12329
  try {
11981
- return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
12330
+ return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
11982
12331
  } catch (error) {
11983
12332
  lastError = error;
11984
12333
  if (!isTransientError(error) || index === chain.length - 1) throw error;
@@ -12028,6 +12377,8 @@ var SolanaLLMClient = class {
12028
12377
  {
12029
12378
  routingProfile: options?.routingProfile,
12030
12379
  requiresStructuredOutput: options?.responseFormat !== void 0,
12380
+ search: options?.search,
12381
+ searchParameters: options?.searchParameters,
12031
12382
  minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
12032
12383
  }
12033
12384
  );
@@ -12055,6 +12406,8 @@ var SolanaLLMClient = class {
12055
12406
  requiresStructuredOutput: options.responseFormat !== void 0,
12056
12407
  tools: options.tools,
12057
12408
  toolChoice: options.toolChoice,
12409
+ search: options.search,
12410
+ searchParameters: options.searchParameters,
12058
12411
  conversationChars,
12059
12412
  hasVision,
12060
12413
  minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
@@ -12070,6 +12423,7 @@ var SolanaLLMClient = class {
12070
12423
  toolChoice: options.toolChoice,
12071
12424
  responseFormat: options.responseFormat,
12072
12425
  stop: options.stop,
12426
+ timeout: options.timeout,
12073
12427
  // An explicit caller-supplied chain wins over the routed one.
12074
12428
  fallbackModels: options.fallbackModels ?? decision.fallbacks
12075
12429
  });
@@ -12207,7 +12561,7 @@ var SolanaLLMClient = class {
12207
12561
  *
12208
12562
  * @returns USDC balance as a float
12209
12563
  */
12210
- async getBalance() {
12564
+ async getBalance(options) {
12211
12565
  const usdc_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
12212
12566
  const address = await this.getWalletAddress();
12213
12567
  try {
@@ -12222,6 +12576,9 @@ var SolanaLLMClient = class {
12222
12576
  })
12223
12577
  });
12224
12578
  const data = await response.json();
12579
+ if (options?.strict && (!response.ok || data.error)) {
12580
+ throw new Error(`Solana RPC balance lookup failed: ${data.error?.message ?? `HTTP ${response.status}`}`);
12581
+ }
12225
12582
  const accounts = data.result?.value || [];
12226
12583
  if (!accounts.length) return 0;
12227
12584
  let total = 0;
@@ -12229,7 +12586,8 @@ var SolanaLLMClient = class {
12229
12586
  total += acct.account?.data?.parsed?.info?.tokenAmount?.uiAmount || 0;
12230
12587
  }
12231
12588
  return total;
12232
- } catch {
12589
+ } catch (error) {
12590
+ if (options?.strict) throw error;
12233
12591
  return 0;
12234
12592
  }
12235
12593
  }
@@ -12504,15 +12862,16 @@ var SolanaLLMClient = class {
12504
12862
  * `"paid-or-in-doubt"`, and so is any error with an API key once the billed
12505
12863
  * request was sent, except the account API's explicit 4xx refusal.
12506
12864
  */
12507
- async requestWithPayment(endpoint, body) {
12865
+ async requestWithPayment(endpoint, body, timeoutMs) {
12508
12866
  const url = `${this.apiUrl}${endpoint}`;
12867
+ const json = JSON.stringify(body);
12509
12868
  const closesAtStart = await this.batchPayer?.closeFence();
12510
12869
  for (let staleRetries = 0; ; ) {
12511
12870
  const response = await this.sendUnpaid(url, {
12512
12871
  method: "POST",
12513
12872
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
12514
- body: JSON.stringify(body)
12515
- });
12873
+ body: json
12874
+ }, timeoutMs);
12516
12875
  if (response.status === 402) {
12517
12876
  try {
12518
12877
  let paymentRequired;
@@ -12528,16 +12887,16 @@ var SolanaLLMClient = class {
12528
12887
  (paymentHeaders) => this.fetchWithTimeout(url, {
12529
12888
  method: "POST",
12530
12889
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
12531
- body: JSON.stringify(body)
12532
- }),
12890
+ body: json
12891
+ }, timeoutMs),
12533
12892
  // A fresh, unpaid challenge after a 429 wait: the request itself,
12534
12893
  // sent again without a payment. Every error it throws is unpaid.
12535
12894
  async () => {
12536
12895
  const challenge = await this.sendUnpaid(url, {
12537
12896
  method: "POST",
12538
12897
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
12539
- body: JSON.stringify(body)
12540
- });
12898
+ body: json
12899
+ }, timeoutMs);
12541
12900
  if (challenge.status === 402) {
12542
12901
  try {
12543
12902
  return { kind: "challenge", paymentRequired: await this.readPaymentRequired(challenge) };
@@ -12552,13 +12911,14 @@ var SolanaLLMClient = class {
12552
12911
  );
12553
12912
  if (batch.kind === "paid") {
12554
12913
  this.recordSettlement(batch.chargedUsd);
12555
- return await this.readPaidJson(batch.response);
12914
+ const paid = await this.readPaidJson(batch.response);
12915
+ return withCost(paid, batch.response, batch.chargedUsd, "batch");
12556
12916
  }
12557
- if (batch.kind === "served") return await this.readUnpaidJson(batch.response);
12917
+ if (batch.kind === "served") return this.withUnpaidCost(await this.readUnpaidJson(batch.response));
12558
12918
  if (batch.kind === "failed") throw batch.error;
12559
12919
  exactRequired = batch.paymentRequired ?? paymentRequired;
12560
12920
  }
12561
- return await this.handlePaymentAndRetry(url, body, exactRequired, staleRetries > 0);
12921
+ return await this.handlePaymentAndRetry(url, json, exactRequired, staleRetries > 0, timeoutMs);
12562
12922
  } catch (error) {
12563
12923
  if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
12564
12924
  await waitForStaleRetry(staleRetries++);
@@ -12566,9 +12926,21 @@ var SolanaLLMClient = class {
12566
12926
  }
12567
12927
  }
12568
12928
  if (!response.ok) throw await unpaidApiError(response);
12569
- return await this.readUnpaidJson(response);
12929
+ return this.withUnpaidCost(await this.readUnpaidJson(response));
12570
12930
  }
12571
12931
  }
12932
+ /**
12933
+ * Cost of a chat response served without a payment: 0 with a wallet (a free
12934
+ * model). With an API key the call is billed to the account, so costUsd stays
12935
+ * undefined (see the dashboard).
12936
+ */
12937
+ withUnpaidCost(served) {
12938
+ if (!this.apiAuth) {
12939
+ served.costUsd = 0;
12940
+ served.settlement = { scheme: "free", quotedUsd: 0 };
12941
+ }
12942
+ return served;
12943
+ }
12572
12944
  /**
12573
12945
  * Read the JSON body of a 2xx served without a payment: the free tier, or
12574
12946
  * with an API key, where this very request was the billed one.
@@ -12585,9 +12957,9 @@ var SolanaLLMClient = class {
12585
12957
  * {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
12586
12958
  * {@link accountErrorDisposition} (the request itself is billed).
12587
12959
  */
12588
- async sendUnpaid(url, init) {
12960
+ async sendUnpaid(url, init, timeoutMs) {
12589
12961
  try {
12590
- return await this.fetchWithTimeout(url, init);
12962
+ return await this.fetchWithTimeout(url, init, timeoutMs);
12591
12963
  } catch (error) {
12592
12964
  throw withDisposition(error, this.apiAuth ? accountErrorDisposition(error) : "unpaid");
12593
12965
  }
@@ -12856,7 +13228,7 @@ var SolanaLLMClient = class {
12856
13228
  if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
12857
13229
  const fromAddress = await this.getWalletAddress();
12858
13230
  const secretKey = await solanaKeyToBytes(this.privateKey);
12859
- const extensions = paymentRequired.extensions;
13231
+ const extensions = withPaymentIdentifier(paymentRequired).extensions;
12860
13232
  const paymentPayload = await createSolanaPaymentPayload(
12861
13233
  secretKey,
12862
13234
  fromAddress,
@@ -12879,7 +13251,7 @@ var SolanaLLMClient = class {
12879
13251
  );
12880
13252
  return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
12881
13253
  }
12882
- async handlePaymentAndRetry(url, body, paymentRequired, forceFreshBlockhash = false) {
13254
+ async handlePaymentAndRetry(url, json, paymentRequired, forceFreshBlockhash = false, timeoutMs) {
12883
13255
  let signed;
12884
13256
  try {
12885
13257
  signed = await this.signExactPayment(url, paymentRequired, forceFreshBlockhash, `${this.apiUrl}/v1/chat/completions`);
@@ -12896,14 +13268,15 @@ var SolanaLLMClient = class {
12896
13268
  "User-Agent": USER_AGENT,
12897
13269
  "PAYMENT-SIGNATURE": paymentPayload
12898
13270
  },
12899
- body: JSON.stringify(body)
12900
- });
13271
+ body: json
13272
+ }, timeoutMs);
12901
13273
  } catch (error) {
12902
13274
  throw withDisposition(error, "paid-or-in-doubt");
12903
13275
  }
12904
13276
  await this.assertPaid(retryResponse);
12905
13277
  this.recordSettlement(costUsd);
12906
- return this.readPaidJson(retryResponse);
13278
+ const paid = await this.readPaidJson(retryResponse);
13279
+ return withCost(paid, retryResponse, costUsd, "exact");
12907
13280
  }
12908
13281
  async requestWithPaymentRaw(endpoint, body, media = {}) {
12909
13282
  const url = `${this.apiUrl}${endpoint}`;
@@ -13618,6 +13991,7 @@ export {
13618
13991
  BatchPaymentUnresolvedError,
13619
13992
  BlockrunClient,
13620
13993
  BlockrunError,
13994
+ DEFAULT_BATCH_IN_DOUBT,
13621
13995
  DEFAULT_BATCH_RATE_LIMIT,
13622
13996
  EVM_NETWORKS,
13623
13997
  ImageClient,
@@ -13681,6 +14055,7 @@ export {
13681
14055
  loadWallet,
13682
14056
  logCost,
13683
14057
  parsePaymentRequired,
14058
+ readSettlement,
13684
14059
  retryDisposition,
13685
14060
  saveSolanaWallet,
13686
14061
  saveToCache,
@@ -13695,6 +14070,8 @@ export {
13695
14070
  solanaKeyToBytes,
13696
14071
  solanaPublicKey,
13697
14072
  status,
14073
+ toWireSearchParameters,
14074
+ toWireTools,
13698
14075
  validateMaxTokens,
13699
14076
  validateModel,
13700
14077
  validateTemperature,