@blockrun/llm 3.21.0 → 3.22.1

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.cjs CHANGED
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  BatchPaymentUnresolvedError: () => BatchPaymentUnresolvedError,
41
41
  BlockrunClient: () => BlockrunClient,
42
42
  BlockrunError: () => BlockrunError,
43
+ DEFAULT_BATCH_IN_DOUBT: () => DEFAULT_BATCH_IN_DOUBT,
43
44
  DEFAULT_BATCH_RATE_LIMIT: () => DEFAULT_BATCH_RATE_LIMIT,
44
45
  EVM_NETWORKS: () => EVM_NETWORKS,
45
46
  ImageClient: () => ImageClient,
@@ -103,6 +104,7 @@ __export(index_exports, {
103
104
  loadWallet: () => loadWallet,
104
105
  logCost: () => logCost,
105
106
  parsePaymentRequired: () => parsePaymentRequired,
107
+ readSettlement: () => readSettlement,
106
108
  retryDisposition: () => retryDisposition,
107
109
  saveSolanaWallet: () => saveSolanaWallet,
108
110
  saveToCache: () => saveToCache,
@@ -117,6 +119,8 @@ __export(index_exports, {
117
119
  solanaKeyToBytes: () => solanaKeyToBytes,
118
120
  solanaPublicKey: () => solanaPublicKey,
119
121
  status: () => status,
122
+ toWireSearchParameters: () => toWireSearchParameters,
123
+ toWireTools: () => toWireTools,
120
124
  validateMaxTokens: () => validateMaxTokens,
121
125
  validateModel: () => validateModel,
122
126
  validateTemperature: () => validateTemperature,
@@ -439,6 +443,130 @@ var ApiKeyAuth = class {
439
443
  }
440
444
  };
441
445
 
446
+ // src/search-wire.ts
447
+ function put(out, key, value) {
448
+ if (value !== void 0) out[key] = value;
449
+ }
450
+ function handles(list) {
451
+ return list?.map((h) => h.trim().replace(/^@+/, ""));
452
+ }
453
+ function sourceToWire(source) {
454
+ const raw = source;
455
+ const out = { ...raw };
456
+ for (const key of Object.keys(out)) if (/[A-Z]/.test(key)) delete out[key];
457
+ switch (source.type) {
458
+ case "x":
459
+ put(out, "included_x_handles", handles(source.includedXHandles) ?? out.included_x_handles);
460
+ put(out, "excluded_x_handles", handles(source.excludedXHandles) ?? out.excluded_x_handles);
461
+ put(out, "post_favorite_count", source.postFavoriteCount);
462
+ put(out, "post_view_count", source.postViewCount);
463
+ put(out, "enable_image_understanding", source.enableImageUnderstanding);
464
+ put(out, "enable_video_understanding", source.enableVideoUnderstanding);
465
+ break;
466
+ case "web":
467
+ put(out, "country", source.country);
468
+ put(out, "allowed_websites", source.allowedWebsites);
469
+ put(out, "excluded_websites", source.excludedWebsites);
470
+ put(out, "safe_search", source.safeSearch);
471
+ put(out, "enable_image_understanding", source.enableImageUnderstanding);
472
+ break;
473
+ case "news":
474
+ put(out, "country", source.country);
475
+ put(out, "allowed_websites", source.allowedWebsites);
476
+ put(out, "excluded_websites", source.excludedWebsites);
477
+ put(out, "safe_search", source.safeSearch);
478
+ break;
479
+ case "rss":
480
+ put(out, "links", source.links);
481
+ break;
482
+ }
483
+ return out;
484
+ }
485
+ function toWireSearchParameters(sp) {
486
+ const raw = sp;
487
+ const out = {};
488
+ for (const [key, value] of Object.entries(raw)) if (!/[A-Z]/.test(key) && key !== "sources") out[key] = value;
489
+ put(out, "mode", sp.mode);
490
+ if (sp.sources) out.sources = sp.sources.map(sourceToWire);
491
+ put(out, "return_citations", sp.returnCitations);
492
+ put(out, "from_date", sp.fromDate);
493
+ put(out, "to_date", sp.toDate);
494
+ put(out, "max_search_results", sp.maxSearchResults);
495
+ put(out, "max_turns", sp.maxTurns);
496
+ return out;
497
+ }
498
+ function toWireTools(tools) {
499
+ return tools.map((tool) => {
500
+ if (tool.type === "x_search") {
501
+ const out = { type: "x_search" };
502
+ put(out, "allowed_x_handles", handles(tool.allowedXHandles));
503
+ put(out, "excluded_x_handles", handles(tool.excludedXHandles));
504
+ put(out, "from_date", tool.fromDate);
505
+ put(out, "to_date", tool.toDate);
506
+ put(out, "enable_image_understanding", tool.enableImageUnderstanding);
507
+ put(out, "enable_video_understanding", tool.enableVideoUnderstanding);
508
+ return out;
509
+ }
510
+ if (tool.type === "web_search") {
511
+ const out = { type: "web_search" };
512
+ put(out, "allowed_domains", tool.allowedDomains);
513
+ put(out, "excluded_domains", tool.excludedDomains);
514
+ put(out, "enable_image_understanding", tool.enableImageUnderstanding);
515
+ return out;
516
+ }
517
+ return tool;
518
+ });
519
+ }
520
+
521
+ // src/receipt.ts
522
+ function atomicToUsd(amount) {
523
+ return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
524
+ }
525
+ function receiptChargedUsd(receipt) {
526
+ return atomicToUsd(receipt?.extra?.chargedAmount) ?? atomicToUsd(receipt?.amount);
527
+ }
528
+ function decode(header) {
529
+ try {
530
+ const json = typeof Buffer !== "undefined" ? Buffer.from(header, "base64").toString("utf8") : atob(header);
531
+ const parsed = JSON.parse(json);
532
+ return parsed && typeof parsed === "object" ? parsed : void 0;
533
+ } catch {
534
+ return void 0;
535
+ }
536
+ }
537
+ function readSettlement(response, quotedUsd, scheme) {
538
+ const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
539
+ const receipt = header ? decode(header) : void 0;
540
+ const extra = receipt?.extra;
541
+ const costUsd = receiptChargedUsd({
542
+ amount: receipt?.amount,
543
+ extra: extra && typeof extra === "object" ? extra : void 0
544
+ }) ?? quotedUsd;
545
+ const receiptTx = typeof receipt?.transaction === "string" && receipt.transaction ? receipt.transaction : void 0;
546
+ const transaction = receiptTx ?? (response.headers.get("X-Payment-Receipt")?.trim() || void 0);
547
+ const settlement = {
548
+ scheme,
549
+ quotedUsd,
550
+ ...transaction ? { transaction } : {},
551
+ ...typeof receipt?.network === "string" ? { network: receipt.network } : {},
552
+ ...receipt?.success === false ? { success: false } : {}
553
+ };
554
+ return { costUsd, settlement };
555
+ }
556
+ function settlementSkipped(response) {
557
+ return response.headers.get("X-Settlement-Skipped") === "free-fallback";
558
+ }
559
+ function withCost(body, response, bookedUsd, scheme) {
560
+ if (settlementSkipped(response)) {
561
+ body.costUsd = 0;
562
+ body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped: "free-fallback" };
563
+ return body;
564
+ }
565
+ body.costUsd = bookedUsd;
566
+ body.settlement = readSettlement(response, bookedUsd, scheme).settlement;
567
+ return body;
568
+ }
569
+
442
570
  // src/client.ts
443
571
  var import_accounts2 = require("viem/accounts");
444
572
 
@@ -4225,6 +4353,11 @@ function errSummary(err) {
4225
4353
  }
4226
4354
  return String(err).slice(0, 100);
4227
4355
  }
4356
+ function requestsSearch(options) {
4357
+ if (options.tools?.some((tool) => tool.type === "x_search" || tool.type === "web_search")) return true;
4358
+ if (options.searchParameters !== void 0) return options.searchParameters.mode !== "off";
4359
+ return options.search === true;
4360
+ }
4228
4361
  function routingProfileForModel(model) {
4229
4362
  return AUTO_ROUTING_PROFILES[model.toLowerCase()];
4230
4363
  }
@@ -4252,7 +4385,7 @@ function routingText(messages) {
4252
4385
  };
4253
4386
  }
