@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.js CHANGED
@@ -313,6 +313,130 @@ 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 settlementSkipped(response) {
427
+ return response.headers.get("X-Settlement-Skipped") === "free-fallback";
428
+ }
429
+ function withCost(body, response, bookedUsd, scheme) {
430
+ if (settlementSkipped(response)) {
431
+ body.costUsd = 0;
432
+ body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped: "free-fallback" };
433
+ return body;
434
+ }
435
+ body.costUsd = bookedUsd;
436
+ body.settlement = readSettlement(response, bookedUsd, scheme).settlement;
437
+ return body;
438
+ }
439
+
316
440
  // src/client.ts
317
441
  import { privateKeyToAccount } from "viem/accounts";
318
442
 
@@ -4099,6 +4223,11 @@ function errSummary(err) {
4099
4223
  }
4100
4224
  return String(err).slice(0, 100);
4101
4225
  }
4226
+ function requestsSearch(options) {
4227
+ if (options.tools?.some((tool) => tool.type === "x_search" || tool.type === "web_search")) return true;
4228
+ if (options.searchParameters !== void 0) return options.searchParameters.mode !== "off";
4229
+ return options.search === true;
4230
+ }
4102
4231
  function routingProfileForModel(model) {
4103
4232
  return AUTO_ROUTING_PROFILES[model.toLowerCase()];
4104
4233
  }
@@ -4126,7 +4255,7 @@ function routingText(messages) {
4126
4255
  };
4127
4256
  }
4128
4257
  function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, options = {}) {
4129
- const tools = options.tools ?? [];
4258
+ const tools = (options.tools ?? []).filter((tool) => tool.type === "function");
4130
4259
  const requiresTools = options.toolChoice === "none" ? false : options.toolChoice === "required" || typeof options.toolChoice === "object" ? true : void 0;
4131
4260
  const decision = route(prompt, systemPrompt, maxOutputTokens, {
4132
4261
  config: DEFAULT_ROUTING_CONFIG,
@@ -4146,6 +4275,15 @@ function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, o
4146
4275
  const resolved = !id.startsWith("free/") ? id : modelPricing.has(`nvidia/${id.slice(5)}`) ? `nvidia/${id.slice(5)}` : null;
4147
4276
  if (resolved && !callable.includes(resolved)) callable.push(resolved);
4148
4277
  }
4278
+ if (requestsSearch(options)) {
4279
+ const searchable = callable.filter((id) => id.startsWith("xai/"));
4280
+ if (searchable.length === 0) {
4281
+ throw new Error(
4282
+ `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.`
4283
+ );
4284
+ }
4285
+ callable.splice(0, callable.length, ...searchable);
4286
+ }
4149
4287
  const estimatedInputTokens = Math.ceil(
4150
4288
  Math.max(options.conversationChars ?? 0, `${systemPrompt ?? ""} ${prompt}`.length) / 4
4151
4289
  );
@@ -4564,7 +4702,7 @@ function getCostSummary() {
4564
4702
  }
4565
4703
 
4566
4704
  // src/version.ts
4567
- var SDK_VERSION = "3.21.0";
4705
+ var SDK_VERSION = "3.22.1";
4568
4706
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4569
4707
 
4570
4708
  // src/client.ts
@@ -4710,7 +4848,9 @@ var LLMClient = class _LLMClient {
4710
4848
  options?.maxOutputTokens ?? options?.maxTokens ?? DEFAULT_MAX_TOKENS,
4711
4849
  {
4712
4850
  routingProfile: options?.routingProfile,
4713
- requiresStructuredOutput: options?.responseFormat !== void 0
4851
+ requiresStructuredOutput: options?.responseFormat !== void 0,
4852
+ search: options?.search,
4853
+ searchParameters: options?.searchParameters
4714
4854
  }
4715
4855
  );
4716
4856
  }
@@ -4777,6 +4917,8 @@ var LLMClient = class _LLMClient {
4777
4917
  requiresStructuredOutput: options.responseFormat !== void 0,
4778
4918
  tools: options.tools,
4779
4919
  toolChoice: options.toolChoice,
4920
+ search: options.search,
4921
+ searchParameters: options.searchParameters,
4780
4922
  conversationChars,
4781
4923
  hasVision
4782
4924
  }
@@ -4791,6 +4933,7 @@ var LLMClient = class _LLMClient {
4791
4933
  toolChoice: options.toolChoice,
4792
4934
  responseFormat: options.responseFormat,
4793
4935
  stop: options.stop,
4936
+ timeout: options.timeout,
4794
4937
  // An explicit caller-supplied chain wins over the routed one.
4795
4938
  fallbackModels: options.fallbackModels ?? decision.fallbacks
4796
4939
  });
