@blockrun/llm 3.20.1 → 3.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -16
- package/dist/index.cjs +821 -191
- package/dist/index.d.cts +276 -38
- package/dist/index.d.ts +276 -38
- package/dist/index.js +814 -188
- package/package.json +1 -1
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,122 @@ 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 withCost(body, response, bookedUsd, scheme) {
|
|
557
|
+
body.costUsd = bookedUsd;
|
|
558
|
+
body.settlement = readSettlement(response, bookedUsd, scheme).settlement;
|
|
559
|
+
return body;
|
|
560
|
+
}
|
|
561
|
+
|
|
442
562
|
// src/client.ts
|
|
443
563
|
var import_accounts2 = require("viem/accounts");
|
|
444
564
|
|
|
@@ -4225,6 +4345,11 @@ function errSummary(err) {
|
|
|
4225
4345
|
}
|
|
4226
4346
|
return String(err).slice(0, 100);
|
|
4227
4347
|
}
|
|
4348
|
+
function requestsSearch(options) {
|
|
4349
|
+
if (options.tools?.some((tool) => tool.type === "x_search" || tool.type === "web_search")) return true;
|
|
4350
|
+
if (options.searchParameters !== void 0) return options.searchParameters.mode !== "off";
|
|
4351
|
+
return options.search === true;
|
|
4352
|
+
}
|
|
4228
4353
|
function routingProfileForModel(model) {
|
|
4229
4354
|
return AUTO_ROUTING_PROFILES[model.toLowerCase()];
|
|
4230
4355
|
}
|
|
@@ -4252,7 +4377,7 @@ function routingText(messages) {
|
|
|
4252
4377
|
};
|
|
4253
4378
|
}
|
|
4254
4379
|
function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, options = {}) {
|
|
4255
|
-
const tools = options.tools ?? [];
|
|
4380
|
+
const tools = (options.tools ?? []).filter((tool) => tool.type === "function");
|
|
4256
4381
|
const requiresTools = options.toolChoice === "none" ? false : options.toolChoice === "required" || typeof options.toolChoice === "object" ? true : void 0;
|
|
4257
4382
|
const decision = route(prompt, systemPrompt, maxOutputTokens, {
|
|
4258
4383
|
config: DEFAULT_ROUTING_CONFIG,
|
|
@@ -4272,6 +4397,15 @@ function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, o
|
|
|
4272
4397
|
const resolved = !id.startsWith("free/") ? id : modelPricing.has(`nvidia/${id.slice(5)}`) ? `nvidia/${id.slice(5)}` : null;
|
|
4273
4398
|
if (resolved && !callable.includes(resolved)) callable.push(resolved);
|
|
4274
4399
|
}
|
|
4400
|
+
if (requestsSearch(options)) {
|
|
4401
|
+
const searchable = callable.filter((id) => id.startsWith("xai/"));
|
|
4402
|
+
if (searchable.length === 0) {
|
|
4403
|
+
throw new Error(
|
|
4404
|
+
`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.`
|
|
4405
|
+
);
|
|
4406
|
+
}
|
|
4407
|
+
callable.splice(0, callable.length, ...searchable);
|
|
4408
|
+
}
|
|
4275
4409
|
const estimatedInputTokens = Math.ceil(
|
|
4276
4410
|
Math.max(options.conversationChars ?? 0, `${systemPrompt ?? ""} ${prompt}`.length) / 4
|
|
4277
4411
|
);
|
|
@@ -4690,7 +4824,7 @@ function getCostSummary() {
|
|
|
4690
4824
|
}
|
|
4691
4825
|
|
|
4692
4826
|
// src/version.ts
|
|
4693
|
-
var SDK_VERSION = "3.
|
|
4827
|
+
var SDK_VERSION = "3.22.0";
|
|
4694
4828
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4695
4829
|
|
|
4696
4830
|
// src/client.ts
|
|
@@ -4836,7 +4970,9 @@ var LLMClient = class _LLMClient {
|
|
|
4836
4970
|
options?.maxOutputTokens ?? options?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
4837
4971
|
{
|
|
4838
4972
|
routingProfile: options?.routingProfile,
|
|
4839
|
-
requiresStructuredOutput: options?.responseFormat !== void 0
|
|
4973
|
+
requiresStructuredOutput: options?.responseFormat !== void 0,
|
|
4974
|
+
search: options?.search,
|
|
4975
|
+
searchParameters: options?.searchParameters
|
|
4840
4976
|
}
|
|
4841
4977
|
);
|
|
4842
4978
|
}
|
|
@@ -4903,6 +5039,8 @@ var LLMClient = class _LLMClient {
|
|
|
4903
5039
|
requiresStructuredOutput: options.responseFormat !== void 0,
|
|
4904
5040
|
tools: options.tools,
|
|
4905
5041
|
toolChoice: options.toolChoice,
|
|
5042
|
+
search: options.search,
|
|
5043
|
+
searchParameters: options.searchParameters,
|
|
4906
5044
|
conversationChars,
|
|
4907
5045
|
hasVision
|
|
4908
5046
|
}
|
|
@@ -4917,6 +5055,7 @@ var LLMClient = class _LLMClient {
|
|
|
4917
5055
|
toolChoice: options.toolChoice,
|
|
4918
5056
|
responseFormat: options.responseFormat,
|
|
4919
5057
|
stop: options.stop,
|
|
5058
|
+
timeout: options.timeout,
|
|
4920
5059
|
// An explicit caller-supplied chain wins over the routed one.
|
|
4921
5060
|
fallbackModels: options.fallbackModels ?? decision.fallbacks
|
|
4922
5061
|
});
|
|
@@ -4993,11 +5132,11 @@ var LLMClient = class _LLMClient {
|
|
|
4993
5132
|
if (options?.temperature !== void 0) body.temperature = options.temperature;
|
|
4994
5133
|
if (options?.topP !== void 0) body.top_p = options.topP;
|
|
4995
5134
|
if (options?.searchParameters !== void 0) {
|
|
4996
|
-
body.search_parameters = options.searchParameters;
|
|
5135
|
+
body.search_parameters = toWireSearchParameters(options.searchParameters);
|
|
4997
5136
|
} else if (options?.search === true) {
|
|
4998
5137
|
body.search_parameters = { mode: "on" };
|
|
4999
5138
|
}
|
|
5000
|
-
if (options?.tools !== void 0) body.tools = options.tools;
|
|
5139
|
+
if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
|
|
5001
5140
|
if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
|
|
5002
5141
|
if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
|
|
5003
5142
|
if (options?.stop !== void 0) body.stop = options.stop;
|
|
@@ -5008,7 +5147,7 @@ var LLMClient = class _LLMClient {
|
|
|
5008
5147
|
for (let i = 0; i < chain.length; i++) {
|
|
5009
5148
|
const candidate = chain[i];
|
|
5010
5149
|
try {
|
|
5011
|
-
return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
|
|
5150
|
+
return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
|
|
5012
5151
|
} catch (err) {
|
|
5013
5152
|
lastErr = err;
|
|
5014
5153
|
const next = chain[i + 1];
|
|
@@ -5079,19 +5218,19 @@ var LLMClient = class _LLMClient {
|
|
|
5079
5218
|
* is `"paid-or-in-doubt"`. With an API key the request itself is billed, so
|
|
5080
5219
|
* only the account API's explicit 4xx refusal is `"unpaid"`.
|
|
5081
5220
|
*/
|
|
5082
|
-
async requestWithPayment(endpoint, body) {
|
|
5221
|
+
async requestWithPayment(endpoint, body, timeoutMs) {
|
|
5083
5222
|
const url = `${this.apiUrl}${endpoint}`;
|
|
5084
5223
|
const unpaid = {
|
|
5085
5224
|
method: "POST",
|
|
5086
5225
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
5087
5226
|
body: JSON.stringify(body)
|
|
5088
5227
|
};
|
|
5089
|
-
const response = await this.sendUnpaid(url, unpaid);
|
|
5228
|
+
const response = await this.sendUnpaid(url, unpaid, timeoutMs);
|
|
5090
5229
|
if (response.status === 502 || response.status === 503) {
|
|
5091
5230
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
5092
|
-
const retryResp = await this.sendUnpaid(url, unpaid);
|
|
5231
|
+
const retryResp = await this.sendUnpaid(url, unpaid, timeoutMs);
|
|
5093
5232
|
if (retryResp.status !== 502 && retryResp.status !== 503) {
|
|
5094
|
-
if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp);
|
|
5233
|
+
if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp, timeoutMs);
|
|
5095
5234
|
if (!retryResp.ok) {
|
|
5096
5235
|
let errorBody;
|
|
5097
5236
|
try {
|
|
@@ -5108,7 +5247,7 @@ var LLMClient = class _LLMClient {
|
|
|
5108
5247
|
}
|
|
5109
5248
|
}
|
|
5110
5249
|
if (response.status === 402) {
|
|
5111
|
-
return this.handlePaymentAndRetry(url, body, response);
|
|
5250
|
+
return this.handlePaymentAndRetry(url, body, response, timeoutMs);
|
|
5112
5251
|
}
|
|
5113
5252
|
if (!response.ok) {
|
|
5114
5253
|
let errorBody;
|
|
@@ -5133,9 +5272,9 @@ var LLMClient = class _LLMClient {
|
|
|
5133
5272
|
* {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
|
|
5134
5273
|
* {@link accountErrorDisposition} (the request itself is billed).
|
|
5135
5274
|
*/
|
|
5136
|
-
async sendUnpaid(url, init) {
|
|
5275
|
+
async sendUnpaid(url, init, timeoutMs) {
|
|
5137
5276
|
try {
|
|
5138
|
-
return await this.fetchWithTimeout(url, init);
|
|
5277
|
+
return await this.fetchWithTimeout(url, init, timeoutMs);
|
|
5139
5278
|
} catch (err) {
|
|
5140
5279
|
throw withDisposition(err, this.apiAuth ? accountErrorDisposition(err) : "unpaid");
|
|
5141
5280
|
}
|
|
@@ -5143,13 +5282,22 @@ var LLMClient = class _LLMClient {
|
|
|
5143
5282
|
/**
|
|
5144
5283
|
* Parse a response to a request that carried no payment: the free tier,
|
|
5145
5284
|
* or, with an API key, the billed request itself.
|
|
5285
|
+
*
|
|
5286
|
+
* With a wallet nothing was paid, so `costUsd` is 0 (a free model). With an
|
|
5287
|
+
* API key the call is billed to the account and `costUsd` stays undefined.
|
|
5146
5288
|
*/
|
|
5147
5289
|
async parseUnpaidChatResponse(response) {
|
|
5290
|
+
let served;
|
|
5148
5291
|
try {
|
|
5149
|
-
|
|
5292
|
+
served = await this.parseChatResponse(response);
|
|
5150
5293
|
} catch (err) {
|
|
5151
5294
|
throw withDisposition(err, this.apiAuth ? "paid-or-in-doubt" : "unpaid");
|
|
5152
5295
|
}
|
|
5296
|
+
if (!this.apiAuth) {
|
|
5297
|
+
served.costUsd = 0;
|
|
5298
|
+
served.settlement = { scheme: "free", quotedUsd: 0 };
|
|
5299
|
+
}
|
|
5300
|
+
return served;
|
|
5153
5301
|
}
|
|
5154
5302
|
/**
|
|
5155
5303
|
* Handle 402 response: parse requirements, sign payment, retry.
|
|
@@ -5157,7 +5305,7 @@ var LLMClient = class _LLMClient {
|
|
|
5157
5305
|
* Anything thrown before the signed payment is sent is `"unpaid"`; anything
|
|
5158
5306
|
* after it, `"paid-or-in-doubt"` (see {@link RetryDisposition}).
|
|
5159
5307
|
*/
|
|
5160
|
-
async handlePaymentAndRetry(url, body, response) {
|
|
5308
|
+
async handlePaymentAndRetry(url, body, response, timeoutMs) {
|
|
5161
5309
|
let signed;
|
|
5162
5310
|
try {
|
|
5163
5311
|
signed = await this.signFrom402(url, response);
|
|
@@ -5165,7 +5313,7 @@ var LLMClient = class _LLMClient {
|
|
|
5165
5313
|
throw withDisposition(err, "unpaid");
|
|
5166
5314
|
}
|
|
5167
5315
|
try {
|
|
5168
|
-
return await this.sendPaid(url, body, signed.paymentPayload, signed.details);
|
|
5316
|
+
return await this.sendPaid(url, body, signed.paymentPayload, signed.details, timeoutMs);
|
|
5169
5317
|
} catch (err) {
|
|
5170
5318
|
throw withDisposition(err, "paid-or-in-doubt");
|
|
5171
5319
|
}
|
|
@@ -5210,7 +5358,7 @@ var LLMClient = class _LLMClient {
|
|
|
5210
5358
|
return { paymentPayload, details };
|
|
5211
5359
|
}
|
|
5212
5360
|
/** Send the signed payment and read the answer. The payment may settle from here on. */
|
|
5213
|
-
async sendPaid(url, body, paymentPayload, details) {
|
|
5361
|
+
async sendPaid(url, body, paymentPayload, details, timeoutMs) {
|
|
5214
5362
|
const retryResponse = await this.fetchWithTimeout(url, {
|
|
5215
5363
|
method: "POST",
|
|
5216
5364
|
headers: {
|
|
@@ -5219,7 +5367,7 @@ var LLMClient = class _LLMClient {
|
|
|
5219
5367
|
"PAYMENT-SIGNATURE": paymentPayload
|
|
5220
5368
|
},
|
|
5221
5369
|
body: JSON.stringify(body)
|
|
5222
|
-
});
|
|
5370
|
+
}, timeoutMs);
|
|
5223
5371
|
if (retryResponse.status === 502 || retryResponse.status === 503) {
|
|
5224
5372
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
5225
5373
|
const retryResp2 = await this.fetchWithTimeout(url, {
|
|
@@ -5230,7 +5378,7 @@ var LLMClient = class _LLMClient {
|
|
|
5230
5378
|
"PAYMENT-SIGNATURE": paymentPayload
|
|
5231
5379
|
},
|
|
5232
5380
|
body: JSON.stringify(body)
|
|
5233
|
-
});
|
|
5381
|
+
}, timeoutMs);
|
|
5234
5382
|
if (retryResp2.status !== 502 && retryResp2.status !== 503) {
|
|
5235
5383
|
if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
|
|
5236
5384
|
if (!retryResp2.ok) {
|
|
@@ -5246,7 +5394,7 @@ var LLMClient = class _LLMClient {
|
|
|
5246
5394
|
this.sessionCalls += 1;
|
|
5247
5395
|
this.sessionTotalUsd += costUsd2;
|
|
5248
5396
|
this.recordCost(url, costUsd2, { body, network: details.network });
|
|
5249
|
-
return this.parseChatResponse(retryResp2);
|
|
5397
|
+
return withCost(await this.parseChatResponse(retryResp2), retryResp2, costUsd2, "exact");
|
|
5250
5398
|
}
|
|
5251
5399
|
}
|
|
5252
5400
|
if (retryResponse.status === 402) {
|
|
@@ -5269,7 +5417,7 @@ var LLMClient = class _LLMClient {
|
|
|
5269
5417
|
this.sessionCalls += 1;
|
|
5270
5418
|
this.sessionTotalUsd += costUsd;
|
|
5271
5419
|
this.recordCost(url, costUsd, { body, network: details.network });
|
|
5272
|
-
return this.parseChatResponse(retryResponse);
|
|
5420
|
+
return withCost(await this.parseChatResponse(retryResponse), retryResponse, costUsd, "exact");
|
|
5273
5421
|
}
|
|
5274
5422
|
/**
|
|
5275
5423
|
* Sign a payment header and return the PAYMENT-SIGNATURE value.
|
|
@@ -5339,7 +5487,7 @@ var LLMClient = class _LLMClient {
|
|
|
5339
5487
|
};
|
|
5340
5488
|
if (options?.temperature !== void 0) body.temperature = options.temperature;
|
|
5341
5489
|
if (options?.topP !== void 0) body.top_p = options.topP;
|
|
5342
|
-
if (options?.tools !== void 0) body.tools = options.tools;
|
|
5490
|
+
if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
|
|
5343
5491
|
if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
|
|
5344
5492
|
if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
|
|
5345
5493
|
if (options?.stop !== void 0) body.stop = options.stop;
|
|
@@ -5737,11 +5885,11 @@ var LLMClient = class _LLMClient {
|
|
|
5737
5885
|
return retryResponse.json();
|
|
5738
5886
|
}
|
|
5739
5887
|
/**
|
|
5740
|
-
* Fetch with timeout
|
|
5888
|
+
* Fetch with timeout: `timeoutMs` (a per-call override), else the client's `timeout`.
|
|
5741
5889
|
*/
|
|
5742
|
-
async fetchWithTimeout(url, options) {
|
|
5890
|
+
async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
|
|
5743
5891
|
const controller = new AbortController();
|
|
5744
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
5892
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
5745
5893
|
try {
|
|
5746
5894
|
const response = await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, {
|
|
5747
5895
|
...options,
|
|
@@ -9455,11 +9603,11 @@ var WALLET_DIR = path4.join(os3.homedir(), ".blockrun");
|
|
|
9455
9603
|
var SOLANA_WALLET_FILE = path4.join(WALLET_DIR, ".solana-session");
|
|
9456
9604
|
async function createSolanaWallet() {
|
|
9457
9605
|
const { Keypair } = await loadSolanaWeb3("Solana wallet operations");
|
|
9458
|
-
const
|
|
9606
|
+
const bs583 = await import("bs58");
|
|
9459
9607
|
const keypair = Keypair.generate();
|
|
9460
9608
|
return {
|
|
9461
9609
|
address: keypair.publicKey.toBase58(),
|
|
9462
|
-
privateKey: (
|
|
9610
|
+
privateKey: (bs583.default ?? bs583).encode(keypair.secretKey)
|
|
9463
9611
|
};
|
|
9464
9612
|
}
|
|
9465
9613
|
async function solanaKeyToBytes(privateKey) {
|
|
@@ -9497,8 +9645,8 @@ async function solanaKeyToBytes(privateKey) {
|
|
|
9497
9645
|
return bytes;
|
|
9498
9646
|
}
|
|
9499
9647
|
try {
|
|
9500
|
-
const
|
|
9501
|
-
const bytes = (
|
|
9648
|
+
const bs583 = await import("bs58");
|
|
9649
|
+
const bytes = (bs583.default ?? bs583).decode(key);
|
|
9502
9650
|
if (bytes.length !== 64) {
|
|
9503
9651
|
throw new Error(`Invalid Solana key length: expected 64 bytes, got ${bytes.length}`);
|
|
9504
9652
|
}
|
|
@@ -9677,15 +9825,56 @@ async function getOrCreateSolanaWallet() {
|
|
|
9677
9825
|
return { address, privateKey, isNew: true };
|
|
9678
9826
|
}
|
|
9679
9827
|
|
|
9828
|
+
// src/poll-ownership.ts
|
|
9829
|
+
var import_crypto = require("crypto");
|
|
9830
|
+
var import_bs58 = __toESM(require("bs58"), 1);
|
|
9831
|
+
var POLL_AUTH_HEADERS = {
|
|
9832
|
+
wallet: "x-poll-wallet",
|
|
9833
|
+
timestamp: "x-poll-timestamp",
|
|
9834
|
+
signature: "x-poll-signature"
|
|
9835
|
+
};
|
|
9836
|
+
function pollAuthMessage(kind, jobId, timestampS) {
|
|
9837
|
+
return `blockrun-poll:v1:${kind}:${jobId}:${timestampS}`;
|
|
9838
|
+
}
|
|
9839
|
+
function pollJobIdFromUrl(pollUrl) {
|
|
9840
|
+
const path7 = new URL(pollUrl, "https://placeholder.invalid").pathname.replace(/\/+$/, "");
|
|
9841
|
+
const segment = path7.slice(path7.lastIndexOf("/") + 1);
|
|
9842
|
+
if (!segment) throw new Error(`poll_url has no job id segment: ${pollUrl}`);
|
|
9843
|
+
return decodeURIComponent(segment);
|
|
9844
|
+
}
|
|
9845
|
+
var ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
9846
|
+
function signPollOwnership(secretKey, kind, jobId, nowMs = Date.now()) {
|
|
9847
|
+
if (secretKey.length !== 64 && secretKey.length !== 32) {
|
|
9848
|
+
throw new Error(`Solana secret key must be 64 bytes (or a 32-byte seed), got ${secretKey.length}`);
|
|
9849
|
+
}
|
|
9850
|
+
const seed = Buffer.from(secretKey.subarray(0, 32));
|
|
9851
|
+
const privateKey = (0, import_crypto.createPrivateKey)({
|
|
9852
|
+
key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]),
|
|
9853
|
+
format: "der",
|
|
9854
|
+
type: "pkcs8"
|
|
9855
|
+
});
|
|
9856
|
+
const jwk = (0, import_crypto.createPublicKey)(privateKey).export({ format: "jwk" });
|
|
9857
|
+
if (!jwk.x) throw new Error("Could not derive the Ed25519 public key");
|
|
9858
|
+
const publicKey = Buffer.from(jwk.x, "base64url");
|
|
9859
|
+
const timestampS = Math.floor(nowMs / 1e3);
|
|
9860
|
+
const signature = (0, import_crypto.sign)(null, Buffer.from(pollAuthMessage(kind, jobId, timestampS), "utf8"), privateKey);
|
|
9861
|
+
return {
|
|
9862
|
+
[POLL_AUTH_HEADERS.wallet]: import_bs58.default.encode(publicKey),
|
|
9863
|
+
[POLL_AUTH_HEADERS.timestamp]: String(timestampS),
|
|
9864
|
+
[POLL_AUTH_HEADERS.signature]: import_bs58.default.encode(signature)
|
|
9865
|
+
};
|
|
9866
|
+
}
|
|
9867
|
+
|
|
9680
9868
|
// src/solana-batch.ts
|
|
9681
9869
|
var import_async_hooks = require("async_hooks");
|
|
9682
|
-
var
|
|
9870
|
+
var import_crypto2 = require("crypto");
|
|
9683
9871
|
var fs6 = __toESM(require("fs"), 1);
|
|
9684
9872
|
var path5 = __toESM(require("path"), 1);
|
|
9685
|
-
var
|
|
9873
|
+
var import_bs582 = __toESM(require("bs58"), 1);
|
|
9686
9874
|
var BATCH_SCHEME = "batch-settlement";
|
|
9687
9875
|
var BLOCKRUN_SOL_OPERATOR = "5YKPQUFjw5WQqhSUkEGKNNfYYVqnRRNbpYyL71qQ1vm3";
|
|
9688
9876
|
var DEFAULT_BATCH_RATE_LIMIT = { maxAttempts: 3, maxWaitMs: 6e4 };
|
|
9877
|
+
var DEFAULT_BATCH_IN_DOUBT = { maxWaitMs: 3e5 };
|
|
9689
9878
|
var BatchPaymentUnresolvedError = class extends PaymentError {
|
|
9690
9879
|
/** The {@link SolanaBatchEvent.reason} of the `unresolved` event reported with it. */
|
|
9691
9880
|
reason;
|
|
@@ -9940,7 +10129,7 @@ var FileIntentJournal = class _FileIntentJournal {
|
|
|
9940
10129
|
fsyncDirectory(dir);
|
|
9941
10130
|
return;
|
|
9942
10131
|
}
|
|
9943
|
-
const tmp = `${this.file}.${process.pid}.${(0,
|
|
10132
|
+
const tmp = `${this.file}.${process.pid}.${(0, import_crypto2.randomUUID)()}.tmp`;
|
|
9944
10133
|
const fd = fs6.openSync(tmp, "w", 384);
|
|
9945
10134
|
try {
|
|
9946
10135
|
fs6.writeSync(fd, JSON.stringify({ version: 1, intents }, null, 2));
|
|
@@ -10066,7 +10255,7 @@ function lockChannelFile(file) {
|
|
|
10066
10255
|
if (registry2.locks.has(file)) return void 0;
|
|
10067
10256
|
const lock = `${file}.lock`;
|
|
10068
10257
|
fs6.mkdirSync(path5.dirname(lock), { recursive: true, mode: 448 });
|
|
10069
|
-
const token = (0,
|
|
10258
|
+
const token = (0, import_crypto2.randomUUID)();
|
|
10070
10259
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
10071
10260
|
const tmp = `${lock}.${process.pid}.${token}.tmp`;
|
|
10072
10261
|
try {
|
|
@@ -10242,7 +10431,7 @@ function decodeChannelAccount(owner, data) {
|
|
|
10242
10431
|
return void 0;
|
|
10243
10432
|
}
|
|
10244
10433
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
10245
|
-
const key = (offset) =>
|
|
10434
|
+
const key = (offset) => import_bs582.default.encode(data.subarray(offset, offset + 32));
|
|
10246
10435
|
return {
|
|
10247
10436
|
deposit: view.getBigUint64(CHANNEL.deposit, true),
|
|
10248
10437
|
settled: view.getBigUint64(CHANNEL.settled, true),
|
|
@@ -10362,6 +10551,46 @@ function withoutClientSignedBatch(paymentRequired) {
|
|
|
10362
10551
|
const accepts = paymentRequired.accepts ?? [];
|
|
10363
10552
|
return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
|
|
10364
10553
|
}
|
|
10554
|
+
var PAYMENT_IDENTIFIER = "payment-identifier";
|
|
10555
|
+
function withPaymentIdentifier(paymentRequired) {
|
|
10556
|
+
const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
|
|
10557
|
+
if (typeof declared?.info?.required !== "boolean") return paymentRequired;
|
|
10558
|
+
const id = `pay_${(0, import_crypto2.randomUUID)().replace(/-/g, "")}`;
|
|
10559
|
+
return {
|
|
10560
|
+
...paymentRequired,
|
|
10561
|
+
extensions: { ...paymentRequired.extensions, [PAYMENT_IDENTIFIER]: { ...declared, info: { ...declared.info, id } } }
|
|
10562
|
+
};
|
|
10563
|
+
}
|
|
10564
|
+
function peekReceipt(http, getHeader) {
|
|
10565
|
+
try {
|
|
10566
|
+
return http.getPaymentSettleResponse(getHeader);
|
|
10567
|
+
} catch {
|
|
10568
|
+
return void 0;
|
|
10569
|
+
}
|
|
10570
|
+
}
|
|
10571
|
+
function receiptSucceeded(http, answer) {
|
|
10572
|
+
return peekReceipt(http, (name) => answer.headers.get(name))?.success === true;
|
|
10573
|
+
}
|
|
10574
|
+
function hasPaymentIdentifier(payload) {
|
|
10575
|
+
const info = payload.extensions?.[PAYMENT_IDENTIFIER]?.info;
|
|
10576
|
+
return typeof info?.id === "string";
|
|
10577
|
+
}
|
|
10578
|
+
var IN_DOUBT_MIN_WAIT_MS = 1e3;
|
|
10579
|
+
var IN_DOUBT_MAX_WAIT_MS = 3e4;
|
|
10580
|
+
function replayMayResolve(doubt) {
|
|
10581
|
+
if (doubt.unanswered) return true;
|
|
10582
|
+
const answer = doubt.held;
|
|
10583
|
+
if (!answer) return false;
|
|
10584
|
+
return answer.status >= 500 || answer.status === 429 || answer.status === 409 && Boolean(answer.headers.get("retry-after")?.trim());
|
|
10585
|
+
}
|
|
10586
|
+
function inDoubtWait(doubt, replay) {
|
|
10587
|
+
const asked = doubt.held ? parseRetryAfter(doubt.held.headers.get("retry-after")) : void 0;
|
|
10588
|
+
return Math.min(IN_DOUBT_MAX_WAIT_MS, Math.max(IN_DOUBT_MIN_WAIT_MS, asked ?? backoffDelay(replay)));
|
|
10589
|
+
}
|
|
10590
|
+
function replayDoubt(outcome) {
|
|
10591
|
+
if (outcome.kind === "charged" || outcome.kind === "in_doubt") return outcome;
|
|
10592
|
+
return { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
|
|
10593
|
+
}
|
|
10365
10594
|
function trustedBatchAccepts(paymentRequired, operators) {
|
|
10366
10595
|
return (paymentRequired.accepts ?? []).filter((candidate) => {
|
|
10367
10596
|
const extra = candidate.extra;
|
|
@@ -10371,9 +10600,6 @@ function trustedBatchAccepts(paymentRequired, operators) {
|
|
|
10371
10600
|
function trustedBatchAccept(paymentRequired, operators) {
|
|
10372
10601
|
return trustedBatchAccepts(paymentRequired, operators)[0];
|
|
10373
10602
|
}
|
|
10374
|
-
function atomicToUsd(amount) {
|
|
10375
|
-
return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
|
|
10376
|
-
}
|
|
10377
10603
|
function chargedUsd(getReceipt) {
|
|
10378
10604
|
let receipt;
|
|
10379
10605
|
try {
|
|
@@ -10381,7 +10607,7 @@ function chargedUsd(getReceipt) {
|
|
|
10381
10607
|
} catch {
|
|
10382
10608
|
return 0;
|
|
10383
10609
|
}
|
|
10384
|
-
return
|
|
10610
|
+
return receiptChargedUsd(receipt) ?? 0;
|
|
10385
10611
|
}
|
|
10386
10612
|
function rebuiltCharge(settled, payload) {
|
|
10387
10613
|
if (settled?.success !== true) return 0n;
|
|
@@ -10501,13 +10727,42 @@ function parseRetryAfter(value, now = Date.now()) {
|
|
|
10501
10727
|
}
|
|
10502
10728
|
return Number.isFinite(ms) && ms <= MAX_RETRY_AFTER_MS ? ms : void 0;
|
|
10503
10729
|
}
|
|
10504
|
-
var
|
|
10730
|
+
var NOT_BROADCAST_REFUSALS = /* @__PURE__ */ new Set([
|
|
10731
|
+
// 429
|
|
10505
10732
|
"batch_account_channel_capacity_exhausted",
|
|
10506
10733
|
"batch_channel_capacity_exhausted",
|
|
10507
|
-
"batch_deposit_rate_limited"
|
|
10734
|
+
"batch_deposit_rate_limited",
|
|
10735
|
+
"batch_deposit_ip_rate_limited",
|
|
10736
|
+
// 503
|
|
10737
|
+
"batch_signer_funding_headroom_exhausted",
|
|
10738
|
+
"batch_new_channels_disabled",
|
|
10739
|
+
"batch_public_new_channels_disabled",
|
|
10740
|
+
// 403
|
|
10741
|
+
"batch_account_admission_paused",
|
|
10742
|
+
"batch_channel_owner_mismatch",
|
|
10743
|
+
"batch_fee_payer_unavailable",
|
|
10744
|
+
"batch_fee_payer_not_dedicated",
|
|
10745
|
+
"batch_fee_payer_lane_not_authorized",
|
|
10746
|
+
"batch_transaction_fee_payer_mismatch",
|
|
10747
|
+
// 400
|
|
10748
|
+
"batch_initial_deposit_out_of_range",
|
|
10749
|
+
"batch_deposit_asset_not_supported",
|
|
10750
|
+
"batch_withdraw_delay_out_of_range"
|
|
10508
10751
|
]);
|
|
10509
10752
|
function provesNothingBroadcast(receipt) {
|
|
10510
|
-
return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" &&
|
|
10753
|
+
return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_REFUSALS.has(receipt.errorReason);
|
|
10754
|
+
}
|
|
10755
|
+
var CONFIG_REFUSALS = /* @__PURE__ */ new Set([
|
|
10756
|
+
"batch_initial_deposit_out_of_range",
|
|
10757
|
+
"batch_deposit_asset_not_supported",
|
|
10758
|
+
"batch_withdraw_delay_out_of_range"
|
|
10759
|
+
]);
|
|
10760
|
+
var REFUSAL_COOLDOWN_MS = 6e4;
|
|
10761
|
+
var CONFIG_REFUSAL_COOLDOWN_MS = 10 * 6e4;
|
|
10762
|
+
var REFUSAL_COOLDOWN_MAX_MS = 15 * 6e4;
|
|
10763
|
+
function refusalCooldownMs(errorReason, retryAfterMs2) {
|
|
10764
|
+
if (retryAfterMs2 !== void 0) return Math.min(retryAfterMs2, REFUSAL_COOLDOWN_MAX_MS);
|
|
10765
|
+
return errorReason !== void 0 && CONFIG_REFUSALS.has(errorReason) ? CONFIG_REFUSAL_COOLDOWN_MS : REFUSAL_COOLDOWN_MS;
|
|
10511
10766
|
}
|
|
10512
10767
|
function backoffDelay(attempt) {
|
|
10513
10768
|
const base = 1e3 * 2 ** Math.max(0, attempt - 1);
|
|
@@ -10640,7 +10895,7 @@ function intentTarget(intent) {
|
|
|
10640
10895
|
var SolanaBatchPayer = class {
|
|
10641
10896
|
constructor(init) {
|
|
10642
10897
|
this.init = init;
|
|
10643
|
-
const { operators, rateLimit } = init.options;
|
|
10898
|
+
const { operators, rateLimit, inDoubt } = init.options;
|
|
10644
10899
|
if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
|
|
10645
10900
|
throw new Error("batch.operators must list at least one base58 operator public key");
|
|
10646
10901
|
}
|
|
@@ -10652,11 +10907,16 @@ var SolanaBatchPayer = class {
|
|
|
10652
10907
|
if (!Number.isFinite(this.maxWaitMs) || this.maxWaitMs < 0) {
|
|
10653
10908
|
throw new Error("batch.rateLimit.maxWaitMs must be a finite number of ms >= 0");
|
|
10654
10909
|
}
|
|
10910
|
+
this.inDoubtMaxWaitMs = inDoubt?.maxWaitMs ?? DEFAULT_BATCH_IN_DOUBT.maxWaitMs;
|
|
10911
|
+
if (!Number.isFinite(this.inDoubtMaxWaitMs) || this.inDoubtMaxWaitMs < 0) {
|
|
10912
|
+
throw new Error("batch.inDoubt.maxWaitMs must be a finite number of ms >= 0");
|
|
10913
|
+
}
|
|
10655
10914
|
}
|
|
10656
10915
|
init;
|
|
10657
10916
|
state;
|
|
10658
10917
|
maxAttempts;
|
|
10659
10918
|
maxWaitMs;
|
|
10919
|
+
inDoubtMaxWaitMs;
|
|
10660
10920
|
counters = {
|
|
10661
10921
|
fallbacks: 0,
|
|
10662
10922
|
fallbacksByReason: {},
|
|
@@ -11025,14 +11285,19 @@ var SolanaBatchPayer = class {
|
|
|
11025
11285
|
* the call's result (`served`, nothing paid); one that fails otherwise
|
|
11026
11286
|
* is the call's error (`failed`, `"unpaid"`), never an exact payment
|
|
11027
11287
|
* against the stale challenge;
|
|
11028
|
-
* - in doubt: ends only with a definitive success receipt from
|
|
11029
|
-
*
|
|
11030
|
-
*
|
|
11031
|
-
*
|
|
11032
|
-
*
|
|
11288
|
+
* - in doubt: ends only with a definitive success receipt from a replay
|
|
11289
|
+
* of the identical payment ({@link resolveInDoubt}: the one replay a
|
|
11290
|
+
* receipt-less 429 is allowed, or the replays of a payment carrying a
|
|
11291
|
+
* payment identifier while the gateway says one can succeed, within
|
|
11292
|
+
* `inDoubt.maxWaitMs`), or a thrown {@link BatchPaymentUnresolvedError}.
|
|
11293
|
+
* No new authorization, no new deposit, no exact payment and no fallback
|
|
11294
|
+
* model is ever paid for a call in doubt.
|
|
11033
11295
|
*
|
|
11034
11296
|
* The wallet stays in cooldown after a 429, so its other calls wait
|
|
11035
|
-
* instead of sending opens of their own.
|
|
11297
|
+
* instead of sending opens of their own. After a deposit the facilitator
|
|
11298
|
+
* refused before broadcasting it (a 402 with a failed `batch_*` admission
|
|
11299
|
+
* receipt), its calls do not wait: they pay exact at once
|
|
11300
|
+
* (`admission_refused`) until the refusal cooldown ends.
|
|
11036
11301
|
*
|
|
11037
11302
|
* @param closesAtStart - {@link closeFence}, taken before the call's first
|
|
11038
11303
|
* (unpaid) request. If a close completed since, the call pays exact
|
|
@@ -11096,6 +11361,15 @@ var SolanaBatchPayer = class {
|
|
|
11096
11361
|
try {
|
|
11097
11362
|
for (; ; ) {
|
|
11098
11363
|
const now = Date.now();
|
|
11364
|
+
if ((wallet.refusedUntil ?? 0) > now) {
|
|
11365
|
+
return fallback({
|
|
11366
|
+
reason: "admission_refused",
|
|
11367
|
+
errorReason: wallet.refusedFor,
|
|
11368
|
+
retryAfterMs: (wallet.refusedUntil ?? 0) - now,
|
|
11369
|
+
attempt: budget.attempt,
|
|
11370
|
+
detail: `the facilitator refused this wallet's last batch deposit (${wallet.refusedFor ?? "unknown"}); batch is skipped until the refusal cooldown ends`
|
|
11371
|
+
});
|
|
11372
|
+
}
|
|
11099
11373
|
if (wallet.cooldownUntil > now && wallet.cooldownUntil !== waitedUntil) {
|
|
11100
11374
|
const wait = wallet.cooldownUntil - now;
|
|
11101
11375
|
if (budget.waited + wait > this.maxWaitMs) {
|
|
@@ -11185,12 +11459,23 @@ var SolanaBatchPayer = class {
|
|
|
11185
11459
|
if (built.kind !== "authorization") {
|
|
11186
11460
|
if (outcome.depositSafe) this.forgetIntent(wallet, built.key);
|
|
11187
11461
|
else this.distrust(wallet, built, "deposit_refused");
|
|
11462
|
+
if (outcome.refusedForMs !== void 0) {
|
|
11463
|
+
const until = Date.now() + outcome.refusedForMs;
|
|
11464
|
+
if (until > (wallet.refusedUntil ?? 0)) {
|
|
11465
|
+
wallet.refusedUntil = until;
|
|
11466
|
+
wallet.refusedFor = outcome.errorReason;
|
|
11467
|
+
}
|
|
11468
|
+
}
|
|
11188
11469
|
}
|
|
11189
11470
|
sent = void 0;
|
|
11190
11471
|
release();
|
|
11191
11472
|
if (!outcome.rateLimited) {
|
|
11192
|
-
const { kind: _kind, rateLimited: _limited, depositSafe: _safe, ...event } = outcome;
|
|
11193
|
-
return fallback({
|
|
11473
|
+
const { kind: _kind, rateLimited: _limited, depositSafe: _safe, refusedForMs, ...event } = outcome;
|
|
11474
|
+
return fallback({
|
|
11475
|
+
...event,
|
|
11476
|
+
...refusedForMs !== void 0 && built.kind !== "authorization" ? { retryAfterMs: refusedForMs } : {},
|
|
11477
|
+
attempt: budget.attempt
|
|
11478
|
+
});
|
|
11194
11479
|
}
|
|
11195
11480
|
const wait = this.coolDown(wallet, outcome.rateLimited.retryAfterMs ?? backoffDelay(budget.attempt));
|
|
11196
11481
|
if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
|
|
@@ -11236,7 +11521,7 @@ var SolanaBatchPayer = class {
|
|
|
11236
11521
|
if (built.kind !== "authorization" && !(built.key && wallet.resyncs.has(built.key))) this.forgetIntent(wallet, built.key);
|
|
11237
11522
|
sent = void 0;
|
|
11238
11523
|
release();
|
|
11239
|
-
await this.report({ type: "recovered", reason: "rate_limited", attempt: budget.attempt });
|
|
11524
|
+
await this.report({ type: "recovered", reason: outcome.status === 429 ? "rate_limited" : "in_doubt", attempt: budget.attempt });
|
|
11240
11525
|
return { kind: "paid", response: resolved.response, chargedUsd: resolved.chargedUsd };
|
|
11241
11526
|
}
|
|
11242
11527
|
return await this.raiseUnresolved(wallet, built, resolved, budget.attempt);
|
|
@@ -11285,21 +11570,24 @@ var SolanaBatchPayer = class {
|
|
|
11285
11570
|
/**
|
|
11286
11571
|
* Try to end a payment in doubt with the gateway's own word.
|
|
11287
11572
|
*
|
|
11288
|
-
* The one source of evidence this SDK accepts
|
|
11289
|
-
*
|
|
11290
|
-
*
|
|
11291
|
-
*
|
|
11292
|
-
*
|
|
11293
|
-
*
|
|
11573
|
+
* The one source of evidence this SDK accepts is a definitive success
|
|
11574
|
+
* receipt on a byte-identical replay of the payment. Any other answer to a
|
|
11575
|
+
* replay (a 402 or `duplicate_settlement`, which mean the original reached
|
|
11576
|
+
* the gateway; a 409; another 429; a 5xx; a timeout) leaves it in doubt.
|
|
11577
|
+
*
|
|
11578
|
+
* - A payment carrying a payment identifier is replayed for as long as
|
|
11579
|
+
* the gateway says a later replay can succeed ({@link replayInDoubt}).
|
|
11580
|
+
* - Any other payment gets a replay only after a 429 without a receipt:
|
|
11581
|
+
* one, after its backoff (the owner's bounded safety attempt), within
|
|
11582
|
+
* `rateLimit`. Every other in-doubt outcome is never replayed.
|
|
11294
11583
|
*
|
|
11295
11584
|
* Chain state is not evidence here: it can settle a deposit (see
|
|
11296
|
-
* `resync`), never whether the gateway charged.
|
|
11297
|
-
* receipt on every response, a request-status endpoint with a fence)
|
|
11298
|
-
* plugs in here, before the caller raises.
|
|
11585
|
+
* `resync`), never whether the gateway charged.
|
|
11299
11586
|
*
|
|
11300
11587
|
* @returns the charged replay, or the doubt to raise.
|
|
11301
11588
|
*/
|
|
11302
11589
|
async resolveInDoubt(wallet, sent, doubt, send, budget) {
|
|
11590
|
+
if (hasPaymentIdentifier(sent.payload)) return this.replayInDoubt(wallet, sent, doubt, send, budget.attempt);
|
|
11303
11591
|
if (!doubt.replayable) return doubt;
|
|
11304
11592
|
const wait = this.coolDown(wallet, doubt.retryAfterMs ?? backoffDelay(budget.attempt));
|
|
11305
11593
|
if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
|
|
@@ -11319,9 +11607,94 @@ var SolanaBatchPayer = class {
|
|
|
11319
11607
|
budget.waited += wait;
|
|
11320
11608
|
await sleep2(wait);
|
|
11321
11609
|
budget.attempt += 1;
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11610
|
+
return replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
|
|
11611
|
+
}
|
|
11612
|
+
/**
|
|
11613
|
+
* Replay a payment in doubt that carries a payment identifier until the
|
|
11614
|
+
* gateway's answer settles it.
|
|
11615
|
+
*
|
|
11616
|
+
* A gateway that declares `payment-identifier` stores each paid response
|
|
11617
|
+
* under its identifier and answers a byte-identical replay of a completed
|
|
11618
|
+
* request with it, the original receipt included; while the original is
|
|
11619
|
+
* still running, it answers 409 with a `Retry-After`. So the identical
|
|
11620
|
+
* payment (the same `PAYMENT-SIGNATURE` header and the same body) is
|
|
11621
|
+
* replayed for as long as the last answer says a later one can succeed
|
|
11622
|
+
* ({@link replayMayResolve}), after {@link inDoubtWait} each time, as long
|
|
11623
|
+
* as the wait fits in `inDoubt.maxWaitMs`, counted from the first doubt:
|
|
11624
|
+
* every wait (as long as it really took) and every replay's round trip. A
|
|
11625
|
+
* replay already sent is not cut short. These replays do not count
|
|
11626
|
+
* against `rateLimit`; a 429 among them still puts the wallet in cooldown.
|
|
11627
|
+
*
|
|
11628
|
+
* Every answer that does not settle it is held back from the scheme
|
|
11629
|
+
* ({@link InDoubt.held}) and handed over only once no replay follows, so
|
|
11630
|
+
* the scheme rolls the payment back once, or takes the receipt that answer
|
|
11631
|
+
* carries. That is the last answer, unless an earlier one carried a
|
|
11632
|
+
* success receipt and the later ones do not: a success receipt is the
|
|
11633
|
+
* gateway's word that it charged, and handing the scheme a receipt-less
|
|
11634
|
+
* refusal of a replay instead would roll a real charge back and desync the
|
|
11635
|
+
* channel's cumulative.
|
|
11636
|
+
*
|
|
11637
|
+
* @returns the charged replay, or the doubt to raise.
|
|
11638
|
+
*/
|
|
11639
|
+
async replayInDoubt(wallet, sent, first, send, attempt) {
|
|
11640
|
+
let doubt = first;
|
|
11641
|
+
let held = first.held;
|
|
11642
|
+
let heldSucceeded = held !== void 0 && receiptSucceeded(sent.http, held);
|
|
11643
|
+
let replays = 0;
|
|
11644
|
+
let spent = 0;
|
|
11645
|
+
const stop = async (note) => {
|
|
11646
|
+
if (!doubt.settled && held) {
|
|
11647
|
+
const answer = held;
|
|
11648
|
+
await this.settle(sent.http, sent.payload, (name) => answer.headers.get(name), answer.status);
|
|
11649
|
+
}
|
|
11650
|
+
const notes = [replays > 0 ? `${replays} replay${replays === 1 ? "" : "s"} did not resolve it` : "", note ?? ""];
|
|
11651
|
+
return {
|
|
11652
|
+
...doubt,
|
|
11653
|
+
settled: doubt.settled || held !== void 0,
|
|
11654
|
+
held: void 0,
|
|
11655
|
+
firstReason: first.reason,
|
|
11656
|
+
detail: [doubt.detail, ...notes].filter(Boolean).join("; ")
|
|
11657
|
+
};
|
|
11658
|
+
};
|
|
11659
|
+
for (; ; ) {
|
|
11660
|
+
if (!replayMayResolve(doubt)) {
|
|
11661
|
+
return stop(doubt.held?.status === 409 ? "the gateway sent no Retry-After, so no replay can resolve it" : void 0);
|
|
11662
|
+
}
|
|
11663
|
+
const wait = inDoubtWait(doubt, replays + 1);
|
|
11664
|
+
const status2 = doubt.held?.status;
|
|
11665
|
+
if (status2 === 429) this.coolDown(wallet, (doubt.held && parseRetryAfter(doubt.held.headers.get("retry-after"))) ?? wait);
|
|
11666
|
+
if (spent + wait > this.inDoubtMaxWaitMs) {
|
|
11667
|
+
return stop(`batch.inDoubt.maxWaitMs (${this.inDoubtMaxWaitMs} ms) leaves no room for ${replays > 0 ? "another replay" : "a replay"}`);
|
|
11668
|
+
}
|
|
11669
|
+
await this.report({
|
|
11670
|
+
type: "backoff",
|
|
11671
|
+
reason: status2 === 429 ? "rate_limited" : "in_doubt",
|
|
11672
|
+
status: status2,
|
|
11673
|
+
errorReason: doubt.errorReason,
|
|
11674
|
+
retryAfterMs: wait,
|
|
11675
|
+
attempt,
|
|
11676
|
+
detail: `replay ${replays + 1} of the payment in doubt`
|
|
11677
|
+
});
|
|
11678
|
+
const sleptFrom = Date.now();
|
|
11679
|
+
await sleep2(wait);
|
|
11680
|
+
spent += Math.max(wait, Date.now() - sleptFrom);
|
|
11681
|
+
replays += 1;
|
|
11682
|
+
const startedAt = Date.now();
|
|
11683
|
+
const replayed = replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
|
|
11684
|
+
spent += Math.max(0, Date.now() - startedAt);
|
|
11685
|
+
if (replayed.kind === "charged") return replayed;
|
|
11686
|
+
doubt = replayed;
|
|
11687
|
+
if (doubt.held) {
|
|
11688
|
+
const succeeded = receiptSucceeded(sent.http, doubt.held);
|
|
11689
|
+
if (succeeded || !heldSucceeded) {
|
|
11690
|
+
held = doubt.held;
|
|
11691
|
+
heldSucceeded = succeeded;
|
|
11692
|
+
}
|
|
11693
|
+
} else if (doubt.settled) {
|
|
11694
|
+
held = void 0;
|
|
11695
|
+
heldSucceeded = false;
|
|
11696
|
+
}
|
|
11697
|
+
}
|
|
11325
11698
|
}
|
|
11326
11699
|
/**
|
|
11327
11700
|
* Raise a payment in doubt: release the scheme's hold on it (the channel
|
|
@@ -11332,10 +11705,11 @@ var SolanaBatchPayer = class {
|
|
|
11332
11705
|
async raiseUnresolved(wallet, sent, doubt, attempt) {
|
|
11333
11706
|
if (!doubt.settled) await this.settle(sent.http, sent.payload, () => null, 0);
|
|
11334
11707
|
if (sent.kind !== "authorization") {
|
|
11708
|
+
const reason = doubt.firstReason ?? doubt.reason;
|
|
11335
11709
|
this.distrust(
|
|
11336
11710
|
wallet,
|
|
11337
11711
|
sent,
|
|
11338
|
-
|
|
11712
|
+
reason === "no_response" ? "deposit_unanswered" : reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
|
|
11339
11713
|
);
|
|
11340
11714
|
}
|
|
11341
11715
|
await this.report({
|
|
@@ -11422,8 +11796,8 @@ var SolanaBatchPayer = class {
|
|
|
11422
11796
|
* here, and only for a first send.
|
|
11423
11797
|
*
|
|
11424
11798
|
* @param retry - count this send in `retries` (it follows this call's own 429).
|
|
11425
|
-
* @param replay - this is
|
|
11426
|
-
*
|
|
11799
|
+
* @param replay - this is a replay of a payment in doubt: only a 2xx with
|
|
11800
|
+
* a success receipt counts, everything else stays in doubt.
|
|
11427
11801
|
*/
|
|
11428
11802
|
async sendOnce(wallet, sent, send, retry, replay) {
|
|
11429
11803
|
if (retry) this.counters.retries += 1;
|
|
@@ -11437,6 +11811,7 @@ var SolanaBatchPayer = class {
|
|
|
11437
11811
|
reason: replay ? "replay_unresolved" : "no_response",
|
|
11438
11812
|
replayable: false,
|
|
11439
11813
|
settled: false,
|
|
11814
|
+
unanswered: true,
|
|
11440
11815
|
detail: `${replay ? "the replay" : "the payment"} got no answer (${err instanceof Error ? `${err.name}: ${err.message}` : String(err)})`,
|
|
11441
11816
|
cause: err
|
|
11442
11817
|
};
|
|
@@ -11461,6 +11836,7 @@ var SolanaBatchPayer = class {
|
|
|
11461
11836
|
const getHeader = (name) => response.headers.get(name);
|
|
11462
11837
|
const receipted = Boolean(getHeader("PAYMENT-RESPONSE") || getHeader("X-PAYMENT-RESPONSE"));
|
|
11463
11838
|
const status2 = response.status;
|
|
11839
|
+
const hold = hasPaymentIdentifier(payload) && (!response.ok || replay && peekReceipt(http, getHeader)?.success !== true);
|
|
11464
11840
|
if (status2 === 429 && !receipted) {
|
|
11465
11841
|
return {
|
|
11466
11842
|
kind: "in_doubt",
|
|
@@ -11470,12 +11846,27 @@ var SolanaBatchPayer = class {
|
|
|
11470
11846
|
status: status2,
|
|
11471
11847
|
retryAfterMs: parseRetryAfter(getHeader("retry-after")),
|
|
11472
11848
|
errorReason: await rateLimitReason(response, void 0),
|
|
11473
|
-
detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt"
|
|
11849
|
+
detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt",
|
|
11850
|
+
...hold ? { held: response } : {}
|
|
11474
11851
|
};
|
|
11475
11852
|
}
|
|
11853
|
+
const outcome = await this.classifyAnswer(wallet, sent, response, replay, hold);
|
|
11854
|
+
if (!hold) return outcome;
|
|
11855
|
+
if (outcome.kind === "in_doubt") return { ...outcome, settled: false, held: response };
|
|
11856
|
+
await this.settle(http, payload, getHeader, status2);
|
|
11857
|
+
return outcome;
|
|
11858
|
+
}
|
|
11859
|
+
/**
|
|
11860
|
+
* {@link classify}, past a receipt-less 429. With `hold`, the answer is not
|
|
11861
|
+
* handed to the scheme: its receipt is only read.
|
|
11862
|
+
*/
|
|
11863
|
+
async classifyAnswer(wallet, sent, response, replay, hold) {
|
|
11864
|
+
const { http, payload } = sent;
|
|
11865
|
+
const getHeader = (name) => response.headers.get(name);
|
|
11866
|
+
const status2 = response.status;
|
|
11476
11867
|
const channelId = payloadChannelId(payload);
|
|
11477
11868
|
const before = channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0;
|
|
11478
|
-
const reconciled = await this.settle(http, payload, getHeader, status2);
|
|
11869
|
+
const reconciled = hold ? { settleResponse: peekReceipt(http, getHeader), ok: false } : await this.settle(http, payload, getHeader, status2);
|
|
11479
11870
|
const receipt = reconciled.settleResponse;
|
|
11480
11871
|
const definitive = receipt?.success === true && reconciled.ok && recordAdvanced(before, channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0, payload, receipt);
|
|
11481
11872
|
if (!response.ok && !replay && sent.kind === "authorization" && cancelledReceipt(receipt)) {
|
|
@@ -11538,7 +11929,15 @@ var SolanaBatchPayer = class {
|
|
|
11538
11929
|
cause: await afterPaymentError(response)
|
|
11539
11930
|
};
|
|
11540
11931
|
}
|
|
11541
|
-
|
|
11932
|
+
const refused = provesNothingBroadcast(receipt);
|
|
11933
|
+
return {
|
|
11934
|
+
kind: "not_charged",
|
|
11935
|
+
reason: "payment_required",
|
|
11936
|
+
status: status2,
|
|
11937
|
+
errorReason: receipt?.errorReason,
|
|
11938
|
+
depositSafe: refused,
|
|
11939
|
+
...refused ? { refusedForMs: refusalCooldownMs(receipt?.errorReason, parseRetryAfter(getHeader("retry-after"))) } : {}
|
|
11940
|
+
};
|
|
11542
11941
|
}
|
|
11543
11942
|
const refusal = await batchRefusal(response);
|
|
11544
11943
|
if (refusal) {
|
|
@@ -11569,7 +11968,7 @@ var SolanaBatchPayer = class {
|
|
|
11569
11968
|
const http = (await this.build(wallet)).http;
|
|
11570
11969
|
try {
|
|
11571
11970
|
await this.prepareTopUp(wallet, paymentRequired);
|
|
11572
|
-
return { http, payload: await http.createPaymentPayload(withoutClientSignedBatch(paymentRequired)) };
|
|
11971
|
+
return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(withoutClientSignedBatch(paymentRequired))) };
|
|
11573
11972
|
} catch (err) {
|
|
11574
11973
|
if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
|
|
11575
11974
|
} finally {
|
|
@@ -11662,13 +12061,7 @@ var SolanaBatchPayer = class {
|
|
|
11662
12061
|
console.error(
|
|
11663
12062
|
`[@blockrun/llm] batch-settlement receipt not reconciled: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
|
|
11664
12063
|
);
|
|
11665
|
-
|
|
11666
|
-
try {
|
|
11667
|
-
settleResponse = http.getPaymentSettleResponse(getHeader);
|
|
11668
|
-
} catch {
|
|
11669
|
-
settleResponse = void 0;
|
|
11670
|
-
}
|
|
11671
|
-
return { settleResponse, ok: false };
|
|
12064
|
+
return { settleResponse: peekReceipt(http, getHeader), ok: false };
|
|
11672
12065
|
}
|
|
11673
12066
|
}
|
|
11674
12067
|
/**
|
|
@@ -11802,6 +12195,39 @@ var DEFAULT_MAX_TOKENS2 = 1024;
|
|
|
11802
12195
|
var DEFAULT_TIMEOUT14 = 6e4;
|
|
11803
12196
|
var STALE_BLOCKHASH_RETRY_BACKOFFS_MS = [500, 2e3];
|
|
11804
12197
|
var MAX_PAYMENT_FAILURE_BYTES = 64 * 1024;
|
|
12198
|
+
var DEFAULT_VIDEO_MODEL = "xai/grok-imagine-video";
|
|
12199
|
+
var DEFAULT_MUSIC_MODEL = "minimax/music-2.5+";
|
|
12200
|
+
var DEFAULT_VIDEO_BUDGET_MS = 9e5;
|
|
12201
|
+
var DEFAULT_MUSIC_BUDGET_MS = 3e5;
|
|
12202
|
+
var VIDEO_POLL_INTERVAL_MS = 5e3;
|
|
12203
|
+
var MAX_PAID_POLL_REFUSALS = 4;
|
|
12204
|
+
var MAX_RETRY_AFTER_MS2 = 12e4;
|
|
12205
|
+
var MEDIA_SUBMIT_TIMEOUT_MS = 12e4;
|
|
12206
|
+
function sleep3(ms) {
|
|
12207
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
12208
|
+
}
|
|
12209
|
+
async function readJsonOrEmpty(response) {
|
|
12210
|
+
try {
|
|
12211
|
+
const parsed = await response.json();
|
|
12212
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
12213
|
+
} catch {
|
|
12214
|
+
return {};
|
|
12215
|
+
}
|
|
12216
|
+
}
|
|
12217
|
+
function isPendingPollStatus(status2) {
|
|
12218
|
+
return status2 === 202 || status2 === 503 || status2 === 504;
|
|
12219
|
+
}
|
|
12220
|
+
function retryAfterMs(response) {
|
|
12221
|
+
const raw = response.headers.get("retry-after");
|
|
12222
|
+
if (!raw || !/^\d+$/.test(raw.trim())) return void 0;
|
|
12223
|
+
return Math.min(Number(raw.trim()) * 1e3, MAX_RETRY_AFTER_MS2);
|
|
12224
|
+
}
|
|
12225
|
+
function pollKindFor(url) {
|
|
12226
|
+
const path7 = new URL(url).pathname;
|
|
12227
|
+
if (path7.includes("/v1/videos")) return "video";
|
|
12228
|
+
if (path7.includes("/v1/audio/")) return "audio";
|
|
12229
|
+
return "image";
|
|
12230
|
+
}
|
|
11805
12231
|
var SafeStaleBlockhashError = class extends PaymentError {
|
|
11806
12232
|
constructor() {
|
|
11807
12233
|
super("Payment verification used an expired Solana blockhash; retrying with a fresh quote.");
|
|
@@ -12018,9 +12444,9 @@ var SolanaLLMClient = class {
|
|
|
12018
12444
|
};
|
|
12019
12445
|
if (options?.temperature !== void 0) body.temperature = options.temperature;
|
|
12020
12446
|
if (options?.topP !== void 0) body.top_p = options.topP;
|
|
12021
|
-
if (options?.searchParameters !== void 0) body.search_parameters = options.searchParameters;
|
|
12447
|
+
if (options?.searchParameters !== void 0) body.search_parameters = toWireSearchParameters(options.searchParameters);
|
|
12022
12448
|
else if (options?.search === true) body.search_parameters = { mode: "on" };
|
|
12023
|
-
if (options?.tools !== void 0) body.tools = options.tools;
|
|
12449
|
+
if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
|
|
12024
12450
|
if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
|
|
12025
12451
|
if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
|
|
12026
12452
|
if (options?.stop !== void 0) body.stop = options.stop;
|
|
@@ -12031,7 +12457,7 @@ var SolanaLLMClient = class {
|
|
|
12031
12457
|
for (let index = 0; index < chain.length; index += 1) {
|
|
12032
12458
|
const candidate = chain[index];
|
|
12033
12459
|
try {
|
|
12034
|
-
return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
|
|
12460
|
+
return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
|
|
12035
12461
|
} catch (error) {
|
|
12036
12462
|
lastError = error;
|
|
12037
12463
|
if (!isTransientError(error) || index === chain.length - 1) throw error;
|
|
@@ -12081,6 +12507,8 @@ var SolanaLLMClient = class {
|
|
|
12081
12507
|
{
|
|
12082
12508
|
routingProfile: options?.routingProfile,
|
|
12083
12509
|
requiresStructuredOutput: options?.responseFormat !== void 0,
|
|
12510
|
+
search: options?.search,
|
|
12511
|
+
searchParameters: options?.searchParameters,
|
|
12084
12512
|
minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
|
|
12085
12513
|
}
|
|
12086
12514
|
);
|
|
@@ -12108,6 +12536,8 @@ var SolanaLLMClient = class {
|
|
|
12108
12536
|
requiresStructuredOutput: options.responseFormat !== void 0,
|
|
12109
12537
|
tools: options.tools,
|
|
12110
12538
|
toolChoice: options.toolChoice,
|
|
12539
|
+
search: options.search,
|
|
12540
|
+
searchParameters: options.searchParameters,
|
|
12111
12541
|
conversationChars,
|
|
12112
12542
|
hasVision,
|
|
12113
12543
|
minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
|
|
@@ -12123,6 +12553,7 @@ var SolanaLLMClient = class {
|
|
|
12123
12553
|
toolChoice: options.toolChoice,
|
|
12124
12554
|
responseFormat: options.responseFormat,
|
|
12125
12555
|
stop: options.stop,
|
|
12556
|
+
timeout: options.timeout,
|
|
12126
12557
|
// An explicit caller-supplied chain wins over the routed one.
|
|
12127
12558
|
fallbackModels: options.fallbackModels ?? decision.fallbacks
|
|
12128
12559
|
});
|
|
@@ -12260,7 +12691,7 @@ var SolanaLLMClient = class {
|
|
|
12260
12691
|
*
|
|
12261
12692
|
* @returns USDC balance as a float
|
|
12262
12693
|
*/
|
|
12263
|
-
async getBalance() {
|
|
12694
|
+
async getBalance(options) {
|
|
12264
12695
|
const usdc_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
12265
12696
|
const address = await this.getWalletAddress();
|
|
12266
12697
|
try {
|
|
@@ -12275,6 +12706,9 @@ var SolanaLLMClient = class {
|
|
|
12275
12706
|
})
|
|
12276
12707
|
});
|
|
12277
12708
|
const data = await response.json();
|
|
12709
|
+
if (options?.strict && (!response.ok || data.error)) {
|
|
12710
|
+
throw new Error(`Solana RPC balance lookup failed: ${data.error?.message ?? `HTTP ${response.status}`}`);
|
|
12711
|
+
}
|
|
12278
12712
|
const accounts = data.result?.value || [];
|
|
12279
12713
|
if (!accounts.length) return 0;
|
|
12280
12714
|
let total = 0;
|
|
@@ -12282,7 +12716,8 @@ var SolanaLLMClient = class {
|
|
|
12282
12716
|
total += acct.account?.data?.parsed?.info?.tokenAmount?.uiAmount || 0;
|
|
12283
12717
|
}
|
|
12284
12718
|
return total;
|
|
12285
|
-
} catch {
|
|
12719
|
+
} catch (error) {
|
|
12720
|
+
if (options?.strict) throw error;
|
|
12286
12721
|
return 0;
|
|
12287
12722
|
}
|
|
12288
12723
|
}
|
|
@@ -12311,6 +12746,54 @@ var SolanaLLMClient = class {
|
|
|
12311
12746
|
const data = await this.requestWithPaymentRaw("/v1/images/image2image", body);
|
|
12312
12747
|
return data;
|
|
12313
12748
|
}
|
|
12749
|
+
/**
|
|
12750
|
+
* Generate a short video on Solana.
|
|
12751
|
+
*
|
|
12752
|
+
* The POST is verified, not charged. The job is then polled with a wallet
|
|
12753
|
+
* signature (no payment) until it finishes, and ONE payment is signed for
|
|
12754
|
+
* the finished video; a failed job is never charged. If the gateway does not
|
|
12755
|
+
* accept poll signatures for the job, it is polled with a payment per poll
|
|
12756
|
+
* instead, which is likewise only charged on completion.
|
|
12757
|
+
*
|
|
12758
|
+
* @param prompt - text description of the video.
|
|
12759
|
+
* @param options - `model` (default `xai/grok-imagine-video`), `budgetMs`
|
|
12760
|
+
* (total wait, default 15 min); any other keys are sent verbatim, so use
|
|
12761
|
+
* the gateway's snake_case names (`duration_seconds`, `image_url`,
|
|
12762
|
+
* `aspect_ratio`, `resolution`, …).
|
|
12763
|
+
*/
|
|
12764
|
+
async video(prompt, options = {}) {
|
|
12765
|
+
const { model, budgetMs, ...rest } = options;
|
|
12766
|
+
const body = { ...rest, model: model || DEFAULT_VIDEO_MODEL, prompt };
|
|
12767
|
+
const data = await this.requestWithPaymentRaw(
|
|
12768
|
+
"/v1/videos/generations",
|
|
12769
|
+
body,
|
|
12770
|
+
{ pollBudgetMs: budgetMs ?? DEFAULT_VIDEO_BUDGET_MS, submitTimeoutMs: Math.max(this.timeout, MEDIA_SUBMIT_TIMEOUT_MS) }
|
|
12771
|
+
);
|
|
12772
|
+
return data;
|
|
12773
|
+
}
|
|
12774
|
+
/**
|
|
12775
|
+
* Generate a music track on Solana. Paid at the POST; a slow track is then
|
|
12776
|
+
* polled with a wallet signature, never another payment.
|
|
12777
|
+
*
|
|
12778
|
+
* @param prompt - music style, mood, or description.
|
|
12779
|
+
* @param options - `model` (default `minimax/music-2.5+`), `instrumental`
|
|
12780
|
+
* (default true), `lyrics` (requires `instrumental: false`), `budgetMs`
|
|
12781
|
+
* (total wait, default 5 min); other keys are sent verbatim.
|
|
12782
|
+
*/
|
|
12783
|
+
async music(prompt, options = {}) {
|
|
12784
|
+
const { model, instrumental: instrumentalOption, lyrics: rawLyrics, budgetMs, ...rest } = options;
|
|
12785
|
+
const instrumental = instrumentalOption ?? true;
|
|
12786
|
+
const lyrics = rawLyrics?.trim();
|
|
12787
|
+
if (instrumental && lyrics) throw new Error("Cannot specify lyrics when instrumental is true");
|
|
12788
|
+
const body = { ...rest, model: model || DEFAULT_MUSIC_MODEL, prompt, instrumental };
|
|
12789
|
+
if (lyrics) body.lyrics = lyrics;
|
|
12790
|
+
const data = await this.requestWithPaymentRaw(
|
|
12791
|
+
"/v1/audio/generations",
|
|
12792
|
+
body,
|
|
12793
|
+
{ pollBudgetMs: budgetMs ?? DEFAULT_MUSIC_BUDGET_MS, submitTimeoutMs: Math.max(this.timeout, MEDIA_SUBMIT_TIMEOUT_MS) }
|
|
12794
|
+
);
|
|
12795
|
+
return data;
|
|
12796
|
+
}
|
|
12314
12797
|
/** Standalone search (Solana payment). */
|
|
12315
12798
|
async search(query, options) {
|
|
12316
12799
|
const body = { query, max_results: options?.maxResults || 10 };
|
|
@@ -12509,15 +12992,16 @@ var SolanaLLMClient = class {
|
|
|
12509
12992
|
* `"paid-or-in-doubt"`, and so is any error with an API key once the billed
|
|
12510
12993
|
* request was sent, except the account API's explicit 4xx refusal.
|
|
12511
12994
|
*/
|
|
12512
|
-
async requestWithPayment(endpoint, body) {
|
|
12995
|
+
async requestWithPayment(endpoint, body, timeoutMs) {
|
|
12513
12996
|
const url = `${this.apiUrl}${endpoint}`;
|
|
12997
|
+
const json = JSON.stringify(body);
|
|
12514
12998
|
const closesAtStart = await this.batchPayer?.closeFence();
|
|
12515
12999
|
for (let staleRetries = 0; ; ) {
|
|
12516
13000
|
const response = await this.sendUnpaid(url, {
|
|
12517
13001
|
method: "POST",
|
|
12518
13002
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
12519
|
-
body:
|
|
12520
|
-
});
|
|
13003
|
+
body: json
|
|
13004
|
+
}, timeoutMs);
|
|
12521
13005
|
if (response.status === 402) {
|
|
12522
13006
|
try {
|
|
12523
13007
|
let paymentRequired;
|
|
@@ -12533,16 +13017,16 @@ var SolanaLLMClient = class {
|
|
|
12533
13017
|
(paymentHeaders) => this.fetchWithTimeout(url, {
|
|
12534
13018
|
method: "POST",
|
|
12535
13019
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
|
|
12536
|
-
body:
|
|
12537
|
-
}),
|
|
13020
|
+
body: json
|
|
13021
|
+
}, timeoutMs),
|
|
12538
13022
|
// A fresh, unpaid challenge after a 429 wait: the request itself,
|
|
12539
13023
|
// sent again without a payment. Every error it throws is unpaid.
|
|
12540
13024
|
async () => {
|
|
12541
13025
|
const challenge = await this.sendUnpaid(url, {
|
|
12542
13026
|
method: "POST",
|
|
12543
13027
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
12544
|
-
body:
|
|
12545
|
-
});
|
|
13028
|
+
body: json
|
|
13029
|
+
}, timeoutMs);
|
|
12546
13030
|
if (challenge.status === 402) {
|
|
12547
13031
|
try {
|
|
12548
13032
|
return { kind: "challenge", paymentRequired: await this.readPaymentRequired(challenge) };
|
|
@@ -12557,13 +13041,14 @@ var SolanaLLMClient = class {
|
|
|
12557
13041
|
);
|
|
12558
13042
|
if (batch.kind === "paid") {
|
|
12559
13043
|
this.recordSettlement(batch.chargedUsd);
|
|
12560
|
-
|
|
13044
|
+
const paid = await this.readPaidJson(batch.response);
|
|
13045
|
+
return withCost(paid, batch.response, batch.chargedUsd, "batch");
|
|
12561
13046
|
}
|
|
12562
|
-
if (batch.kind === "served") return await this.readUnpaidJson(batch.response);
|
|
13047
|
+
if (batch.kind === "served") return this.withUnpaidCost(await this.readUnpaidJson(batch.response));
|
|
12563
13048
|
if (batch.kind === "failed") throw batch.error;
|
|
12564
13049
|
exactRequired = batch.paymentRequired ?? paymentRequired;
|
|
12565
13050
|
}
|
|
12566
|
-
return await this.handlePaymentAndRetry(url,
|
|
13051
|
+
return await this.handlePaymentAndRetry(url, json, exactRequired, staleRetries > 0, timeoutMs);
|
|
12567
13052
|
} catch (error) {
|
|
12568
13053
|
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
12569
13054
|
await waitForStaleRetry(staleRetries++);
|
|
@@ -12571,8 +13056,20 @@ var SolanaLLMClient = class {
|
|
|
12571
13056
|
}
|
|
12572
13057
|
}
|
|
12573
13058
|
if (!response.ok) throw await unpaidApiError(response);
|
|
12574
|
-
return await this.readUnpaidJson(response);
|
|
13059
|
+
return this.withUnpaidCost(await this.readUnpaidJson(response));
|
|
13060
|
+
}
|
|
13061
|
+
}
|
|
13062
|
+
/**
|
|
13063
|
+
* Cost of a chat response served without a payment: 0 with a wallet (a free
|
|
13064
|
+
* model). With an API key the call is billed to the account, so costUsd stays
|
|
13065
|
+
* undefined (see the dashboard).
|
|
13066
|
+
*/
|
|
13067
|
+
withUnpaidCost(served) {
|
|
13068
|
+
if (!this.apiAuth) {
|
|
13069
|
+
served.costUsd = 0;
|
|
13070
|
+
served.settlement = { scheme: "free", quotedUsd: 0 };
|
|
12575
13071
|
}
|
|
13072
|
+
return served;
|
|
12576
13073
|
}
|
|
12577
13074
|
/**
|
|
12578
13075
|
* Read the JSON body of a 2xx served without a payment: the free tier, or
|
|
@@ -12590,9 +13087,9 @@ var SolanaLLMClient = class {
|
|
|
12590
13087
|
* {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
|
|
12591
13088
|
* {@link accountErrorDisposition} (the request itself is billed).
|
|
12592
13089
|
*/
|
|
12593
|
-
async sendUnpaid(url, init) {
|
|
13090
|
+
async sendUnpaid(url, init, timeoutMs) {
|
|
12594
13091
|
try {
|
|
12595
|
-
return await this.fetchWithTimeout(url, init);
|
|
13092
|
+
return await this.fetchWithTimeout(url, init, timeoutMs);
|
|
12596
13093
|
} catch (error) {
|
|
12597
13094
|
throw withDisposition(error, this.apiAuth ? accountErrorDisposition(error) : "unpaid");
|
|
12598
13095
|
}
|
|
@@ -12606,106 +13103,221 @@ var SolanaLLMClient = class {
|
|
|
12606
13103
|
}
|
|
12607
13104
|
}
|
|
12608
13105
|
/**
|
|
12609
|
-
*
|
|
12610
|
-
*
|
|
12611
|
-
*
|
|
12612
|
-
*
|
|
12613
|
-
*
|
|
12614
|
-
*
|
|
12615
|
-
*
|
|
12616
|
-
*
|
|
12617
|
-
*
|
|
12618
|
-
*
|
|
12619
|
-
*
|
|
12620
|
-
*
|
|
12621
|
-
*
|
|
12622
|
-
*
|
|
12623
|
-
*
|
|
12624
|
-
*
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
*
|
|
12628
|
-
*
|
|
12629
|
-
*
|
|
12630
|
-
*
|
|
12631
|
-
*
|
|
12632
|
-
*
|
|
12633
|
-
*
|
|
12634
|
-
*
|
|
12635
|
-
*
|
|
12636
|
-
*
|
|
12637
|
-
*
|
|
12638
|
-
*
|
|
12639
|
-
*
|
|
12640
|
-
* it would book spend for jobs that were never charged.
|
|
12641
|
-
* 2. **Every poll re-signs.** One authorization cannot be replayed across a
|
|
12642
|
-
* long render. The gateway binds the job to the payer address rather than
|
|
12643
|
-
* to the signature, so a fresh signature from the same wallet is accepted
|
|
12644
|
-
* and is never charged again.
|
|
13106
|
+
* Follow a 202 `{ id, poll_url }` to completion on a Solana media route
|
|
13107
|
+
* (image, music, video).
|
|
13108
|
+
*
|
|
13109
|
+
* **Polls prove ownership, they do not pay.** A Solana payment is a
|
|
13110
|
+
* transaction pinned to a recent blockhash (~60s), so paying every poll of a
|
|
13111
|
+
* multi-minute job meant signing a fresh transaction every few seconds. Each
|
|
13112
|
+
* poll here instead carries the `x-poll-*` headers: the submitting wallet's
|
|
13113
|
+
* ed25519 signature over `blockrun-poll:v1:<kind>:<job id>:<unix seconds>`
|
|
13114
|
+
* (see `poll-ownership.ts`). That proof never charges.
|
|
13115
|
+
*
|
|
13116
|
+
* **Who pays when:**
|
|
13117
|
+
* - image / music: the POST already settled (sol settles at submit because the
|
|
13118
|
+
* signed transaction would be dead by completion). The caller recorded the
|
|
13119
|
+
* cost at POST; this loop only fetches the result and never signs a payment.
|
|
13120
|
+
* A batch-billed image job still answers its proof poll with 402: that job
|
|
13121
|
+
* falls back to the per-poll payment below.
|
|
13122
|
+
* - video: settles on completion, and a failed job is never charged. A
|
|
13123
|
+
* finished, unpaid job answers the proof poll with 402 `{status:"completed"}`
|
|
13124
|
+
* and a normal x402 challenge; ONE payment is signed from that challenge and
|
|
13125
|
+
* the same poll is re-sent with it. The cost is recorded here, on delivery.
|
|
13126
|
+
* A paid poll the gateway answers 402 (settlement in progress on another
|
|
13127
|
+
* poll, outcome unknown, stale blockhash) is re-polled after its
|
|
13128
|
+
* Retry-After — the gateway's per-job settlement claim makes a later fresh
|
|
13129
|
+
* payment safe — up to `MAX_PAID_POLL_REFUSALS` times.
|
|
13130
|
+
*
|
|
13131
|
+
* **Fallback, once per job:** if the gateway refuses the proof with its "no
|
|
13132
|
+
* such job" answer (video 400, image/music 404) or answers it with a plain
|
|
13133
|
+
* payment challenge (a gateway or job that predates poll proofs), the rest of
|
|
13134
|
+
* this job is polled the old way — an x-payment signed per poll, which the
|
|
13135
|
+
* gateway verifies against the job's payer and does not charge until a video
|
|
13136
|
+
* completes. Logged to stderr; never switches back.
|
|
12645
13137
|
*/
|
|
12646
|
-
async followSolanaJob(submitBody, budgetMs, intervalMs = 2e3) {
|
|
13138
|
+
async followSolanaJob(submitBody, budgetMs, kind, intervalMs = kind === "video" ? VIDEO_POLL_INTERVAL_MS : 2e3) {
|
|
12647
13139
|
const id = submitBody.id;
|
|
12648
13140
|
const pollPath = submitBody.poll_url;
|
|
12649
13141
|
if (!id || !pollPath) return submitBody;
|
|
12650
|
-
const pollUrl =
|
|
13142
|
+
const pollUrl = this.resolvePollUrl(pollPath);
|
|
13143
|
+
const jobId = pollJobIdFromUrl(pollUrl);
|
|
12651
13144
|
const deadline = Date.now() + budgetMs;
|
|
12652
13145
|
let lastStatus = submitBody.status || "queued";
|
|
13146
|
+
let ownershipPolls = true;
|
|
13147
|
+
let waitMs = intervalMs;
|
|
13148
|
+
let paidRefusalsLeft = MAX_PAID_POLL_REFUSALS;
|
|
13149
|
+
let videoCostUsd;
|
|
13150
|
+
let settleAttempted = false;
|
|
13151
|
+
const fallBack = (why) => {
|
|
13152
|
+
ownershipPolls = false;
|
|
13153
|
+
waitMs = 0;
|
|
13154
|
+
console.warn(
|
|
13155
|
+
`[@blockrun/llm] ${kind} job ${id}: ${why}; falling back to x-payment polling for this job`
|
|
13156
|
+
);
|
|
13157
|
+
};
|
|
13158
|
+
const deliver = (data, response) => {
|
|
13159
|
+
if (kind === "video") {
|
|
13160
|
+
if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd);
|
|
13161
|
+
const txHash = response.headers.get("x-payment-receipt");
|
|
13162
|
+
if (txHash) data.txHash = txHash;
|
|
13163
|
+
}
|
|
13164
|
+
return data;
|
|
13165
|
+
};
|
|
13166
|
+
const failIfFailed = (data, status2) => {
|
|
13167
|
+
if (lastStatus === "failed") {
|
|
13168
|
+
throw new APIError(
|
|
13169
|
+
`Upstream job failed: ${data.error || "unknown"}`,
|
|
13170
|
+
status2,
|
|
13171
|
+
sanitizeErrorResponse(data)
|
|
13172
|
+
);
|
|
13173
|
+
}
|
|
13174
|
+
};
|
|
12653
13175
|
while (Date.now() < deadline) {
|
|
12654
|
-
|
|
12655
|
-
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
13176
|
+
if (waitMs > 0) await sleep3(Math.min(waitMs, Math.max(0, deadline - Date.now())));
|
|
13177
|
+
waitMs = intervalMs;
|
|
13178
|
+
let challenge;
|
|
13179
|
+
if (ownershipPolls) {
|
|
13180
|
+
const proof = signPollOwnership(await solanaKeyToBytes(this.privateKey), kind, jobId);
|
|
13181
|
+
const polled = await this.fetchWithTimeout(pollUrl, {
|
|
13182
|
+
method: "GET",
|
|
13183
|
+
headers: { "User-Agent": USER_AGENT, ...proof }
|
|
13184
|
+
});
|
|
13185
|
+
const data2 = await readJsonOrEmpty(polled);
|
|
13186
|
+
if (polled.status === 402) {
|
|
13187
|
+
if (kind === "video" && data2.status === "completed") {
|
|
13188
|
+
lastStatus = "completed";
|
|
13189
|
+
settleAttempted = true;
|
|
13190
|
+
challenge = new Response(JSON.stringify(data2), { status: 402, headers: polled.headers });
|
|
13191
|
+
} else {
|
|
13192
|
+
fallBack(
|
|
13193
|
+
kind === "image" ? "the gateway asks for a payment on this poll (a batch-billed job, or a gateway without poll proofs)" : "the gateway answered the poll proof with a payment challenge (a gateway without poll proofs)"
|
|
13194
|
+
);
|
|
13195
|
+
continue;
|
|
13196
|
+
}
|
|
13197
|
+
} else if (polled.status === (kind === "video" ? 400 : 404)) {
|
|
13198
|
+
fallBack(`the gateway refused the poll proof (HTTP ${polled.status})`);
|
|
13199
|
+
continue;
|
|
13200
|
+
} else {
|
|
13201
|
+
lastStatus = data2.status || lastStatus;
|
|
13202
|
+
failIfFailed(data2, polled.status);
|
|
13203
|
+
if (polled.status === 200 && lastStatus === "completed") return deliver(data2, polled);
|
|
13204
|
+
if (!isPendingPollStatus(polled.status)) {
|
|
13205
|
+
throw new APIError(`Poll failed: ${polled.status}`, polled.status, sanitizeErrorResponse(data2));
|
|
13206
|
+
}
|
|
13207
|
+
waitMs = retryAfterMs(polled) ?? intervalMs;
|
|
13208
|
+
continue;
|
|
13209
|
+
}
|
|
12662
13210
|
}
|
|
12663
|
-
if (challenge
|
|
12664
|
-
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
}
|
|
12668
|
-
|
|
13211
|
+
if (!challenge) {
|
|
13212
|
+
const unpaid = await this.fetchWithTimeout(pollUrl, {
|
|
13213
|
+
method: "GET",
|
|
13214
|
+
headers: { "User-Agent": USER_AGENT }
|
|
13215
|
+
});
|
|
13216
|
+
if (unpaid.status === 200) {
|
|
13217
|
+
return deliver(await unpaid.json(), unpaid);
|
|
12669
13218
|
}
|
|
12670
|
-
|
|
13219
|
+
if (unpaid.status !== 402) {
|
|
13220
|
+
let errorBody;
|
|
13221
|
+
try {
|
|
13222
|
+
errorBody = await unpaid.json();
|
|
13223
|
+
} catch {
|
|
13224
|
+
errorBody = { error: "Poll failed" };
|
|
13225
|
+
}
|
|
13226
|
+
throw new APIError(`Poll failed: ${unpaid.status}`, unpaid.status, sanitizeErrorResponse(errorBody));
|
|
13227
|
+
}
|
|
13228
|
+
challenge = unpaid;
|
|
12671
13229
|
}
|
|
12672
|
-
const { paymentPayload } = await this.signPaymentFrom402(
|
|
13230
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(
|
|
12673
13231
|
pollUrl,
|
|
12674
13232
|
challenge,
|
|
12675
13233
|
true,
|
|
12676
13234
|
// always a fresh blockhash; the last one is stale by now
|
|
12677
13235
|
pollUrl
|
|
12678
13236
|
);
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
headers: { "User-Agent": USER_AGENT, "PAYMENT-SIGNATURE": paymentPayload }
|
|
12682
|
-
});
|
|
12683
|
-
let data = {};
|
|
13237
|
+
if (kind === "video") videoCostUsd = costUsd;
|
|
13238
|
+
let paid;
|
|
12684
13239
|
try {
|
|
12685
|
-
|
|
12686
|
-
|
|
13240
|
+
paid = await this.fetchWithTimeout(pollUrl, {
|
|
13241
|
+
method: "GET",
|
|
13242
|
+
headers: { "User-Agent": USER_AGENT, "PAYMENT-SIGNATURE": paymentPayload }
|
|
13243
|
+
});
|
|
13244
|
+
} catch (error) {
|
|
13245
|
+
if (kind === "video" && lastStatus === "completed") settleAttempted = true;
|
|
13246
|
+
if (--paidRefusalsLeft <= 0) {
|
|
13247
|
+
throw withDisposition(
|
|
13248
|
+
new PaymentError(
|
|
13249
|
+
`Paid poll failed in transit (${error instanceof Error ? error.message : String(error)}). The payment may have settled: resume the job by polling ${pollUrl} with the same wallet before submitting again.`
|
|
13250
|
+
),
|
|
13251
|
+
"paid-or-in-doubt"
|
|
13252
|
+
);
|
|
13253
|
+
}
|
|
13254
|
+
continue;
|
|
12687
13255
|
}
|
|
13256
|
+
const data = await readJsonOrEmpty(paid);
|
|
12688
13257
|
lastStatus = data.status || lastStatus;
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
13258
|
+
failIfFailed(data, paid.status);
|
|
13259
|
+
if (paid.status === 200 && lastStatus === "completed") return deliver(data, paid);
|
|
13260
|
+
if (paid.status === 402 && kind === "video") {
|
|
13261
|
+
if (data.error === "Payment verification failed" && !await isSafeStaleBlockhashResponse(new Response(JSON.stringify(data), { status: 402 }))) {
|
|
13262
|
+
throw new PaymentError(
|
|
13263
|
+
`Payment for the finished video was rejected. Check your Solana USDC balance, then resume the job by polling ${pollUrl} with the same wallet.`
|
|
13264
|
+
);
|
|
13265
|
+
}
|
|
13266
|
+
settleAttempted = true;
|
|
13267
|
+
if (--paidRefusalsLeft <= 0) {
|
|
13268
|
+
throw withDisposition(
|
|
13269
|
+
new PaymentError(
|
|
13270
|
+
`Payment for the finished video did not settle after ${MAX_PAID_POLL_REFUSALS} attempts (last: ${data.error || "HTTP 402"}). It may still settle: resume the job by polling ${pollUrl} with the same wallet before submitting again.`
|
|
13271
|
+
),
|
|
13272
|
+
"paid-or-in-doubt"
|
|
13273
|
+
);
|
|
13274
|
+
}
|
|
13275
|
+
waitMs = retryAfterMs(paid) ?? intervalMs;
|
|
13276
|
+
continue;
|
|
12698
13277
|
}
|
|
12699
|
-
if (paid.status
|
|
13278
|
+
if (!isPendingPollStatus(paid.status) && paid.status !== 200) {
|
|
12700
13279
|
throw new APIError(`Poll failed: ${paid.status}`, paid.status, sanitizeErrorResponse(data));
|
|
12701
13280
|
}
|
|
13281
|
+
waitMs = retryAfterMs(paid) ?? intervalMs;
|
|
12702
13282
|
}
|
|
13283
|
+
const billing = kind === "video" ? settleAttempted ? "A payment for the finished video was sent and may still settle." : "Nothing was charged: a video is charged only when a completed poll is paid." : "The submit was already paid.";
|
|
12703
13284
|
throw new APIError(
|
|
12704
|
-
`Job did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}).
|
|
13285
|
+
`Job did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). ${billing} Resume by polling the same poll_url with the same wallet before submitting again.`,
|
|
12705
13286
|
504,
|
|
12706
|
-
{ id, last_status: lastStatus }
|
|
13287
|
+
{ id, last_status: lastStatus, poll_url: pollUrl }
|
|
12707
13288
|
);
|
|
12708
13289
|
}
|
|
13290
|
+
/**
|
|
13291
|
+
* Absolute URL for a job's `poll_url`. The gateway returns `/api/v1/...`;
|
|
13292
|
+
* {@link buildUrl} drops the `/api` that `apiUrl` already ends with. An
|
|
13293
|
+
* absolute poll_url must be on the gateway's own origin: the poll carries a
|
|
13294
|
+
* wallet signature (and, for a finished video, a payment).
|
|
13295
|
+
*/
|
|
13296
|
+
resolvePollUrl(pollPath) {
|
|
13297
|
+
if (!/^https?:\/\//i.test(pollPath)) return this.buildUrl(pollPath);
|
|
13298
|
+
if (new URL(pollPath).origin !== new URL(this.apiUrl).origin) {
|
|
13299
|
+
throw new APIError(`poll_url is not on the gateway origin: ${pollPath}`, 502, { poll_url: pollPath });
|
|
13300
|
+
}
|
|
13301
|
+
return pollPath;
|
|
13302
|
+
}
|
|
13303
|
+
/**
|
|
13304
|
+
* Turn a `402` into a signed Solana payment payload.
|
|
13305
|
+
*
|
|
13306
|
+
* Extracted because four call sites need it — chat, the raw POST helpers, the
|
|
13307
|
+
* raw GET helper, and {@link SolanaLLMClient.stream} — and it had been
|
|
13308
|
+
* written out three times before this. That mattered more than ordinary
|
|
13309
|
+
* duplication: this is the code that signs a transfer of the caller's USDC,
|
|
13310
|
+
* so three copies meant every fix to it had to be applied three times or
|
|
13311
|
+
* quietly apply to two thirds of the paths.
|
|
13312
|
+
*
|
|
13313
|
+
* @param url - the request being paid for.
|
|
13314
|
+
* @param response - the gateway's `402`, not yet consumed.
|
|
13315
|
+
* @param forceFreshBlockhash - set on a re-sign after a stale-blockhash
|
|
13316
|
+
* rejection, so the retry cannot produce byte-identical transaction bytes.
|
|
13317
|
+
* @param resourceFallback - resource URL to claim when the 402 states none.
|
|
13318
|
+
* @returns the header value to replay with, and what it will settle for.
|
|
13319
|
+
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
13320
|
+
*/
|
|
12709
13321
|
async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
|
|
12710
13322
|
return this.signExactPayment(
|
|
12711
13323
|
url,
|
|
@@ -12746,7 +13358,7 @@ var SolanaLLMClient = class {
|
|
|
12746
13358
|
if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
|
|
12747
13359
|
const fromAddress = await this.getWalletAddress();
|
|
12748
13360
|
const secretKey = await solanaKeyToBytes(this.privateKey);
|
|
12749
|
-
const extensions = paymentRequired.extensions;
|
|
13361
|
+
const extensions = withPaymentIdentifier(paymentRequired).extensions;
|
|
12750
13362
|
const paymentPayload = await createSolanaPaymentPayload(
|
|
12751
13363
|
secretKey,
|
|
12752
13364
|
fromAddress,
|
|
@@ -12769,7 +13381,7 @@ var SolanaLLMClient = class {
|
|
|
12769
13381
|
);
|
|
12770
13382
|
return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
|
|
12771
13383
|
}
|
|
12772
|
-
async handlePaymentAndRetry(url,
|
|
13384
|
+
async handlePaymentAndRetry(url, json, paymentRequired, forceFreshBlockhash = false, timeoutMs) {
|
|
12773
13385
|
let signed;
|
|
12774
13386
|
try {
|
|
12775
13387
|
signed = await this.signExactPayment(url, paymentRequired, forceFreshBlockhash, `${this.apiUrl}/v1/chat/completions`);
|
|
@@ -12786,26 +13398,28 @@ var SolanaLLMClient = class {
|
|
|
12786
13398
|
"User-Agent": USER_AGENT,
|
|
12787
13399
|
"PAYMENT-SIGNATURE": paymentPayload
|
|
12788
13400
|
},
|
|
12789
|
-
body:
|
|
12790
|
-
});
|
|
13401
|
+
body: json
|
|
13402
|
+
}, timeoutMs);
|
|
12791
13403
|
} catch (error) {
|
|
12792
13404
|
throw withDisposition(error, "paid-or-in-doubt");
|
|
12793
13405
|
}
|
|
12794
13406
|
await this.assertPaid(retryResponse);
|
|
12795
13407
|
this.recordSettlement(costUsd);
|
|
12796
|
-
|
|
13408
|
+
const paid = await this.readPaidJson(retryResponse);
|
|
13409
|
+
return withCost(paid, retryResponse, costUsd, "exact");
|
|
12797
13410
|
}
|
|
12798
|
-
async requestWithPaymentRaw(endpoint, body) {
|
|
13411
|
+
async requestWithPaymentRaw(endpoint, body, media = {}) {
|
|
12799
13412
|
const url = `${this.apiUrl}${endpoint}`;
|
|
13413
|
+
const pollBudgetMs = media.pollBudgetMs ?? this.timeout;
|
|
12800
13414
|
for (let staleRetries = 0; ; ) {
|
|
12801
13415
|
const response = await this.fetchWithTimeout(url, {
|
|
12802
13416
|
method: "POST",
|
|
12803
13417
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
12804
13418
|
body: JSON.stringify(body)
|
|
12805
|
-
});
|
|
13419
|
+
}, media.submitTimeoutMs);
|
|
12806
13420
|
if (response.status === 402) {
|
|
12807
13421
|
try {
|
|
12808
|
-
return await this.handlePaymentAndRetryRaw(url, body, response, staleRetries > 0);
|
|
13422
|
+
return await this.handlePaymentAndRetryRaw(url, body, response, staleRetries > 0, media);
|
|
12809
13423
|
} catch (error) {
|
|
12810
13424
|
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
12811
13425
|
await waitForStaleRetry(staleRetries++);
|
|
@@ -12821,26 +13435,38 @@ var SolanaLLMClient = class {
|
|
|
12821
13435
|
}
|
|
12822
13436
|
throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
|
|
12823
13437
|
}
|
|
12824
|
-
if (this.apiAuth &&
|
|
13438
|
+
if (this.apiAuth && /^\/v1\/(images|videos|audio)\//.test(endpoint)) {
|
|
13439
|
+
return this.apiAuth.poll(
|
|
13440
|
+
response,
|
|
13441
|
+
pollBudgetMs,
|
|
13442
|
+
endpoint.startsWith("/v1/videos/") ? VIDEO_POLL_INTERVAL_MS : 2e3
|
|
13443
|
+
);
|
|
13444
|
+
}
|
|
12825
13445
|
return response.json();
|
|
12826
13446
|
}
|
|
12827
13447
|
}
|
|
12828
|
-
async handlePaymentAndRetryRaw(url, body, response, forceFreshBlockhash = false) {
|
|
13448
|
+
async handlePaymentAndRetryRaw(url, body, response, forceFreshBlockhash = false, media = {}) {
|
|
12829
13449
|
const { paymentPayload, costUsd } = await this.signPaymentFrom402(url, response, forceFreshBlockhash);
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
13450
|
+
let retryResponse;
|
|
13451
|
+
try {
|
|
13452
|
+
retryResponse = await this.fetchWithTimeout(url, {
|
|
13453
|
+
method: "POST",
|
|
13454
|
+
headers: {
|
|
13455
|
+
"Content-Type": "application/json",
|
|
13456
|
+
"User-Agent": USER_AGENT,
|
|
13457
|
+
"PAYMENT-SIGNATURE": paymentPayload
|
|
13458
|
+
},
|
|
13459
|
+
body: JSON.stringify(body)
|
|
13460
|
+
}, media.submitTimeoutMs);
|
|
13461
|
+
} catch (error) {
|
|
13462
|
+
throw withDisposition(error, "paid-or-in-doubt");
|
|
13463
|
+
}
|
|
12839
13464
|
await this.assertPaid(retryResponse);
|
|
12840
13465
|
if (retryResponse.status === 202) {
|
|
12841
|
-
|
|
13466
|
+
const kind = pollKindFor(url);
|
|
13467
|
+
if (kind !== "video") this.recordSettlement(costUsd);
|
|
12842
13468
|
const submitted = await retryResponse.json();
|
|
12843
|
-
return this.followSolanaJob(submitted, this.timeout);
|
|
13469
|
+
return this.followSolanaJob(submitted, media.pollBudgetMs ?? this.timeout, kind);
|
|
12844
13470
|
}
|
|
12845
13471
|
this.recordSettlement(costUsd);
|
|
12846
13472
|
return retryResponse.json();
|
|
@@ -12929,9 +13555,9 @@ var SolanaLLMClient = class {
|
|
|
12929
13555
|
this.sessionCalls += 1;
|
|
12930
13556
|
this.sessionTotalUsd += costUsd;
|
|
12931
13557
|
}
|
|
12932
|
-
async fetchWithTimeout(url, options) {
|
|
13558
|
+
async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
|
|
12933
13559
|
const controller = new AbortController();
|
|
12934
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
13560
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
12935
13561
|
try {
|
|
12936
13562
|
return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...options, signal: controller.signal });
|
|
12937
13563
|
} finally {
|
|
@@ -13496,6 +14122,7 @@ var AnthropicClient = class {
|
|
|
13496
14122
|
BatchPaymentUnresolvedError,
|
|
13497
14123
|
BlockrunClient,
|
|
13498
14124
|
BlockrunError,
|
|
14125
|
+
DEFAULT_BATCH_IN_DOUBT,
|
|
13499
14126
|
DEFAULT_BATCH_RATE_LIMIT,
|
|
13500
14127
|
EVM_NETWORKS,
|
|
13501
14128
|
ImageClient,
|
|
@@ -13558,6 +14185,7 @@ var AnthropicClient = class {
|
|
|
13558
14185
|
loadWallet,
|
|
13559
14186
|
logCost,
|
|
13560
14187
|
parsePaymentRequired,
|
|
14188
|
+
readSettlement,
|
|
13561
14189
|
retryDisposition,
|
|
13562
14190
|
saveSolanaWallet,
|
|
13563
14191
|
saveToCache,
|
|
@@ -13572,6 +14200,8 @@ var AnthropicClient = class {
|
|
|
13572
14200
|
solanaKeyToBytes,
|
|
13573
14201
|
solanaPublicKey,
|
|
13574
14202
|
status,
|
|
14203
|
+
toWireSearchParameters,
|
|
14204
|
+
toWireTools,
|
|
13575
14205
|
validateMaxTokens,
|
|
13576
14206
|
validateModel,
|
|
13577
14207
|
validateTemperature,
|