4254
4387
  function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, options = {}) {
4255
- const tools = options.tools ?? [];
4388
+ const tools = (options.tools ?? []).filter((tool) => tool.type === "function");
4256
4389
  const requiresTools = options.toolChoice === "none" ? false : options.toolChoice === "required" || typeof options.toolChoice === "object" ? true : void 0;
4257
4390
  const decision = route(prompt, systemPrompt, maxOutputTokens, {
4258
4391
  config: DEFAULT_ROUTING_CONFIG,
@@ -4272,6 +4405,15 @@ function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, o
4272
4405
  const resolved = !id.startsWith("free/") ? id : modelPricing.has(`nvidia/${id.slice(5)}`) ? `nvidia/${id.slice(5)}` : null;
4273
4406
  if (resolved && !callable.includes(resolved)) callable.push(resolved);
4274
4407
  }
4408
+ if (requestsSearch(options)) {
4409
+ const searchable = callable.filter((id) => id.startsWith("xai/"));
4410
+ if (searchable.length === 0) {
4411
+ throw new Error(
4412
+ `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.`
4413
+ );
4414
+ }
4415
+ callable.splice(0, callable.length, ...searchable);
4416
+ }
4275
4417
  const estimatedInputTokens = Math.ceil(
4276
4418
  Math.max(options.conversationChars ?? 0, `${systemPrompt ?? ""} ${prompt}`.length) / 4
4277
4419
  );
@@ -4690,7 +4832,7 @@ function getCostSummary() {
4690
4832
  }
4691
4833
 
4692
4834
  // src/version.ts
4693
- var SDK_VERSION = "3.21.0";
4835
+ var SDK_VERSION = "3.22.1";
4694
4836
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4695
4837
 
4696
4838
  // src/client.ts
@@ -4836,7 +4978,9 @@ var LLMClient = class _LLMClient {
4836
4978
  options?.maxOutputTokens ?? options?.maxTokens ?? DEFAULT_MAX_TOKENS,
4837
4979
  {
4838
4980
  routingProfile: options?.routingProfile,
4839
- requiresStructuredOutput: options?.responseFormat !== void 0
4981
+ requiresStructuredOutput: options?.responseFormat !== void 0,
4982
+ search: options?.search,
4983
+ searchParameters: options?.searchParameters
4840
4984
  }
4841
4985
  );
4842
4986
  }
@@ -4903,6 +5047,8 @@ var LLMClient = class _LLMClient {
4903
5047
  requiresStructuredOutput: options.responseFormat !== void 0,
4904
5048
  tools: options.tools,
4905
5049
  toolChoice: options.toolChoice,
5050
+ search: options.search,
5051
+ searchParameters: options.searchParameters,
4906
5052
  conversationChars,
4907
5053
  hasVision
4908
5054
  }
@@ -4917,6 +5063,7 @@ var LLMClient = class _LLMClient {
4917
5063
  toolChoice: options.toolChoice,
4918
5064
  responseFormat: options.responseFormat,
4919
5065
  stop: options.stop,
5066
+ timeout: options.timeout,
4920
5067
  // An explicit caller-supplied chain wins over the routed one.
4921
5068
  fallbackModels: options.fallbackModels ?? decision.fallbacks
4922
5069
  });