@@ -4867,11 +5010,11 @@ var LLMClient = class _LLMClient {
4867
5010
  if (options?.temperature !== void 0) body.temperature = options.temperature;
4868
5011
  if (options?.topP !== void 0) body.top_p = options.topP;
4869
5012
  if (options?.searchParameters !== void 0) {
4870
- body.search_parameters = options.searchParameters;
5013
+ body.search_parameters = toWireSearchParameters(options.searchParameters);
4871
5014
  } else if (options?.search === true) {
4872
5015
  body.search_parameters = { mode: "on" };
4873
5016
  }
4874
- if (options?.tools !== void 0) body.tools = options.tools;
5017
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
4875
5018
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
4876
5019
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
4877
5020
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -4882,7 +5025,7 @@ var LLMClient = class _LLMClient {
4882
5025
  for (let i = 0; i < chain.length; i++) {
4883
5026
  const candidate = chain[i];
4884
5027
  try {
4885
- return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
5028
+ return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
4886
5029
  } catch (err) {
4887
5030
  lastErr = err;
4888
5031
  const next = chain[i + 1];
@@ -4953,19 +5096,19 @@ var LLMClient = class _LLMClient {
4953
5096
  * is `"paid-or-in-doubt"`. With an API key the request itself is billed, so
4954
5097
  * only the account API's explicit 4xx refusal is `"unpaid"`.
4955
5098
  */
4956
- async requestWithPayment(endpoint, body) {
5099
+ async requestWithPayment(endpoint, body, timeoutMs) {
4957
5100
  const url = `${this.apiUrl}${endpoint}`;
4958
5101
  const unpaid = {
4959
5102
  method: "POST",
4960
5103
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
4961
5104
  body: JSON.stringify(body)
4962
5105
  };
4963
- const response = await this.sendUnpaid(url, unpaid);
5106
+ const response = await this.sendUnpaid(url, unpaid, timeoutMs);
4964
5107
  if (response.status === 502 || response.status === 503) {
4965
5108
  await new Promise((r) => setTimeout(r, 1e3));
4966
- const retryResp = await this.sendUnpaid(url, unpaid);
5109
+ const retryResp = await this.sendUnpaid(url, unpaid, timeoutMs);
4967
5110
  if (retryResp.status !== 502 && retryResp.status !== 503) {
4968
- if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp);
5111
+ if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp, timeoutMs);
4969
5112
  if (!retryResp.ok) {
4970
5113
  let errorBody;
4971
5114
  try {
@@ -4982,7 +5125,7 @@ var LLMClient = class _LLMClient {
4982
5125
  }
4983
5126
  }
4984
5127
  if (response.status === 402) {
4985
- return this.handlePaymentAndRetry(url, body, response);
5128
+ return this.handlePaymentAndRetry(url, body, response, timeoutMs);
4986
5129
  }
4987
5130
  if (!response.ok) {
4988
5131
  let errorBody;
@@ -5007,9 +5150,9 @@ var LLMClient = class _LLMClient {
5007
5150
  * {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
5008
5151
  * {@link accountErrorDisposition} (the request itself is billed).
5009
5152
  */
5010
- async sendUnpaid(url, init) {
5153
+ async sendUnpaid(url, init, timeoutMs) {
5011
5154
  try {
5012
- return await this.fetchWithTimeout(url, init);
5155
+ return await this.fetchWithTimeout(url, init, timeoutMs);
5013
5156
  } catch (err) {
5014
5157
  throw withDisposition(err, this.apiAuth ? accountErrorDisposition(err) : "unpaid");
5015
5158
  }
@@ -5017,13 +5160,22 @@ var LLMClient = class _LLMClient {
5017
5160
  /**
5018
5161
  * Parse a response to a request that carried no payment: the free tier,
5019
5162
  * or, with an API key, the billed request itself.
5163
+ *
5164
+ * With a wallet nothing was paid, so `costUsd` is 0 (a free model). With an
5165
+ * API key the call is billed to the account and `costUsd` stays undefined.
5020
5166
  */
5021
5167
  async parseUnpaidChatResponse(response) {
5168
+ let served;
5022
5169
  try {
5023
- return await this.parseChatResponse(response);
5170
+ served = await this.parseChatResponse(response);
5024
5171
  } catch (err) {
5025
5172
  throw withDisposition(err, this.apiAuth ? "paid-or-in-doubt" : "unpaid");
5026
5173
  }
5174
+ if (!this.apiAuth) {
5175
+ served.costUsd = 0;
5176
+ served.settlement = { scheme: "free", quotedUsd: 0 };
5177
+ }
5178
+ return served;
5027
5179
  }
5028
5180
  /**
5029
5181
  * Handle 402 response: parse requirements, sign payment, retry.
@@ -5031,7 +5183,7 @@ var LLMClient = class _LLMClient {
5031
5183
  * Anything thrown before the signed payment is sent is `"unpaid"`; anything
5032
5184
  * after it, `"paid-or-in-doubt"` (see {@link RetryDisposition}).
5033
5185
  */
5034
- async handlePaymentAndRetry(url, body, response) {
5186
+ async handlePaymentAndRetry(url, body, response, timeoutMs) {
5035
5187
  let signed;
5036
5188
  try {
5037
5189
  signed = await this.signFrom402(url, response);
@@ -5039,7 +5191,7 @@ var LLMClient = class _LLMClient {
5039
5191
  throw withDisposition(err, "unpaid");
5040
5192
  }
5041
5193
  try {
5042
- return await this.sendPaid(url, body, signed.paymentPayload, signed.details);
5194
+ return await this.sendPaid(url, body, signed.paymentPayload, signed.details, timeoutMs);
5043
5195
  } catch (err) {
5044
5196
  throw withDisposition(err, "paid-or-in-doubt");
5045
5197
  }
@@ -5084,7 +5236,7 @@ var LLMClient = class _LLMClient {
5084
5236
  return { paymentPayload, details };
5085
5237
  }
5086
5238
  /** Send the signed payment and read the answer. The payment may settle from here on. */
5087
- async sendPaid(url, body, paymentPayload, details) {
5239
+ async sendPaid(url, body, paymentPayload, details, timeoutMs) {
5088
5240
  const retryResponse = await this.fetchWithTimeout(url, {
5089
5241
  method: "POST",
5090
5242
  headers: {
@@ -5093,7 +5245,7 @@ var LLMClient = class _LLMClient {
5093
5245
  "PAYMENT-SIGNATURE": paymentPayload
5094
5246
  },
5095
5247
  body: JSON.stringify(body)
5096
- });
5248
+ }, timeoutMs);
5097
5249
  if (retryResponse.status === 502 || retryResponse.status === 503) {
5098
5250
  await new Promise((r) => setTimeout(r, 1e3));
5099
5251
  const retryResp2 = await this.fetchWithTimeout(url, {
@@ -5104,7 +5256,7 @@ var LLMClient = class _LLMClient {
5104
5256
  "PAYMENT-SIGNATURE": paymentPayload
5105
5257
  },
5106
5258
  body: JSON.stringify(body)
5107
- });
5259
+ }, timeoutMs);
5108
5260
  if (retryResp2.status !== 502 && retryResp2.status !== 503) {
5109
5261
  if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
5110
5262
  if (!retryResp2.ok) {
@@ -5116,11 +5268,7 @@ var LLMClient = class _LLMClient {
5116
5268
  }
5117
5269
  throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
5118
5270
  }
5119
- const costUsd2 = parseFloat(details.amount) / 1e6;
5120
- this.sessionCalls += 1;
5121
- this.sessionTotalUsd += costUsd2;
5122
- this.recordCost(url, costUsd2, { body, network: details.network });
5123
- return this.parseChatResponse(retryResp2);
5271
+ return this.bookPaid(url, body, details, retryResp2);
5124
5272
  }
5125
5273
  }
5126
5274
  if (retryResponse.status === 402) {
@@ -5139,11 +5287,22 @@ var LLMClient = class _LLMClient {
5139
5287
  sanitizeErrorResponse(errorBody)
5140
5288
  );
5141
5289
  }
5290
+ return this.bookPaid(url, body, details, retryResponse);
5291
+ }
5292
+ /**
5293
+ * Book a served, paid chat call and attach its cost. A response the gateway
5294
+ * served from a free fallback with settlement skipped
5295
+ * (`X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
5296
+ * not added to `getSpending()` or the cost log, and reports `costUsd: 0`.
5297
+ */
5298
+ async bookPaid(url, body, details, response) {
5142
5299
  const costUsd = parseFloat(details.amount) / 1e6;
5143
- this.sessionCalls += 1;
5144
- this.sessionTotalUsd += costUsd;
5145
- this.recordCost(url, costUsd, { body, network: details.network });
5146
- return this.parseChatResponse(retryResponse);
5300
+ if (!settlementSkipped(response)) {
5301
+ this.sessionCalls += 1;
5302
+ this.sessionTotalUsd += costUsd;
5303
+ this.recordCost(url, costUsd, { body, network: details.network });
5304
+ }
5305
+ return withCost(await this.parseChatResponse(response), response, costUsd, "exact");
5147
5306
  }
5148
5307
  /**
5149
5308
  * Sign a payment header and return the PAYMENT-SIGNATURE value.
@@ -5213,7 +5372,7 @@ var LLMClient = class _LLMClient {
5213
5372
  };
5214
5373
  if (options?.temperature !== void 0) body.temperature = options.temperature;
5215
5374
  if (options?.topP !== void 0) body.top_p = options.topP;
5216
- if (options?.tools !== void 0) body.tools = options.tools;
5375
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
5217
5376
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
5218
5377
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
5219
5378
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -5611,11 +5770,11 @@ var LLMClient = class _LLMClient {
5611
5770
  return retryResponse.json();
5612
5771
  }
5613
5772
  /**
5614
- * Fetch with timeout.
5773
+ * Fetch with timeout: `timeoutMs` (a per-call override), else the client's `timeout`.
5615
5774
  */
5616
- async fetchWithTimeout(url, options) {
5775
+ async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
5617
5776
  const controller = new AbortController();
5618
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
5777
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
5619
5778
  try {
5620
5779
  const response = await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, {
5621
5780
  ...options,
@@ -9600,6 +9759,7 @@ import bs582 from "bs58";
9600
9759
  var BATCH_SCHEME = "batch-settlement";
9601
9760
  var BLOCKRUN_SOL_OPERATOR = "5YKPQUFjw5WQqhSUkEGKNNfYYVqnRRNbpYyL71qQ1vm3";
9602
9761
  var DEFAULT_BATCH_RATE_LIMIT = { maxAttempts: 3, maxWaitMs: 6e4 };
9762
+ var DEFAULT_BATCH_IN_DOUBT = { maxWaitMs: 3e5 };
9603
9763
  var BatchPaymentUnresolvedError = class extends PaymentError {
9604
9764
  /** The {@link SolanaBatchEvent.reason} of the `unresolved` event reported with it. */
9605
9765
  reason;
@@ -10276,6 +10436,46 @@ function withoutClientSignedBatch(paymentRequired) {
10276
10436
  const accepts = paymentRequired.accepts ?? [];
10277
10437
  return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
10278
10438
  }
10439
+ var PAYMENT_IDENTIFIER = "payment-identifier";
10440
+ function withPaymentIdentifier(paymentRequired) {
10441
+ const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
10442
+ if (typeof declared?.info?.required !== "boolean") return paymentRequired;
10443
+ const id = `pay_${randomUUID().replace(/-/g, "")}`;
10444
+ return {
10445
+ ...paymentRequired,
10446
+ extensions: { ...paymentRequired.extensions, [PAYMENT_IDENTIFIER]: { ...declared, info: { ...declared.info, id } } }
10447
+ };
10448
+ }
10449
+ function peekReceipt(http, getHeader) {
10450
+ try {
10451
+ return http.getPaymentSettleResponse(getHeader);
10452
+ } catch {
10453
+ return void 0;
10454
+ }
10455
+ }
10456
+ function receiptSucceeded(http, answer) {
10457
+ return peekReceipt(http, (name) => answer.headers.get(name))?.success === true;
10458
+ }
10459
+ function hasPaymentIdentifier(payload) {
10460
+ const info = payload.extensions?.[PAYMENT_IDENTIFIER]?.info;
10461
+ return typeof info?.id === "string";
10462
+ }
10463
+ var IN_DOUBT_MIN_WAIT_MS = 1e3;
10464
+ var IN_DOUBT_MAX_WAIT_MS = 3e4;
10465
+ function replayMayResolve(doubt) {
10466
+ if (doubt.unanswered) return true;
10467
+ const answer = doubt.held;
10468
+ if (!answer) return false;
10469
+ return answer.status >= 500 || answer.status === 429 || answer.status === 409 && Boolean(answer.headers.get("retry-after")?.trim());
10470
+ }
10471
+ function inDoubtWait(doubt, replay) {
10472
+ const asked = doubt.held ? parseRetryAfter(doubt.held.headers.get("retry-after")) : void 0;
10473
+ return Math.min(IN_DOUBT_MAX_WAIT_MS, Math.max(IN_DOUBT_MIN_WAIT_MS, asked ?? backoffDelay(replay)));
10474
+ }
10475
+ function replayDoubt(outcome) {
10476
+ if (outcome.kind === "charged" || outcome.kind === "in_doubt") return outcome;
10477
+ return { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
10478
+ }
10279
10479
  function trustedBatchAccepts(paymentRequired, operators) {
10280
10480
  return (paymentRequired.accepts ?? []).filter((candidate) => {
10281
10481
  const extra = candidate.extra;
@@ -10285,9 +10485,6 @@ function trustedBatchAccepts(paymentRequired, operators) {
10285
10485
  function trustedBatchAccept(paymentRequired, operators) {
10286
10486
  return trustedBatchAccepts(paymentRequired, operators)[0];
10287
10487
  }
10288
- function atomicToUsd(amount) {
10289
- return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
10290
- }
10291
10488
  function chargedUsd(getReceipt) {
10292
10489
  let receipt;
10293
10490
  try {
@@ -10295,7 +10492,7 @@ function chargedUsd(getReceipt) {
10295
10492
  } catch {
10296
10493
  return 0;
10297
10494
  }
10298
- return atomicToUsd(receipt?.extra?.chargedAmount) ?? atomicToUsd(receipt?.amount) ?? 0;
10495
+ return receiptChargedUsd(receipt) ?? 0;
10299
10496
  }
10300
10497
  function rebuiltCharge(settled, payload) {
10301
10498
  if (settled?.success !== true) return 0n;
@@ -10415,13 +10612,42 @@ function parseRetryAfter(value, now = Date.now()) {
10415
10612
  }
10416
10613
  return Number.isFinite(ms) && ms <= MAX_RETRY_AFTER_MS ? ms : void 0;
10417
10614
  }
10418
- var NOT_BROADCAST_429 = /* @__PURE__ */ new Set([
10615
+ var NOT_BROADCAST_REFUSALS = /* @__PURE__ */ new Set([
10616
+ // 429
10419
10617
  "batch_account_channel_capacity_exhausted",
10420
10618
  "batch_channel_capacity_exhausted",
10421
- "batch_deposit_rate_limited"
10619
+ "batch_deposit_rate_limited",
10620
+ "batch_deposit_ip_rate_limited",
10621
+ // 503
10622
+ "batch_signer_funding_headroom_exhausted",
10623
+ "batch_new_channels_disabled",
10624
+ "batch_public_new_channels_disabled",
10625
+ // 403
10626
+ "batch_account_admission_paused",
10627
+ "batch_channel_owner_mismatch",
10628
+ "batch_fee_payer_unavailable",
10629
+ "batch_fee_payer_not_dedicated",
10630
+ "batch_fee_payer_lane_not_authorized",
10631
+ "batch_transaction_fee_payer_mismatch",
10632
+ // 400
10633
+ "batch_initial_deposit_out_of_range",
10634
+ "batch_deposit_asset_not_supported",
10635
+ "batch_withdraw_delay_out_of_range"
10422
10636
  ]);
10423
10637
  function provesNothingBroadcast(receipt) {
10424
- return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_429.has(receipt.errorReason);
10638
+ return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_REFUSALS.has(receipt.errorReason);
10639
+ }
10640
+ var CONFIG_REFUSALS = /* @__PURE__ */ new Set([
10641
+ "batch_initial_deposit_out_of_range",
10642
+ "batch_deposit_asset_not_supported",
10643
+ "batch_withdraw_delay_out_of_range"
10644
+ ]);
10645
+ var REFUSAL_COOLDOWN_MS = 6e4;
10646
+ var CONFIG_REFUSAL_COOLDOWN_MS = 10 * 6e4;
10647
+ var REFUSAL_COOLDOWN_MAX_MS = 15 * 6e4;
10648
+ function refusalCooldownMs(errorReason, retryAfterMs2) {
10649
+ if (retryAfterMs2 !== void 0) return Math.min(retryAfterMs2, REFUSAL_COOLDOWN_MAX_MS);
10650
+ return errorReason !== void 0 && CONFIG_REFUSALS.has(errorReason) ? CONFIG_REFUSAL_COOLDOWN_MS : REFUSAL_COOLDOWN_MS;
10425
10651
  }
10426
10652
  function backoffDelay(attempt) {
10427
10653
  const base = 1e3 * 2 ** Math.max(0, attempt - 1);
@@ -10554,7 +10780,7 @@ function intentTarget(intent) {
10554
10780
  var SolanaBatchPayer = class {
10555
10781
  constructor(init) {
10556
10782
  this.init = init;
10557
- const { operators, rateLimit } = init.options;
10783
+ const { operators, rateLimit, inDoubt } = init.options;
10558
10784
  if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
10559
10785
  throw new Error("batch.operators must list at least one base58 operator public key");
10560
10786
  }
@@ -10566,11 +10792,16 @@ var SolanaBatchPayer = class {
10566
10792
  if (!Number.isFinite(this.maxWaitMs) || this.maxWaitMs < 0) {
10567
10793
  throw new Error("batch.rateLimit.maxWaitMs must be a finite number of ms >= 0");
10568
10794
  }
10795
+ this.inDoubtMaxWaitMs = inDoubt?.maxWaitMs ?? DEFAULT_BATCH_IN_DOUBT.maxWaitMs;
10796
+ if (!Number.isFinite(this.inDoubtMaxWaitMs) || this.inDoubtMaxWaitMs < 0) {
10797
+ throw new Error("batch.inDoubt.maxWaitMs must be a finite number of ms >= 0");
10798
+ }
10569
10799
  }
10570
10800
  init;
10571
10801
  state;
10572
10802
  maxAttempts;
10573
10803
  maxWaitMs;
10804
+ inDoubtMaxWaitMs;
10574
10805
  counters = {
10575
10806
  fallbacks: 0,
10576
10807
  fallbacksByReason: {},
@@ -10939,14 +11170,19 @@ var SolanaBatchPayer = class {
10939
11170
  * the call's result (`served`, nothing paid); one that fails otherwise
10940
11171
  * is the call's error (`failed`, `"unpaid"`), never an exact payment
10941
11172
  * 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.
11173
+ * - in doubt: ends only with a definitive success receipt from a replay
11174
+ * of the identical payment ({@link resolveInDoubt}: the one replay a
11175
+ * receipt-less 429 is allowed, or the replays of a payment carrying a
11176
+ * payment identifier while the gateway says one can succeed, within
11177
+ * `inDoubt.maxWaitMs`), or a thrown {@link BatchPaymentUnresolvedError}.
11178
+ * No new authorization, no new deposit, no exact payment and no fallback
11179
+ * model is ever paid for a call in doubt.
10947
11180
  *
10948
11181
  * The wallet stays in cooldown after a 429, so its other calls wait
10949
- * instead of sending opens of their own.
11182
+ * instead of sending opens of their own. After a deposit the facilitator
11183
+ * refused before broadcasting it (a 402 with a failed `batch_*` admission
11184
+ * receipt), its calls do not wait: they pay exact at once
11185
+ * (`admission_refused`) until the refusal cooldown ends.
10950
11186
  *
10951
11187
  * @param closesAtStart - {@link closeFence}, taken before the call's first
10952
11188
  * (unpaid) request. If a close completed since, the call pays exact
@@ -11010,6 +11246,15 @@ var SolanaBatchPayer = class {
11010
11246
  try {
11011
11247
  for (; ; ) {
11012
11248
  const now = Date.now();
11249
+ if ((wallet.refusedUntil ?? 0) > now) {
11250
+ return fallback({
11251
+ reason: "admission_refused",
11252
+ errorReason: wallet.refusedFor,
11253
+ retryAfterMs: (wallet.refusedUntil ?? 0) - now,
11254
+ attempt: budget.attempt,
11255
+ detail: `the facilitator refused this wallet's last batch deposit (${wallet.refusedFor ?? "unknown"}); batch is skipped until the refusal cooldown ends`
11256
+ });
11257
+ }
11013
11258
  if (wallet.cooldownUntil > now && wallet.cooldownUntil !== waitedUntil) {
11014
11259
  const wait = wallet.cooldownUntil - now;
11015
11260
  if (budget.waited + wait > this.maxWaitMs) {
@@ -11099,12 +11344,23 @@ var SolanaBatchPayer = class {
11099
11344
  if (built.kind !== "authorization") {
11100
11345
  if (outcome.depositSafe) this.forgetIntent(wallet, built.key);
11101
11346
  else this.distrust(wallet, built, "deposit_refused");
11347
+ if (outcome.refusedForMs !== void 0) {
11348
+ const until = Date.now() + outcome.refusedForMs;
11349
+ if (until > (wallet.refusedUntil ?? 0)) {
11350
+ wallet.refusedUntil = until;
11351
+ wallet.refusedFor = outcome.errorReason;
11352
+ }
11353
+ }
11102
11354
  }
11103
11355
  sent = void 0;
11104
11356
  release();
11105
11357
  if (!outcome.rateLimited) {
11106
- const { kind: _kind, rateLimited: _limited, depositSafe: _safe, ...event } = outcome;
11107
- return fallback({ ...event, attempt: budget.attempt });
11358
+ const { kind: _kind, rateLimited: _limited, depositSafe: _safe, refusedForMs, ...event } = outcome;
11359
+ return fallback({
11360
+ ...event,
11361
+ ...refusedForMs !== void 0 && built.kind !== "authorization" ? { retryAfterMs: refusedForMs } : {},
11362
+ attempt: budget.attempt
11363
+ });
11108
11364
  }
11109
11365
  const wait = this.coolDown(wallet, outcome.rateLimited.retryAfterMs ?? backoffDelay(budget.attempt));
11110
11366
  if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
@@ -11150,7 +11406,7 @@ var SolanaBatchPayer = class {
11150
11406
  if (built.kind !== "authorization" && !(built.key && wallet.resyncs.has(built.key))) this.forgetIntent(wallet, built.key);
11151
11407
  sent = void 0;
11152
11408
  release();
11153
- await this.report({ type: "recovered", reason: "rate_limited", attempt: budget.attempt });
11409
+ await this.report({ type: "recovered", reason: outcome.status === 429 ? "rate_limited" : "in_doubt", attempt: budget.attempt });
11154
11410
  return { kind: "paid", response: resolved.response, chargedUsd: resolved.chargedUsd };
11155
11411
  }
11156
11412
  return await this.raiseUnresolved(wallet, built, resolved, budget.attempt);
@@ -11199,21 +11455,24 @@ var SolanaBatchPayer = class {
11199
11455
  /**
11200
11456
  * Try to end a payment in doubt with the gateway's own word.
11201
11457
  *
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.
11458
+ * The one source of evidence this SDK accepts is a definitive success
11459
+ * receipt on a byte-identical replay of the payment. Any other answer to a
11460
+ * replay (a 402 or `duplicate_settlement`, which mean the original reached
11461
+ * the gateway; a 409; another 429; a 5xx; a timeout) leaves it in doubt.
11462
+ *
11463
+ * - A payment carrying a payment identifier is replayed for as long as
11464
+ * the gateway says a later replay can succeed ({@link replayInDoubt}).
11465
+ * - Any other payment gets a replay only after a 429 without a receipt:
11466
+ * one, after its backoff (the owner's bounded safety attempt), within
11467
+ * `rateLimit`. Every other in-doubt outcome is never replayed.
11208
11468
  *
11209
11469
  * 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.
11470
+ * `resync`), never whether the gateway charged.
11213
11471
  *
11214
11472
  * @returns the charged replay, or the doubt to raise.
11215
11473
  */
11216
11474
  async resolveInDoubt(wallet, sent, doubt, send, budget) {
11475
+ if (hasPaymentIdentifier(sent.payload)) return this.replayInDoubt(wallet, sent, doubt, send, budget.attempt);
11217
11476
  if (!doubt.replayable) return doubt;
11218
11477
  const wait = this.coolDown(wallet, doubt.retryAfterMs ?? backoffDelay(budget.attempt));
11219
11478
  if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
@@ -11233,9 +11492,94 @@ var SolanaBatchPayer = class {
11233
11492
  budget.waited += wait;
11234
11493
  await sleep2(wait);
11235
11494
  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" };
11495
+ return replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
11496
+ }
11497
+ /**
11498
+ * Replay a payment in doubt that carries a payment identifier until the
11499
+ * gateway's answer settles it.
11500
+ *
11501
+ * A gateway that declares `payment-identifier` stores each paid response
11502
+ * under its identifier and answers a byte-identical replay of a completed
11503
+ * request with it, the original receipt included; while the original is
11504
+ * still running, it answers 409 with a `Retry-After`. So the identical
11505
+ * payment (the same `PAYMENT-SIGNATURE` header and the same body) is
11506
+ * replayed for as long as the last answer says a later one can succeed
11507
+ * ({@link replayMayResolve}), after {@link inDoubtWait} each time, as long
11508
+ * as the wait fits in `inDoubt.maxWaitMs`, counted from the first doubt:
11509
+ * every wait (as long as it really took) and every replay's round trip. A
11510
+ * replay already sent is not cut short. These replays do not count
11511
+ * against `rateLimit`; a 429 among them still puts the wallet in cooldown.
11512
+ *
11513
+ * Every answer that does not settle it is held back from the scheme
11514
+ * ({@link InDoubt.held}) and handed over only once no replay follows, so
11515
+ * the scheme rolls the payment back once, or takes the receipt that answer
11516
+ * carries. That is the last answer, unless an earlier one carried a
11517
+ * success receipt and the later ones do not: a success receipt is the
11518
+ * gateway's word that it charged, and handing the scheme a receipt-less
11519
+ * refusal of a replay instead would roll a real charge back and desync the
11520
+ * channel's cumulative.
11521
+ *
11522
+ * @returns the charged replay, or the doubt to raise.
11523
+ */
11524
+ async replayInDoubt(wallet, sent, first, send, attempt) {
11525
+ let doubt = first;
11526
+ let held = first.held;
11527
+ let heldSucceeded = held !== void 0 && receiptSucceeded(sent.http, held);
11528
+ let replays = 0;
11529
+ let spent = 0;
11530
+ const stop = async (note) => {
11531
+ if (!doubt.settled && held) {
11532
+ const answer = held;
11533
+ await this.settle(sent.http, sent.payload, (name) => answer.headers.get(name), answer.status);
11534
+ }
11535
+ const notes = [replays > 0 ? `${replays} replay${replays === 1 ? "" : "s"} did not resolve it` : "", note ?? ""];
11536
+ return {
11537
+ ...doubt,
11538
+ settled: doubt.settled || held !== void 0,
11539
+ held: void 0,
11540
+ firstReason: first.reason,
11541
+ detail: [doubt.detail, ...notes].filter(Boolean).join("; ")
11542
+ };
11543
+ };
11544
+ for (; ; ) {
11545
+ if (!replayMayResolve(doubt)) {
11546
+ return stop(doubt.held?.status === 409 ? "the gateway sent no Retry-After, so no replay can resolve it" : void 0);
11547
+ }
11548
+ const wait = inDoubtWait(doubt, replays + 1);
11549
+ const status2 = doubt.held?.status;
11550
+ if (status2 === 429) this.coolDown(wallet, (doubt.held && parseRetryAfter(doubt.held.headers.get("retry-after"))) ?? wait);
11551
+ if (spent + wait > this.inDoubtMaxWaitMs) {
11552
+ return stop(`batch.inDoubt.maxWaitMs (${this.inDoubtMaxWaitMs} ms) leaves no room for ${replays > 0 ? "another replay" : "a replay"}`);
11553
+ }
11554
+ await this.report({
11555
+ type: "backoff",
11556
+ reason: status2 === 429 ? "rate_limited" : "in_doubt",
11557
+ status: status2,
11558
+ errorReason: doubt.errorReason,
11559
+ retryAfterMs: wait,
11560
+ attempt,
11561
+ detail: `replay ${replays + 1} of the payment in doubt`
11562
+ });
11563
+ const sleptFrom = Date.now();
11564
+ await sleep2(wait);
11565
+ spent += Math.max(wait, Date.now() - sleptFrom);
11566
+ replays += 1;
11567
+ const startedAt = Date.now();
11568
+ const replayed = replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
11569
+ spent += Math.max(0, Date.now() - startedAt);
11570
+ if (replayed.kind === "charged") return replayed;
11571
+ doubt = replayed;
11572
+ if (doubt.held) {
11573
+ const succeeded = receiptSucceeded(sent.http, doubt.held);
11574
+ if (succeeded || !heldSucceeded) {
11575
+ held = doubt.held;
11576
+ heldSucceeded = succeeded;
11577
+ }
11578
+ } else if (doubt.settled) {
11579
+ held = void 0;
11580
+ heldSucceeded = false;
11581
+ }
11582
+ }
11239
11583
  }
11240
11584
  /**
11241
11585
  * Raise a payment in doubt: release the scheme's hold on it (the channel
@@ -11246,10 +11590,11 @@ var SolanaBatchPayer = class {
11246
11590
  async raiseUnresolved(wallet, sent, doubt, attempt) {
11247
11591
  if (!doubt.settled) await this.settle(sent.http, sent.payload, () => null, 0);
11248
11592
  if (sent.kind !== "authorization") {
11593
+ const reason = doubt.firstReason ?? doubt.reason;
11249
11594
  this.distrust(
11250
11595
  wallet,
11251
11596
  sent,
11252
- doubt.reason === "no_response" ? "deposit_unanswered" : doubt.reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
11597
+ reason === "no_response" ? "deposit_unanswered" : reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
11253
11598
  );
11254
11599
  }
11255
11600
  await this.report({
@@ -11336,8 +11681,8 @@ var SolanaBatchPayer = class {
11336
11681
  * here, and only for a first send.
11337
11682
  *
11338
11683
  * @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.
11684
+ * @param replay - this is a replay of a payment in doubt: only a 2xx with
11685
+ * a success receipt counts, everything else stays in doubt.
11341
11686
  */
11342
11687
  async sendOnce(wallet, sent, send, retry, replay) {
11343
11688
  if (retry) this.counters.retries += 1;
@@ -11351,6 +11696,7 @@ var SolanaBatchPayer = class {
11351
11696
  reason: replay ? "replay_unresolved" : "no_response",
11352
11697
  replayable: false,
11353
11698
  settled: false,
11699
+ unanswered: true,
11354
11700
  detail: `${replay ? "the replay" : "the payment"} got no answer (${err instanceof Error ? `${err.name}: ${err.message}` : String(err)})`,
11355
11701
  cause: err
11356
11702
  };
@@ -11375,6 +11721,7 @@ var SolanaBatchPayer = class {
11375
11721
  const getHeader = (name) => response.headers.get(name);
11376
11722
  const receipted = Boolean(getHeader("PAYMENT-RESPONSE") || getHeader("X-PAYMENT-RESPONSE"));
11377
11723
  const status2 = response.status;
11724
+ const hold = hasPaymentIdentifier(payload) && (!response.ok || replay && peekReceipt(http, getHeader)?.success !== true);
11378
11725
  if (status2 === 429 && !receipted) {
11379
11726
  return {
11380
11727
  kind: "in_doubt",
@@ -11384,12 +11731,27 @@ var SolanaBatchPayer = class {
11384
11731
  status: status2,
11385
11732
  retryAfterMs: parseRetryAfter(getHeader("retry-after")),
11386
11733
  errorReason: await rateLimitReason(response, void 0),
11387
- detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt"
11734
+ detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt",
11735
+ ...hold ? { held: response } : {}
11388
11736
  };
11389
11737
  }
11738
+ const outcome = await this.classifyAnswer(wallet, sent, response, replay, hold);
11739
+ if (!hold) return outcome;
11740
+ if (outcome.kind === "in_doubt") return { ...outcome, settled: false, held: response };
11741
+ await this.settle(http, payload, getHeader, status2);
11742
+ return outcome;
11743
+ }
11744
+ /**
11745
+ * {@link classify}, past a receipt-less 429. With `hold`, the answer is not
11746
+ * handed to the scheme: its receipt is only read.
11747
+ */
11748
+ async classifyAnswer(wallet, sent, response, replay, hold) {
11749
+ const { http, payload } = sent;
11750
+ const getHeader = (name) => response.headers.get(name);
11751
+ const status2 = response.status;
11390
11752
  const channelId = payloadChannelId(payload);
11391
11753
  const before = channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0;
11392
- const reconciled = await this.settle(http, payload, getHeader, status2);
11754
+ const reconciled = hold ? { settleResponse: peekReceipt(http, getHeader), ok: false } : await this.settle(http, payload, getHeader, status2);
11393
11755
  const receipt = reconciled.settleResponse;
11394
11756
  const definitive = receipt?.success === true && reconciled.ok && recordAdvanced(before, channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0, payload, receipt);
11395
11757
  if (!response.ok && !replay && sent.kind === "authorization" && cancelledReceipt(receipt)) {
@@ -11452,7 +11814,15 @@ var SolanaBatchPayer = class {
11452
11814
  cause: await afterPaymentError(response)
11453
11815
  };
11454
11816
  }
11455
- return { kind: "not_charged", reason: "payment_required", status: status2, errorReason: receipt?.errorReason, depositSafe: provesNothingBroadcast(receipt) };
11817
+ const refused = provesNothingBroadcast(receipt);
11818
+ return {
11819
+ kind: "not_charged",
11820
+ reason: "payment_required",
11821
+ status: status2,
11822
+ errorReason: receipt?.errorReason,
11823
+ depositSafe: refused,
11824
+ ...refused ? { refusedForMs: refusalCooldownMs(receipt?.errorReason, parseRetryAfter(getHeader("retry-after"))) } : {}
11825
+ };
11456
11826
  }
11457
11827
  const refusal = await batchRefusal(response);
11458
11828
  if (refusal) {
@@ -11483,7 +11853,7 @@ var SolanaBatchPayer = class {
11483
11853
  const http = (await this.build(wallet)).http;
11484
11854
  try {
11485
11855
  await this.prepareTopUp(wallet, paymentRequired);
11486
- return { http, payload: await http.createPaymentPayload(withoutClientSignedBatch(paymentRequired)) };
11856
+ return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(withoutClientSignedBatch(paymentRequired))) };
11487
11857
  } catch (err) {
11488
11858
  if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
11489
11859
  } finally {
@@ -11576,13 +11946,7 @@ var SolanaBatchPayer = class {
11576
11946
  console.error(
11577
11947
  `[@blockrun/llm] batch-settlement receipt not reconciled: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
11578
11948
  );
11579
- let settleResponse;
11580
- try {
11581
- settleResponse = http.getPaymentSettleResponse(getHeader);
11582
- } catch {
11583
- settleResponse = void 0;
11584
- }
11585
- return { settleResponse, ok: false };
11949
+ return { settleResponse: peekReceipt(http, getHeader), ok: false };
11586
11950
  }
11587
11951
  }
11588
11952
  /**
@@ -11965,9 +12329,9 @@ var SolanaLLMClient = class {
11965
12329
  };
11966
12330
  if (options?.temperature !== void 0) body.temperature = options.temperature;
11967
12331
  if (options?.topP !== void 0) body.top_p = options.topP;
11968
- if (options?.searchParameters !== void 0) body.search_parameters = options.searchParameters;
12332
+ if (options?.searchParameters !== void 0) body.search_parameters = toWireSearchParameters(options.searchParameters);
11969
12333
  else if (options?.search === true) body.search_parameters = { mode: "on" };
11970
- if (options?.tools !== void 0) body.tools = options.tools;
12334
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
11971
12335
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
11972
12336
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
11973
12337
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -11978,7 +12342,7 @@ var SolanaLLMClient = class {
11978
12342
  for (let index = 0; index < chain.length; index += 1) {
11979
12343
  const candidate = chain[index];
11980
12344
  try {
11981
- return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
12345
+ return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
11982
12346
  } catch (error) {
11983
12347
  lastError = error;
11984
12348
  if (!isTransientError(error) || index === chain.length - 1) throw error;
@@ -12028,6 +12392,8 @@ var SolanaLLMClient = class {
12028
12392
  {
12029
12393
  routingProfile: options?.routingProfile,
12030
12394
  requiresStructuredOutput: options?.responseFormat !== void 0,
12395
+ search: options?.search,
12396
+ searchParameters: options?.searchParameters,
12031
12397
  minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
12032
12398
  }
12033
12399
  );
@@ -12055,6 +12421,8 @@ var SolanaLLMClient = class {
12055
12421
  requiresStructuredOutput: options.responseFormat !== void 0,
12056
12422
  tools: options.tools,
12057
12423
  toolChoice: options.toolChoice,
12424
+ search: options.search,
12425
+ searchParameters: options.searchParameters,
12058
12426
  conversationChars,
12059
12427
  hasVision,
12060
12428
  minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
@@ -12070,6 +12438,7 @@ var SolanaLLMClient = class {
12070
12438
  toolChoice: options.toolChoice,
12071
12439
  responseFormat: options.responseFormat,
12072
12440
  stop: options.stop,
12441
+ timeout: options.timeout,
12073
12442
  // An explicit caller-supplied chain wins over the routed one.
12074
12443
  fallbackModels: options.fallbackModels ?? decision.fallbacks
12075
12444
  });
@@ -12207,7 +12576,7 @@ var SolanaLLMClient = class {
12207
12576
  *
12208
12577
  * @returns USDC balance as a float
12209
12578
  */
12210
- async getBalance() {
12579
+ async getBalance(options) {
12211
12580
  const usdc_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
12212
12581
  const address = await this.getWalletAddress();
12213
12582
  try {
@@ -12222,6 +12591,9 @@ var SolanaLLMClient = class {
12222
12591
  })
12223
12592
  });
12224
12593
  const data = await response.json();
12594
+ if (options?.strict && (!response.ok || data.error)) {
12595
+ throw new Error(`Solana RPC balance lookup failed: ${data.error?.message ?? `HTTP ${response.status}`}`);
12596
+ }
12225
12597
  const accounts = data.result?.value || [];
12226
12598
  if (!accounts.length) return 0;
12227
12599
  let total = 0;
@@ -12229,7 +12601,8 @@ var SolanaLLMClient = class {
12229
12601
  total += acct.account?.data?.parsed?.info?.tokenAmount?.uiAmount || 0;
12230
12602
  }
12231
12603
  return total;
12232
- } catch {
12604
+ } catch (error) {
12605
+ if (options?.strict) throw error;
12233
12606
  return 0;
12234
12607
  }
12235
12608
  }
@@ -12504,15 +12877,16 @@ var SolanaLLMClient = class {
12504
12877
  * `"paid-or-in-doubt"`, and so is any error with an API key once the billed
12505
12878
  * request was sent, except the account API's explicit 4xx refusal.
12506
12879
  */
12507
- async requestWithPayment(endpoint, body) {
12880
+ async requestWithPayment(endpoint, body, timeoutMs) {
12508
12881
  const url = `${this.apiUrl}${endpoint}`;
12882
+ const json = JSON.stringify(body);
12509
12883
  const closesAtStart = await this.batchPayer?.closeFence();
12510
12884
  for (let staleRetries = 0; ; ) {
12511
12885
  const response = await this.sendUnpaid(url, {
12512
12886
  method: "POST",
12513
12887
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
12514
- body: JSON.stringify(body)
12515
- });
12888
+ body: json
12889
+ }, timeoutMs);
12516
12890
  if (response.status === 402) {
12517
12891
  try {
12518
12892
  let paymentRequired;
@@ -12528,16 +12902,16 @@ var SolanaLLMClient = class {
12528
12902
  (paymentHeaders) => this.fetchWithTimeout(url, {
12529
12903
  method: "POST",
12530
12904
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
12531
- body: JSON.stringify(body)
12532
- }),
12905
+ body: json
12906
+ }, timeoutMs),
12533
12907
  // A fresh, unpaid challenge after a 429 wait: the request itself,
12534
12908
  // sent again without a payment. Every error it throws is unpaid.
12535
12909
  async () => {
12536
12910
  const challenge = await this.sendUnpaid(url, {
12537
12911
  method: "POST",
12538
12912
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
12539
- body: JSON.stringify(body)
12540
- });
12913
+ body: json
12914
+ }, timeoutMs);
12541
12915
  if (challenge.status === 402) {
12542
12916
  try {
12543
12917
  return { kind: "challenge", paymentRequired: await this.readPaymentRequired(challenge) };
@@ -12552,13 +12926,14 @@ var SolanaLLMClient = class {
12552
12926
  );
12553
12927
  if (batch.kind === "paid") {
12554
12928
  this.recordSettlement(batch.chargedUsd);
12555
- return await this.readPaidJson(batch.response);
12929
+ const paid = await this.readPaidJson(batch.response);
12930
+ return withCost(paid, batch.response, batch.chargedUsd, "batch");
12556
12931
  }
12557
- if (batch.kind === "served") return await this.readUnpaidJson(batch.response);
12932
+ if (batch.kind === "served") return this.withUnpaidCost(await this.readUnpaidJson(batch.response));
12558
12933
  if (batch.kind === "failed") throw batch.error;
12559
12934
  exactRequired = batch.paymentRequired ?? paymentRequired;
12560
12935
  }
12561
- return await this.handlePaymentAndRetry(url, body, exactRequired, staleRetries > 0);
12936
+ return await this.handlePaymentAndRetry(url, json, exactRequired, staleRetries > 0, timeoutMs);
12562
12937
  } catch (error) {
12563
12938
  if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
12564
12939
  await waitForStaleRetry(staleRetries++);
@@ -12566,9 +12941,21 @@ var SolanaLLMClient = class {
12566
12941
  }
12567
12942
  }
12568
12943
  if (!response.ok) throw await unpaidApiError(response);
12569
- return await this.readUnpaidJson(response);
12944
+ return this.withUnpaidCost(await this.readUnpaidJson(response));
12570
12945
  }
12571
12946
  }
12947
+ /**
12948
+ * Cost of a chat response served without a payment: 0 with a wallet (a free
12949
+ * model). With an API key the call is billed to the account, so costUsd stays
12950
+ * undefined (see the dashboard).
12951
+ */
12952
+ withUnpaidCost(served) {
12953
+ if (!this.apiAuth) {
12954
+ served.costUsd = 0;
12955
+ served.settlement = { scheme: "free", quotedUsd: 0 };
12956
+ }
12957
+ return served;
12958
+ }
12572
12959
  /**
12573
12960
  * Read the JSON body of a 2xx served without a payment: the free tier, or
12574
12961
  * with an API key, where this very request was the billed one.
@@ -12585,9 +12972,9 @@ var SolanaLLMClient = class {
12585
12972
  * {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
12586
12973
  * {@link accountErrorDisposition} (the request itself is billed).
12587
12974
  */
12588
- async sendUnpaid(url, init) {
12975
+ async sendUnpaid(url, init, timeoutMs) {
12589
12976
  try {
12590
- return await this.fetchWithTimeout(url, init);
12977
+ return await this.fetchWithTimeout(url, init, timeoutMs);
12591
12978
  } catch (error) {
12592
12979
  throw withDisposition(error, this.apiAuth ? accountErrorDisposition(error) : "unpaid");
12593
12980
  }
@@ -12856,7 +13243,7 @@ var SolanaLLMClient = class {
12856
13243
  if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
12857
13244
  const fromAddress = await this.getWalletAddress();
12858
13245
  const secretKey = await solanaKeyToBytes(this.privateKey);
12859
- const extensions = paymentRequired.extensions;
13246
+ const extensions = withPaymentIdentifier(paymentRequired).extensions;
12860
13247
  const paymentPayload = await createSolanaPaymentPayload(
12861
13248
  secretKey,
12862
13249
  fromAddress,
@@ -12879,7 +13266,7 @@ var SolanaLLMClient = class {
12879
13266
  );
12880
13267
  return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
12881
13268
  }
12882
- async handlePaymentAndRetry(url, body, paymentRequired, forceFreshBlockhash = false) {
13269
+ async handlePaymentAndRetry(url, json, paymentRequired, forceFreshBlockhash = false, timeoutMs) {
12883
13270
  let signed;
12884
13271
  try {
12885
13272
  signed = await this.signExactPayment(url, paymentRequired, forceFreshBlockhash, `${this.apiUrl}/v1/chat/completions`);
@@ -12896,14 +13283,15 @@ var SolanaLLMClient = class {
12896
13283
  "User-Agent": USER_AGENT,
12897
13284
  "PAYMENT-SIGNATURE": paymentPayload
12898
13285
  },
12899
- body: JSON.stringify(body)
12900
- });
13286
+ body: json
13287
+ }, timeoutMs);
12901
13288
  } catch (error) {
12902
13289
  throw withDisposition(error, "paid-or-in-doubt");
12903
13290
  }
12904
13291
  await this.assertPaid(retryResponse);
12905
- this.recordSettlement(costUsd);
12906
- return this.readPaidJson(retryResponse);
13292
+ if (!settlementSkipped(retryResponse)) this.recordSettlement(costUsd);
13293
+ const paid = await this.readPaidJson(retryResponse);
13294
+ return withCost(paid, retryResponse, costUsd, "exact");
12907
13295
  }
12908
13296
  async requestWithPaymentRaw(endpoint, body, media = {}) {
12909
13297
  const url = `${this.apiUrl}${endpoint}`;
@@ -13618,6 +14006,7 @@ export {
13618
14006
  BatchPaymentUnresolvedError,
13619
14007
  BlockrunClient,
13620
14008
  BlockrunError,
14009
+ DEFAULT_BATCH_IN_DOUBT,
13621
14010
  DEFAULT_BATCH_RATE_LIMIT,
13622
14011
  EVM_NETWORKS,
13623
14012
  ImageClient,
@@ -13681,6 +14070,7 @@ export {
13681
14070
  loadWallet,
13682
14071
  logCost,
13683
14072
  parsePaymentRequired,
14073
+ readSettlement,
13684
14074
  retryDisposition,
13685
14075
  saveSolanaWallet,
13686
14076
  saveToCache,
@@ -13695,6 +14085,8 @@ export {
13695
14085
  solanaKeyToBytes,
13696
14086
  solanaPublicKey,
13697
14087
  status,
14088
+ toWireSearchParameters,
14089
+ toWireTools,
13698
14090
  validateMaxTokens,
13699
14091
  validateModel,
13700
14092
  validateTemperature,