@@ -4993,11 +5140,11 @@ var LLMClient = class _LLMClient {
4993
5140
  if (options?.temperature !== void 0) body.temperature = options.temperature;
4994
5141
  if (options?.topP !== void 0) body.top_p = options.topP;
4995
5142
  if (options?.searchParameters !== void 0) {
4996
- body.search_parameters = options.searchParameters;
5143
+ body.search_parameters = toWireSearchParameters(options.searchParameters);
4997
5144
  } else if (options?.search === true) {
4998
5145
  body.search_parameters = { mode: "on" };
4999
5146
  }
5000
- if (options?.tools !== void 0) body.tools = options.tools;
5147
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
5001
5148
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
5002
5149
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
5003
5150
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -5008,7 +5155,7 @@ var LLMClient = class _LLMClient {
5008
5155
  for (let i = 0; i < chain.length; i++) {
5009
5156
  const candidate = chain[i];
5010
5157
  try {
5011
- return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
5158
+ return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
5012
5159
  } catch (err) {
5013
5160
  lastErr = err;
5014
5161
  const next = chain[i + 1];
@@ -5079,19 +5226,19 @@ var LLMClient = class _LLMClient {
5079
5226
  * is `"paid-or-in-doubt"`. With an API key the request itself is billed, so
5080
5227
  * only the account API's explicit 4xx refusal is `"unpaid"`.
5081
5228
  */
5082
- async requestWithPayment(endpoint, body) {
5229
+ async requestWithPayment(endpoint, body, timeoutMs) {
5083
5230
  const url = `${this.apiUrl}${endpoint}`;
5084
5231
  const unpaid = {
5085
5232
  method: "POST",
5086
5233
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
5087
5234
  body: JSON.stringify(body)
5088
5235
  };
5089
- const response = await this.sendUnpaid(url, unpaid);
5236
+ const response = await this.sendUnpaid(url, unpaid, timeoutMs);
5090
5237
  if (response.status === 502 || response.status === 503) {
5091
5238
  await new Promise((r) => setTimeout(r, 1e3));
5092
- const retryResp = await this.sendUnpaid(url, unpaid);
5239
+ const retryResp = await this.sendUnpaid(url, unpaid, timeoutMs);
5093
5240
  if (retryResp.status !== 502 && retryResp.status !== 503) {
5094
- if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp);
5241
+ if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp, timeoutMs);
5095
5242
  if (!retryResp.ok) {
5096
5243
  let errorBody;
5097
5244
  try {
@@ -5108,7 +5255,7 @@ var LLMClient = class _LLMClient {
5108
5255
  }
5109
5256
  }
5110
5257
  if (response.status === 402) {
5111
- return this.handlePaymentAndRetry(url, body, response);
5258
+ return this.handlePaymentAndRetry(url, body, response, timeoutMs);
5112
5259
  }
5113
5260
  if (!response.ok) {
5114
5261
  let errorBody;
@@ -5133,9 +5280,9 @@ var LLMClient = class _LLMClient {
5133
5280
  * {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
5134
5281
  * {@link accountErrorDisposition} (the request itself is billed).
5135
5282
  */
5136
- async sendUnpaid(url, init) {
5283
+ async sendUnpaid(url, init, timeoutMs) {
5137
5284
  try {
5138
- return await this.fetchWithTimeout(url, init);
5285
+ return await this.fetchWithTimeout(url, init, timeoutMs);
5139
5286
  } catch (err) {
5140
5287
  throw withDisposition(err, this.apiAuth ? accountErrorDisposition(err) : "unpaid");
5141
5288
  }
@@ -5143,13 +5290,22 @@ var LLMClient = class _LLMClient {
5143
5290
  /**
5144
5291
  * Parse a response to a request that carried no payment: the free tier,
5145
5292
  * or, with an API key, the billed request itself.
5293
+ *
5294
+ * With a wallet nothing was paid, so `costUsd` is 0 (a free model). With an
5295
+ * API key the call is billed to the account and `costUsd` stays undefined.
5146
5296
  */
5147
5297
  async parseUnpaidChatResponse(response) {
5298
+ let served;
5148
5299
  try {
5149
- return await this.parseChatResponse(response);
5300
+ served = await this.parseChatResponse(response);
5150
5301
  } catch (err) {
5151
5302
  throw withDisposition(err, this.apiAuth ? "paid-or-in-doubt" : "unpaid");
5152
5303
  }
5304
+ if (!this.apiAuth) {
5305
+ served.costUsd = 0;
5306
+ served.settlement = { scheme: "free", quotedUsd: 0 };
5307
+ }
5308
+ return served;
5153
5309
  }
5154
5310
  /**
5155
5311
  * Handle 402 response: parse requirements, sign payment, retry.
@@ -5157,7 +5313,7 @@ var LLMClient = class _LLMClient {
5157
5313
  * Anything thrown before the signed payment is sent is `"unpaid"`; anything
5158
5314
  * after it, `"paid-or-in-doubt"` (see {@link RetryDisposition}).
5159
5315
  */
5160
- async handlePaymentAndRetry(url, body, response) {
5316
+ async handlePaymentAndRetry(url, body, response, timeoutMs) {
5161
5317
  let signed;
5162
5318
  try {
5163
5319
  signed = await this.signFrom402(url, response);
@@ -5165,7 +5321,7 @@ var LLMClient = class _LLMClient {
5165
5321
  throw withDisposition(err, "unpaid");
5166
5322
  }
5167
5323
  try {
5168
- return await this.sendPaid(url, body, signed.paymentPayload, signed.details);
5324
+ return await this.sendPaid(url, body, signed.paymentPayload, signed.details, timeoutMs);
5169
5325
  } catch (err) {
5170
5326
  throw withDisposition(err, "paid-or-in-doubt");
5171
5327
  }
@@ -5210,7 +5366,7 @@ var LLMClient = class _LLMClient {
5210
5366
  return { paymentPayload, details };
5211
5367
  }
5212
5368
  /** Send the signed payment and read the answer. The payment may settle from here on. */
5213
- async sendPaid(url, body, paymentPayload, details) {
5369
+ async sendPaid(url, body, paymentPayload, details, timeoutMs) {
5214
5370
  const retryResponse = await this.fetchWithTimeout(url, {
5215
5371
  method: "POST",
5216
5372
  headers: {
@@ -5219,7 +5375,7 @@ var LLMClient = class _LLMClient {
5219
5375
  "PAYMENT-SIGNATURE": paymentPayload
5220
5376
  },
5221
5377
  body: JSON.stringify(body)
5222
- });
5378
+ }, timeoutMs);
5223
5379
  if (retryResponse.status === 502 || retryResponse.status === 503) {
5224
5380
  await new Promise((r) => setTimeout(r, 1e3));
5225
5381
  const retryResp2 = await this.fetchWithTimeout(url, {
@@ -5230,7 +5386,7 @@ var LLMClient = class _LLMClient {
5230
5386
  "PAYMENT-SIGNATURE": paymentPayload
5231
5387
  },
5232
5388
  body: JSON.stringify(body)
5233
- });
5389
+ }, timeoutMs);
5234
5390
  if (retryResp2.status !== 502 && retryResp2.status !== 503) {
5235
5391
  if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
5236
5392
  if (!retryResp2.ok) {
@@ -5242,11 +5398,7 @@ var LLMClient = class _LLMClient {
5242
5398
  }
5243
5399
  throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
5244
5400
  }
5245
- const costUsd2 = parseFloat(details.amount) / 1e6;
5246
- this.sessionCalls += 1;
5247
- this.sessionTotalUsd += costUsd2;
5248
- this.recordCost(url, costUsd2, { body, network: details.network });
5249
- return this.parseChatResponse(retryResp2);
5401
+ return this.bookPaid(url, body, details, retryResp2);
5250
5402
  }
5251
5403
  }
5252
5404
  if (retryResponse.status === 402) {
@@ -5265,11 +5417,22 @@ var LLMClient = class _LLMClient {
5265
5417
  sanitizeErrorResponse(errorBody)
5266
5418
  );
5267
5419
  }
5420
+ return this.bookPaid(url, body, details, retryResponse);
5421
+ }
5422
+ /**
5423
+ * Book a served, paid chat call and attach its cost. A response the gateway
5424
+ * served from a free fallback with settlement skipped
5425
+ * (`X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
5426
+ * not added to `getSpending()` or the cost log, and reports `costUsd: 0`.
5427
+ */
5428
+ async bookPaid(url, body, details, response) {
5268
5429
  const costUsd = parseFloat(details.amount) / 1e6;
5269
- this.sessionCalls += 1;
5270
- this.sessionTotalUsd += costUsd;
5271
- this.recordCost(url, costUsd, { body, network: details.network });
5272
- return this.parseChatResponse(retryResponse);
5430
+ if (!settlementSkipped(response)) {
5431
+ this.sessionCalls += 1;
5432
+ this.sessionTotalUsd += costUsd;
5433
+ this.recordCost(url, costUsd, { body, network: details.network });
5434
+ }
5435
+ return withCost(await this.parseChatResponse(response), response, costUsd, "exact");
5273
5436
  }
5274
5437
  /**
5275
5438
  * Sign a payment header and return the PAYMENT-SIGNATURE value.
@@ -5339,7 +5502,7 @@ var LLMClient = class _LLMClient {
5339
5502
  };
5340
5503
  if (options?.temperature !== void 0) body.temperature = options.temperature;
5341
5504
  if (options?.topP !== void 0) body.top_p = options.topP;
5342
- if (options?.tools !== void 0) body.tools = options.tools;
5505
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
5343
5506
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
5344
5507
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
5345
5508
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -5737,11 +5900,11 @@ var LLMClient = class _LLMClient {
5737
5900
  return retryResponse.json();
5738
5901
  }
5739
5902
  /**
5740
- * Fetch with timeout.
5903
+ * Fetch with timeout: `timeoutMs` (a per-call override), else the client's `timeout`.
5741
5904
  */
5742
- async fetchWithTimeout(url, options) {
5905
+ async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
5743
5906
  const controller = new AbortController();
5744
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
5907
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
5745
5908
  try {
5746
5909
  const response = await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, {
5747
5910
  ...options,
@@ -9726,6 +9889,7 @@ var import_bs582 = __toESM(require("bs58"), 1);
9726
9889
  var BATCH_SCHEME = "batch-settlement";
9727
9890
  var BLOCKRUN_SOL_OPERATOR = "5YKPQUFjw5WQqhSUkEGKNNfYYVqnRRNbpYyL71qQ1vm3";
9728
9891
  var DEFAULT_BATCH_RATE_LIMIT = { maxAttempts: 3, maxWaitMs: 6e4 };
9892
+ var DEFAULT_BATCH_IN_DOUBT = { maxWaitMs: 3e5 };
9729
9893
  var BatchPaymentUnresolvedError = class extends PaymentError {
9730
9894
  /** The {@link SolanaBatchEvent.reason} of the `unresolved` event reported with it. */
9731
9895
  reason;
@@ -10402,6 +10566,46 @@ function withoutClientSignedBatch(paymentRequired) {
10402
10566
  const accepts = paymentRequired.accepts ?? [];
10403
10567
  return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
10404
10568
  }
10569
+ var PAYMENT_IDENTIFIER = "payment-identifier";
10570
+ function withPaymentIdentifier(paymentRequired) {
10571
+ const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
10572
+ if (typeof declared?.info?.required !== "boolean") return paymentRequired;
10573
+ const id = `pay_${(0, import_crypto2.randomUUID)().replace(/-/g, "")}`;
10574
+ return {
10575
+ ...paymentRequired,
10576
+ extensions: { ...paymentRequired.extensions, [PAYMENT_IDENTIFIER]: { ...declared, info: { ...declared.info, id } } }
10577
+ };
10578
+ }
10579
+ function peekReceipt(http, getHeader) {
10580
+ try {
10581
+ return http.getPaymentSettleResponse(getHeader);
10582
+ } catch {
10583
+ return void 0;
10584
+ }
10585
+ }
10586
+ function receiptSucceeded(http, answer) {
10587
+ return peekReceipt(http, (name) => answer.headers.get(name))?.success === true;
10588
+ }
10589
+ function hasPaymentIdentifier(payload) {
10590
+ const info = payload.extensions?.[PAYMENT_IDENTIFIER]?.info;
10591
+ return typeof info?.id === "string";
10592
+ }
10593
+ var IN_DOUBT_MIN_WAIT_MS = 1e3;
10594
+ var IN_DOUBT_MAX_WAIT_MS = 3e4;
10595
+ function replayMayResolve(doubt) {
10596
+ if (doubt.unanswered) return true;
10597
+ const answer = doubt.held;
10598
+ if (!answer) return false;
10599
+ return answer.status >= 500 || answer.status === 429 || answer.status === 409 && Boolean(answer.headers.get("retry-after")?.trim());
10600
+ }
10601
+ function inDoubtWait(doubt, replay) {
10602
+ const asked = doubt.held ? parseRetryAfter(doubt.held.headers.get("retry-after")) : void 0;
10603
+ return Math.min(IN_DOUBT_MAX_WAIT_MS, Math.max(IN_DOUBT_MIN_WAIT_MS, asked ?? backoffDelay(replay)));
10604
+ }
10605
+ function replayDoubt(outcome) {
10606
+ if (outcome.kind === "charged" || outcome.kind === "in_doubt") return outcome;
10607
+ return { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
10608
+ }
10405
10609
  function trustedBatchAccepts(paymentRequired, operators) {
10406
10610
  return (paymentRequired.accepts ?? []).filter((candidate) => {
10407
10611
  const extra = candidate.extra;
@@ -10411,9 +10615,6 @@ function trustedBatchAccepts(paymentRequired, operators) {
10411
10615
  function trustedBatchAccept(paymentRequired, operators) {
10412
10616
  return trustedBatchAccepts(paymentRequired, operators)[0];
10413
10617
  }
10414
- function atomicToUsd(amount) {
10415
- return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
10416
- }
10417
10618
  function chargedUsd(getReceipt) {
10418
10619
  let receipt;
10419
10620
  try {
@@ -10421,7 +10622,7 @@ function chargedUsd(getReceipt) {
10421
10622
  } catch {
10422
10623
  return 0;
10423
10624
  }
10424
- return atomicToUsd(receipt?.extra?.chargedAmount) ?? atomicToUsd(receipt?.amount) ?? 0;
10625
+ return receiptChargedUsd(receipt) ?? 0;
10425
10626
  }
10426
10627
  function rebuiltCharge(settled, payload) {
10427
10628
  if (settled?.success !== true) return 0n;
@@ -10541,13 +10742,42 @@ function parseRetryAfter(value, now = Date.now()) {
10541
10742
  }
10542
10743
  return Number.isFinite(ms) && ms <= MAX_RETRY_AFTER_MS ? ms : void 0;
10543
10744
  }
10544
- var NOT_BROADCAST_429 = /* @__PURE__ */ new Set([
10745
+ var NOT_BROADCAST_REFUSALS = /* @__PURE__ */ new Set([
10746
+ // 429
10545
10747
  "batch_account_channel_capacity_exhausted",
10546
10748
  "batch_channel_capacity_exhausted",
10547
- "batch_deposit_rate_limited"
10749
+ "batch_deposit_rate_limited",
10750
+ "batch_deposit_ip_rate_limited",
10751
+ // 503
10752
+ "batch_signer_funding_headroom_exhausted",
10753
+ "batch_new_channels_disabled",
10754
+ "batch_public_new_channels_disabled",
10755
+ // 403
10756
+ "batch_account_admission_paused",
10757
+ "batch_channel_owner_mismatch",
10758
+ "batch_fee_payer_unavailable",
10759
+ "batch_fee_payer_not_dedicated",
10760
+ "batch_fee_payer_lane_not_authorized",
10761
+ "batch_transaction_fee_payer_mismatch",
10762
+ // 400
10763
+ "batch_initial_deposit_out_of_range",
10764
+ "batch_deposit_asset_not_supported",
10765
+ "batch_withdraw_delay_out_of_range"
10548
10766
  ]);
10549
10767
  function provesNothingBroadcast(receipt) {
10550
- return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_429.has(receipt.errorReason);
10768
+ return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_REFUSALS.has(receipt.errorReason);
10769
+ }
10770
+ var CONFIG_REFUSALS = /* @__PURE__ */ new Set([
10771
+ "batch_initial_deposit_out_of_range",
10772
+ "batch_deposit_asset_not_supported",
10773
+ "batch_withdraw_delay_out_of_range"
10774
+ ]);
10775
+ var REFUSAL_COOLDOWN_MS = 6e4;
10776
+ var CONFIG_REFUSAL_COOLDOWN_MS = 10 * 6e4;
10777
+ var REFUSAL_COOLDOWN_MAX_MS = 15 * 6e4;
10778
+ function refusalCooldownMs(errorReason, retryAfterMs2) {
10779
+ if (retryAfterMs2 !== void 0) return Math.min(retryAfterMs2, REFUSAL_COOLDOWN_MAX_MS);
10780
+ return errorReason !== void 0 && CONFIG_REFUSALS.has(errorReason) ? CONFIG_REFUSAL_COOLDOWN_MS : REFUSAL_COOLDOWN_MS;
10551
10781
  }
10552
10782
  function backoffDelay(attempt) {
10553
10783
  const base = 1e3 * 2 ** Math.max(0, attempt - 1);
@@ -10680,7 +10910,7 @@ function intentTarget(intent) {
10680
10910
  var SolanaBatchPayer = class {
10681
10911
  constructor(init) {
10682
10912
  this.init = init;
10683
- const { operators, rateLimit } = init.options;
10913
+ const { operators, rateLimit, inDoubt } = init.options;
10684
10914
  if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
10685
10915
  throw new Error("batch.operators must list at least one base58 operator public key");
10686
10916
  }
@@ -10692,11 +10922,16 @@ var SolanaBatchPayer = class {
10692
10922
  if (!Number.isFinite(this.maxWaitMs) || this.maxWaitMs < 0) {
10693
10923
  throw new Error("batch.rateLimit.maxWaitMs must be a finite number of ms >= 0");
10694
10924
  }
10925
+ this.inDoubtMaxWaitMs = inDoubt?.maxWaitMs ?? DEFAULT_BATCH_IN_DOUBT.maxWaitMs;
10926
+ if (!Number.isFinite(this.inDoubtMaxWaitMs) || this.inDoubtMaxWaitMs < 0) {
10927
+ throw new Error("batch.inDoubt.maxWaitMs must be a finite number of ms >= 0");
10928
+ }
10695
10929
  }
10696
10930
  init;
10697
10931
  state;
10698
10932
  maxAttempts;
10699
10933
  maxWaitMs;
10934
+ inDoubtMaxWaitMs;
10700
10935
  counters = {
10701
10936
  fallbacks: 0,
10702
10937
  fallbacksByReason: {},
@@ -11065,14 +11300,19 @@ var SolanaBatchPayer = class {
11065
11300
  * the call's result (`served`, nothing paid); one that fails otherwise
11066
11301
  * is the call's error (`failed`, `"unpaid"`), never an exact payment
11067
11302
  * against the stale challenge;
11068
- * - in doubt: ends only with a definitive success receipt from the one
11069
- * replay a receipt-less 429 is allowed ({@link resolveInDoubt}), or a
11070
- * thrown {@link BatchPaymentUnresolvedError}. No new authorization, no
11071
- * new deposit, no exact payment and no fallback model is ever paid for a
11072
- * call in doubt.
11303
+ * - in doubt: ends only with a definitive success receipt from a replay
11304
+ * of the identical payment ({@link resolveInDoubt}: the one replay a
11305
+ * receipt-less 429 is allowed, or the replays of a payment carrying a
11306
+ * payment identifier while the gateway says one can succeed, within
11307
+ * `inDoubt.maxWaitMs`), or a thrown {@link BatchPaymentUnresolvedError}.
11308
+ * No new authorization, no new deposit, no exact payment and no fallback
11309
+ * model is ever paid for a call in doubt.
11073
11310
  *
11074
11311
  * The wallet stays in cooldown after a 429, so its other calls wait
11075
- * instead of sending opens of their own.
11312
+ * instead of sending opens of their own. After a deposit the facilitator
11313
+ * refused before broadcasting it (a 402 with a failed `batch_*` admission
11314
+ * receipt), its calls do not wait: they pay exact at once
11315
+ * (`admission_refused`) until the refusal cooldown ends.
11076
11316
  *
11077
11317
  * @param closesAtStart - {@link closeFence}, taken before the call's first
11078
11318
  * (unpaid) request. If a close completed since, the call pays exact
@@ -11136,6 +11376,15 @@ var SolanaBatchPayer = class {
11136
11376
  try {
11137
11377
  for (; ; ) {
11138
11378
  const now = Date.now();
11379
+ if ((wallet.refusedUntil ?? 0) > now) {
11380
+ return fallback({
11381
+ reason: "admission_refused",
11382
+ errorReason: wallet.refusedFor,
11383
+ retryAfterMs: (wallet.refusedUntil ?? 0) - now,
11384
+ attempt: budget.attempt,
11385
+ detail: `the facilitator refused this wallet's last batch deposit (${wallet.refusedFor ?? "unknown"}); batch is skipped until the refusal cooldown ends`
11386
+ });
11387
+ }
11139
11388
  if (wallet.cooldownUntil > now && wallet.cooldownUntil !== waitedUntil) {
11140
11389
  const wait = wallet.cooldownUntil - now;
11141
11390
  if (budget.waited + wait > this.maxWaitMs) {
@@ -11225,12 +11474,23 @@ var SolanaBatchPayer = class {
11225
11474
  if (built.kind !== "authorization") {
11226
11475
  if (outcome.depositSafe) this.forgetIntent(wallet, built.key);
11227
11476
  else this.distrust(wallet, built, "deposit_refused");
11477
+ if (outcome.refusedForMs !== void 0) {
11478
+ const until = Date.now() + outcome.refusedForMs;
11479
+ if (until > (wallet.refusedUntil ?? 0)) {
11480
+ wallet.refusedUntil = until;
11481
+ wallet.refusedFor = outcome.errorReason;
11482
+ }
11483
+ }
11228
11484
  }
11229
11485
  sent = void 0;
11230
11486
  release();
11231
11487
  if (!outcome.rateLimited) {
11232
- const { kind: _kind, rateLimited: _limited, depositSafe: _safe, ...event } = outcome;
11233
- return fallback({ ...event, attempt: budget.attempt });
11488
+ const { kind: _kind, rateLimited: _limited, depositSafe: _safe, refusedForMs, ...event } = outcome;
11489
+ return fallback({
11490
+ ...event,
11491
+ ...refusedForMs !== void 0 && built.kind !== "authorization" ? { retryAfterMs: refusedForMs } : {},
11492
+ attempt: budget.attempt
11493
+ });
11234
11494
  }
11235
11495
  const wait = this.coolDown(wallet, outcome.rateLimited.retryAfterMs ?? backoffDelay(budget.attempt));
11236
11496
  if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
@@ -11276,7 +11536,7 @@ var SolanaBatchPayer = class {
11276
11536
  if (built.kind !== "authorization" && !(built.key && wallet.resyncs.has(built.key))) this.forgetIntent(wallet, built.key);
11277
11537
  sent = void 0;
11278
11538
  release();
11279
- await this.report({ type: "recovered", reason: "rate_limited", attempt: budget.attempt });
11539
+ await this.report({ type: "recovered", reason: outcome.status === 429 ? "rate_limited" : "in_doubt", attempt: budget.attempt });
11280
11540
  return { kind: "paid", response: resolved.response, chargedUsd: resolved.chargedUsd };
11281
11541
  }
11282
11542
  return await this.raiseUnresolved(wallet, built, resolved, budget.attempt);
@@ -11325,21 +11585,24 @@ var SolanaBatchPayer = class {
11325
11585
  /**
11326
11586
  * Try to end a payment in doubt with the gateway's own word.
11327
11587
  *
11328
- * The one source of evidence this SDK accepts today is a definitive
11329
- * success receipt on the single byte-identical replay a 429 without a
11330
- * receipt is allowed, after its backoff (the owner's bounded safety
11331
- * attempt). Any other answer to it (a 402 or `duplicate_settlement`, which
11332
- * mean the original reached the gateway; another 429; a 5xx; a timeout)
11333
- * leaves it in doubt. Every other in-doubt outcome is never replayed.
11588
+ * The one source of evidence this SDK accepts is a definitive success
11589
+ * receipt on a byte-identical replay of the payment. Any other answer to a
11590
+ * replay (a 402 or `duplicate_settlement`, which mean the original reached
11591
+ * the gateway; a 409; another 429; a 5xx; a timeout) leaves it in doubt.
11592
+ *
11593
+ * - A payment carrying a payment identifier is replayed for as long as
11594
+ * the gateway says a later replay can succeed ({@link replayInDoubt}).
11595
+ * - Any other payment gets a replay only after a 429 without a receipt:
11596
+ * one, after its backoff (the owner's bounded safety attempt), within
11597
+ * `rateLimit`. Every other in-doubt outcome is never replayed.
11334
11598
  *
11335
11599
  * Chain state is not evidence here: it can settle a deposit (see
11336
- * `resync`), never whether the gateway charged. Gateway-side evidence (a
11337
- * receipt on every response, a request-status endpoint with a fence)
11338
- * plugs in here, before the caller raises.
11600
+ * `resync`), never whether the gateway charged.
11339
11601
  *
11340
11602
  * @returns the charged replay, or the doubt to raise.
11341
11603
  */
11342
11604
  async resolveInDoubt(wallet, sent, doubt, send, budget) {
11605
+ if (hasPaymentIdentifier(sent.payload)) return this.replayInDoubt(wallet, sent, doubt, send, budget.attempt);
11343
11606
  if (!doubt.replayable) return doubt;
11344
11607
  const wait = this.coolDown(wallet, doubt.retryAfterMs ?? backoffDelay(budget.attempt));
11345
11608
  if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
@@ -11359,9 +11622,94 @@ var SolanaBatchPayer = class {
11359
11622
  budget.waited += wait;
11360
11623
  await sleep2(wait);
11361
11624
  budget.attempt += 1;
11362
- const replayed = await this.sendOnce(wallet, sent, send, true, true);
11363
- if (replayed.kind === "charged") return replayed;
11364
- return replayed.kind === "in_doubt" ? replayed : { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
11625
+ return replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
11626
+ }
11627
+ /**
11628
+ * Replay a payment in doubt that carries a payment identifier until the
11629
+ * gateway's answer settles it.
11630
+ *
11631
+ * A gateway that declares `payment-identifier` stores each paid response
11632
+ * under its identifier and answers a byte-identical replay of a completed
11633
+ * request with it, the original receipt included; while the original is
11634
+ * still running, it answers 409 with a `Retry-After`. So the identical
11635
+ * payment (the same `PAYMENT-SIGNATURE` header and the same body) is
11636
+ * replayed for as long as the last answer says a later one can succeed
11637
+ * ({@link replayMayResolve}), after {@link inDoubtWait} each time, as long
11638
+ * as the wait fits in `inDoubt.maxWaitMs`, counted from the first doubt:
11639
+ * every wait (as long as it really took) and every replay's round trip. A
11640
+ * replay already sent is not cut short. These replays do not count
11641
+ * against `rateLimit`; a 429 among them still puts the wallet in cooldown.
11642
+ *
11643
+ * Every answer that does not settle it is held back from the scheme
11644
+ * ({@link InDoubt.held}) and handed over only once no replay follows, so
11645
+ * the scheme rolls the payment back once, or takes the receipt that answer
11646
+ * carries. That is the last answer, unless an earlier one carried a
11647
+ * success receipt and the later ones do not: a success receipt is the
11648
+ * gateway's word that it charged, and handing the scheme a receipt-less
11649
+ * refusal of a replay instead would roll a real charge back and desync the
11650
+ * channel's cumulative.
11651
+ *
11652
+ * @returns the charged replay, or the doubt to raise.
11653
+ */
11654
+ async replayInDoubt(wallet, sent, first, send, attempt) {
11655
+ let doubt = first;
11656
+ let held = first.held;
11657
+ let heldSucceeded = held !== void 0 && receiptSucceeded(sent.http, held);
11658
+ let replays = 0;
11659
+ let spent = 0;
11660
+ const stop = async (note) => {
11661
+ if (!doubt.settled && held) {
11662
+ const answer = held;
11663
+ await this.settle(sent.http, sent.payload, (name) => answer.headers.get(name), answer.status);
11664
+ }
11665
+ const notes = [replays > 0 ? `${replays} replay${replays === 1 ? "" : "s"} did not resolve it` : "", note ?? ""];
11666
+ return {
11667
+ ...doubt,
11668
+ settled: doubt.settled || held !== void 0,
11669
+ held: void 0,
11670
+ firstReason: first.reason,
11671
+ detail: [doubt.detail, ...notes].filter(Boolean).join("; ")
11672
+ };
11673
+ };
11674
+ for (; ; ) {
11675
+ if (!replayMayResolve(doubt)) {
11676
+ return stop(doubt.held?.status === 409 ? "the gateway sent no Retry-After, so no replay can resolve it" : void 0);
11677
+ }
11678
+ const wait = inDoubtWait(doubt, replays + 1);
11679
+ const status2 = doubt.held?.status;
11680
+ if (status2 === 429) this.coolDown(wallet, (doubt.held && parseRetryAfter(doubt.held.headers.get("retry-after"))) ?? wait);
11681
+ if (spent + wait > this.inDoubtMaxWaitMs) {
11682
+ return stop(`batch.inDoubt.maxWaitMs (${this.inDoubtMaxWaitMs} ms) leaves no room for ${replays > 0 ? "another replay" : "a replay"}`);
11683
+ }
11684
+ await this.report({
11685
+ type: "backoff",
11686
+ reason: status2 === 429 ? "rate_limited" : "in_doubt",
11687
+ status: status2,
11688
+ errorReason: doubt.errorReason,
11689
+ retryAfterMs: wait,
11690
+ attempt,
11691
+ detail: `replay ${replays + 1} of the payment in doubt`
11692
+ });
11693
+ const sleptFrom = Date.now();
11694
+ await sleep2(wait);
11695
+ spent += Math.max(wait, Date.now() - sleptFrom);
11696
+ replays += 1;
11697
+ const startedAt = Date.now();
11698
+ const replayed = replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
11699
+ spent += Math.max(0, Date.now() - startedAt);
11700
+ if (replayed.kind === "charged") return replayed;
11701
+ doubt = replayed;
11702
+ if (doubt.held) {
11703
+ const succeeded = receiptSucceeded(sent.http, doubt.held);
11704
+ if (succeeded || !heldSucceeded) {
11705
+ held = doubt.held;
11706
+ heldSucceeded = succeeded;
11707
+ }
11708
+ } else if (doubt.settled) {
11709
+ held = void 0;
11710
+ heldSucceeded = false;
11711
+ }
11712
+ }
11365
11713
  }
11366
11714
  /**
11367
11715
  * Raise a payment in doubt: release the scheme's hold on it (the channel
@@ -11372,10 +11720,11 @@ var SolanaBatchPayer = class {
11372
11720
  async raiseUnresolved(wallet, sent, doubt, attempt) {
11373
11721
  if (!doubt.settled) await this.settle(sent.http, sent.payload, () => null, 0);
11374
11722
  if (sent.kind !== "authorization") {
11723
+ const reason = doubt.firstReason ?? doubt.reason;
11375
11724
  this.distrust(
11376
11725
  wallet,
11377
11726
  sent,
11378
- doubt.reason === "no_response" ? "deposit_unanswered" : doubt.reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
11727
+ reason === "no_response" ? "deposit_unanswered" : reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
11379
11728
  );
11380
11729
  }
11381
11730
  await this.report({
@@ -11462,8 +11811,8 @@ var SolanaBatchPayer = class {
11462
11811
  * here, and only for a first send.
11463
11812
  *
11464
11813
  * @param retry - count this send in `retries` (it follows this call's own 429).
11465
- * @param replay - this is the one replay of a receipt-less 429: only a 2xx
11466
- * with a success receipt counts, everything else stays in doubt.
11814
+ * @param replay - this is a replay of a payment in doubt: only a 2xx with
11815
+ * a success receipt counts, everything else stays in doubt.
11467
11816
  */
11468
11817
  async sendOnce(wallet, sent, send, retry, replay) {
11469
11818
  if (retry) this.counters.retries += 1;
@@ -11477,6 +11826,7 @@ var SolanaBatchPayer = class {
11477
11826
  reason: replay ? "replay_unresolved" : "no_response",
11478
11827
  replayable: false,
11479
11828
  settled: false,
11829
+ unanswered: true,
11480
11830
  detail: `${replay ? "the replay" : "the payment"} got no answer (${err instanceof Error ? `${err.name}: ${err.message}` : String(err)})`,
11481
11831
  cause: err
11482
11832
  };
@@ -11501,6 +11851,7 @@ var SolanaBatchPayer = class {
11501
11851
  const getHeader = (name) => response.headers.get(name);
11502
11852
  const receipted = Boolean(getHeader("PAYMENT-RESPONSE") || getHeader("X-PAYMENT-RESPONSE"));
11503
11853
  const status2 = response.status;
11854
+ const hold = hasPaymentIdentifier(payload) && (!response.ok || replay && peekReceipt(http, getHeader)?.success !== true);
11504
11855
  if (status2 === 429 && !receipted) {
11505
11856
  return {
11506
11857
  kind: "in_doubt",
@@ -11510,12 +11861,27 @@ var SolanaBatchPayer = class {
11510
11861
  status: status2,
11511
11862
  retryAfterMs: parseRetryAfter(getHeader("retry-after")),
11512
11863
  errorReason: await rateLimitReason(response, void 0),
11513
- detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt"
11864
+ detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt",
11865
+ ...hold ? { held: response } : {}
11514
11866
  };
11515
11867
  }
11868
+ const outcome = await this.classifyAnswer(wallet, sent, response, replay, hold);
11869
+ if (!hold) return outcome;
11870
+ if (outcome.kind === "in_doubt") return { ...outcome, settled: false, held: response };
11871
+ await this.settle(http, payload, getHeader, status2);
11872
+ return outcome;
11873
+ }
11874
+ /**
11875
+ * {@link classify}, past a receipt-less 429. With `hold`, the answer is not
11876
+ * handed to the scheme: its receipt is only read.
11877
+ */
11878
+ async classifyAnswer(wallet, sent, response, replay, hold) {
11879
+ const { http, payload } = sent;
11880
+ const getHeader = (name) => response.headers.get(name);
11881
+ const status2 = response.status;
11516
11882
  const channelId = payloadChannelId(payload);
11517
11883
  const before = channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0;
11518
- const reconciled = await this.settle(http, payload, getHeader, status2);
11884
+ const reconciled = hold ? { settleResponse: peekReceipt(http, getHeader), ok: false } : await this.settle(http, payload, getHeader, status2);
11519
11885
  const receipt = reconciled.settleResponse;
11520
11886
  const definitive = receipt?.success === true && reconciled.ok && recordAdvanced(before, channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0, payload, receipt);
11521
11887
  if (!response.ok && !replay && sent.kind === "authorization" && cancelledReceipt(receipt)) {
@@ -11578,7 +11944,15 @@ var SolanaBatchPayer = class {
11578
11944
  cause: await afterPaymentError(response)
11579
11945
  };
11580
11946
  }
11581
- return { kind: "not_charged", reason: "payment_required", status: status2, errorReason: receipt?.errorReason, depositSafe: provesNothingBroadcast(receipt) };
11947
+ const refused = provesNothingBroadcast(receipt);
11948
+ return {
11949
+ kind: "not_charged",
11950
+ reason: "payment_required",
11951
+ status: status2,
11952
+ errorReason: receipt?.errorReason,
11953
+ depositSafe: refused,
11954
+ ...refused ? { refusedForMs: refusalCooldownMs(receipt?.errorReason, parseRetryAfter(getHeader("retry-after"))) } : {}
11955
+ };
11582
11956
  }
11583
11957
  const refusal = await batchRefusal(response);
11584
11958
  if (refusal) {
@@ -11609,7 +11983,7 @@ var SolanaBatchPayer = class {
11609
11983
  const http = (await this.build(wallet)).http;
11610
11984
  try {
11611
11985
  await this.prepareTopUp(wallet, paymentRequired);
11612
- return { http, payload: await http.createPaymentPayload(withoutClientSignedBatch(paymentRequired)) };
11986
+ return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(withoutClientSignedBatch(paymentRequired))) };
11613
11987
  } catch (err) {
11614
11988
  if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
11615
11989
  } finally {
@@ -11702,13 +12076,7 @@ var SolanaBatchPayer = class {
11702
12076
  console.error(
11703
12077
  `[@blockrun/llm] batch-settlement receipt not reconciled: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
11704
12078
  );
11705
- let settleResponse;
11706
- try {
11707
- settleResponse = http.getPaymentSettleResponse(getHeader);
11708
- } catch {
11709
- settleResponse = void 0;
11710
- }
11711
- return { settleResponse, ok: false };
12079
+ return { settleResponse: peekReceipt(http, getHeader), ok: false };
11712
12080
  }
11713
12081
  }
11714
12082
  /**
@@ -12091,9 +12459,9 @@ var SolanaLLMClient = class {
12091
12459
  };
12092
12460
  if (options?.temperature !== void 0) body.temperature = options.temperature;
12093
12461
  if (options?.topP !== void 0) body.top_p = options.topP;
12094
- if (options?.searchParameters !== void 0) body.search_parameters = options.searchParameters;
12462
+ if (options?.searchParameters !== void 0) body.search_parameters = toWireSearchParameters(options.searchParameters);
12095
12463
  else if (options?.search === true) body.search_parameters = { mode: "on" };
12096
- if (options?.tools !== void 0) body.tools = options.tools;
12464
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
12097
12465
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
12098
12466
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
12099
12467
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -12104,7 +12472,7 @@ var SolanaLLMClient = class {
12104
12472
  for (let index = 0; index < chain.length; index += 1) {
12105
12473
  const candidate = chain[index];
12106
12474
  try {
12107
- return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
12475
+ return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
12108
12476
  } catch (error) {
12109
12477
  lastError = error;
12110
12478
  if (!isTransientError(error) || index === chain.length - 1) throw error;
@@ -12154,6 +12522,8 @@ var SolanaLLMClient = class {
12154
12522
  {
12155
12523
  routingProfile: options?.routingProfile,
12156
12524
  requiresStructuredOutput: options?.responseFormat !== void 0,
12525
+ search: options?.search,
12526
+ searchParameters: options?.searchParameters,
12157
12527
  minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
12158
12528
  }
12159
12529
  );
@@ -12181,6 +12551,8 @@ var SolanaLLMClient = class {
12181
12551
  requiresStructuredOutput: options.responseFormat !== void 0,
12182
12552
  tools: options.tools,
12183
12553
  toolChoice: options.toolChoice,
12554
+ search: options.search,
12555
+ searchParameters: options.searchParameters,
12184
12556
  conversationChars,
12185
12557
  hasVision,
12186
12558
  minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
@@ -12196,6 +12568,7 @@ var SolanaLLMClient = class {
12196
12568
  toolChoice: options.toolChoice,
12197
12569
  responseFormat: options.responseFormat,
12198
12570
  stop: options.stop,
12571
+ timeout: options.timeout,
12199
12572
  // An explicit caller-supplied chain wins over the routed one.
12200
12573
  fallbackModels: options.fallbackModels ?? decision.fallbacks
12201
12574
  });
@@ -12333,7 +12706,7 @@ var SolanaLLMClient = class {
12333
12706
  *
12334
12707
  * @returns USDC balance as a float
12335
12708
  */
12336
- async getBalance() {
12709
+ async getBalance(options) {
12337
12710
  const usdc_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
12338
12711
  const address = await this.getWalletAddress();
12339
12712
  try {
@@ -12348,6 +12721,9 @@ var SolanaLLMClient = class {
12348
12721
  })
12349
12722
  });
12350
12723
  const data = await response.json();
12724
+ if (options?.strict && (!response.ok || data.error)) {
12725
+ throw new Error(`Solana RPC balance lookup failed: ${data.error?.message ?? `HTTP ${response.status}`}`);
12726
+ }
12351
12727
  const accounts = data.result?.value || [];
12352
12728
  if (!accounts.length) return 0;
12353
12729
  let total = 0;
@@ -12355,7 +12731,8 @@ var SolanaLLMClient = class {
12355
12731
  total += acct.account?.data?.parsed?.info?.tokenAmount?.uiAmount || 0;
12356
12732
  }
12357
12733
  return total;
12358
- } catch {
12734
+ } catch (error) {
12735
+ if (options?.strict) throw error;
12359
12736
  return 0;
12360
12737
  }
12361
12738
  }
@@ -12630,15 +13007,16 @@ var SolanaLLMClient = class {
12630
13007
  * `"paid-or-in-doubt"`, and so is any error with an API key once the billed
12631
13008
  * request was sent, except the account API's explicit 4xx refusal.
12632
13009
  */
12633
- async requestWithPayment(endpoint, body) {
13010
+ async requestWithPayment(endpoint, body, timeoutMs) {
12634
13011
  const url = `${this.apiUrl}${endpoint}`;
13012
+ const json = JSON.stringify(body);
12635
13013
  const closesAtStart = await this.batchPayer?.closeFence();
12636
13014
  for (let staleRetries = 0; ; ) {
12637
13015
  const response = await this.sendUnpaid(url, {
12638
13016
  method: "POST",
12639
13017
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
12640
- body: JSON.stringify(body)
12641
- });
13018
+ body: json
13019
+ }, timeoutMs);
12642
13020
  if (response.status === 402) {
12643
13021
  try {
12644
13022
  let paymentRequired;
@@ -12654,16 +13032,16 @@ var SolanaLLMClient = class {
12654
13032
  (paymentHeaders) => this.fetchWithTimeout(url, {
12655
13033
  method: "POST",
12656
13034
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
12657
- body: JSON.stringify(body)
12658
- }),
13035
+ body: json
13036
+ }, timeoutMs),
12659
13037
  // A fresh, unpaid challenge after a 429 wait: the request itself,
12660
13038
  // sent again without a payment. Every error it throws is unpaid.
12661
13039
  async () => {
12662
13040
  const challenge = await this.sendUnpaid(url, {
12663
13041
  method: "POST",
12664
13042
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
12665
- body: JSON.stringify(body)
12666
- });
13043
+ body: json
13044
+ }, timeoutMs);
12667
13045
  if (challenge.status === 402) {
12668
13046
  try {
12669
13047
  return { kind: "challenge", paymentRequired: await this.readPaymentRequired(challenge) };
@@ -12678,13 +13056,14 @@ var SolanaLLMClient = class {
12678
13056
  );
12679
13057
  if (batch.kind === "paid") {
12680
13058
  this.recordSettlement(batch.chargedUsd);
12681
- return await this.readPaidJson(batch.response);
13059
+ const paid = await this.readPaidJson(batch.response);
13060
+ return withCost(paid, batch.response, batch.chargedUsd, "batch");
12682
13061
  }
12683
- if (batch.kind === "served") return await this.readUnpaidJson(batch.response);
13062
+ if (batch.kind === "served") return this.withUnpaidCost(await this.readUnpaidJson(batch.response));
12684
13063
  if (batch.kind === "failed") throw batch.error;
12685
13064
  exactRequired = batch.paymentRequired ?? paymentRequired;
12686
13065
  }
12687
- return await this.handlePaymentAndRetry(url, body, exactRequired, staleRetries > 0);
13066
+ return await this.handlePaymentAndRetry(url, json, exactRequired, staleRetries > 0, timeoutMs);
12688
13067
  } catch (error) {
12689
13068
  if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
12690
13069
  await waitForStaleRetry(staleRetries++);
@@ -12692,9 +13071,21 @@ var SolanaLLMClient = class {
12692
13071
  }
12693
13072
  }
12694
13073
  if (!response.ok) throw await unpaidApiError(response);
12695
- return await this.readUnpaidJson(response);
13074
+ return this.withUnpaidCost(await this.readUnpaidJson(response));
12696
13075
  }
12697
13076
  }
13077
+ /**
13078
+ * Cost of a chat response served without a payment: 0 with a wallet (a free
13079
+ * model). With an API key the call is billed to the account, so costUsd stays
13080
+ * undefined (see the dashboard).
13081
+ */
13082
+ withUnpaidCost(served) {
13083
+ if (!this.apiAuth) {
13084
+ served.costUsd = 0;
13085
+ served.settlement = { scheme: "free", quotedUsd: 0 };
13086
+ }
13087
+ return served;
13088
+ }
12698
13089
  /**
12699
13090
  * Read the JSON body of a 2xx served without a payment: the free tier, or
12700
13091
  * with an API key, where this very request was the billed one.
@@ -12711,9 +13102,9 @@ var SolanaLLMClient = class {
12711
13102
  * {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
12712
13103
  * {@link accountErrorDisposition} (the request itself is billed).
12713
13104
  */
12714
- async sendUnpaid(url, init) {
13105
+ async sendUnpaid(url, init, timeoutMs) {
12715
13106
  try {
12716
- return await this.fetchWithTimeout(url, init);
13107
+ return await this.fetchWithTimeout(url, init, timeoutMs);
12717
13108
  } catch (error) {
12718
13109
  throw withDisposition(error, this.apiAuth ? accountErrorDisposition(error) : "unpaid");
12719
13110
  }
@@ -12982,7 +13373,7 @@ var SolanaLLMClient = class {
12982
13373
  if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
12983
13374
  const fromAddress = await this.getWalletAddress();
12984
13375
  const secretKey = await solanaKeyToBytes(this.privateKey);
12985
- const extensions = paymentRequired.extensions;
13376
+ const extensions = withPaymentIdentifier(paymentRequired).extensions;
12986
13377
  const paymentPayload = await createSolanaPaymentPayload(
12987
13378
  secretKey,
12988
13379
  fromAddress,
@@ -13005,7 +13396,7 @@ var SolanaLLMClient = class {
13005
13396
  );
13006
13397
  return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
13007
13398
  }
13008
- async handlePaymentAndRetry(url, body, paymentRequired, forceFreshBlockhash = false) {
13399
+ async handlePaymentAndRetry(url, json, paymentRequired, forceFreshBlockhash = false, timeoutMs) {
13009
13400
  let signed;
13010
13401
  try {
13011
13402
  signed = await this.signExactPayment(url, paymentRequired, forceFreshBlockhash, `${this.apiUrl}/v1/chat/completions`);
@@ -13022,14 +13413,15 @@ var SolanaLLMClient = class {
13022
13413
  "User-Agent": USER_AGENT,
13023
13414
  "PAYMENT-SIGNATURE": paymentPayload
13024
13415
  },
13025
- body: JSON.stringify(body)
13026
- });
13416
+ body: json
13417
+ }, timeoutMs);
13027
13418
  } catch (error) {
13028
13419
  throw withDisposition(error, "paid-or-in-doubt");
13029
13420
  }
13030
13421
  await this.assertPaid(retryResponse);
13031
- this.recordSettlement(costUsd);
13032
- return this.readPaidJson(retryResponse);
13422
+ if (!settlementSkipped(retryResponse)) this.recordSettlement(costUsd);
13423
+ const paid = await this.readPaidJson(retryResponse);
13424
+ return withCost(paid, retryResponse, costUsd, "exact");
13033
13425
  }
13034
13426
  async requestWithPaymentRaw(endpoint, body, media = {}) {
13035
13427
  const url = `${this.apiUrl}${endpoint}`;
@@ -13745,6 +14137,7 @@ var AnthropicClient = class {
13745
14137
  BatchPaymentUnresolvedError,
13746
14138
  BlockrunClient,
13747
14139
  BlockrunError,
14140
+ DEFAULT_BATCH_IN_DOUBT,
13748
14141
  DEFAULT_BATCH_RATE_LIMIT,
13749
14142
  EVM_NETWORKS,
13750
14143
  ImageClient,
@@ -13807,6 +14200,7 @@ var AnthropicClient = class {
13807
14200
  loadWallet,
13808
14201
  logCost,
13809
14202
  parsePaymentRequired,
14203
+ readSettlement,
13810
14204
  retryDisposition,
13811
14205
  saveSolanaWallet,
13812
14206
  saveToCache,
@@ -13821,6 +14215,8 @@ var AnthropicClient = class {
13821
14215
  solanaKeyToBytes,
13822
14216
  solanaPublicKey,
13823
14217
  status,
14218
+ toWireSearchParameters,
14219
+ toWireTools,
13824
14220
  validateMaxTokens,
13825
14221
  validateModel,
13826
14222
  validateTemperature,