@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.js
CHANGED
|
@@ -313,6 +313,122 @@ var ApiKeyAuth = class {
|
|
|
313
313
|
}
|
|
314
314
|
};
|
|
315
315
|
|
|
316
|
+
// src/search-wire.ts
|
|
317
|
+
function put(out, key, value) {
|
|
318
|
+
if (value !== void 0) out[key] = value;
|
|
319
|
+
}
|
|
320
|
+
function handles(list) {
|
|
321
|
+
return list?.map((h) => h.trim().replace(/^@+/, ""));
|
|
322
|
+
}
|
|
323
|
+
function sourceToWire(source) {
|
|
324
|
+
const raw = source;
|
|
325
|
+
const out = { ...raw };
|
|
326
|
+
for (const key of Object.keys(out)) if (/[A-Z]/.test(key)) delete out[key];
|
|
327
|
+
switch (source.type) {
|
|
328
|
+
case "x":
|
|
329
|
+
put(out, "included_x_handles", handles(source.includedXHandles) ?? out.included_x_handles);
|
|
330
|
+
put(out, "excluded_x_handles", handles(source.excludedXHandles) ?? out.excluded_x_handles);
|
|
331
|
+
put(out, "post_favorite_count", source.postFavoriteCount);
|
|
332
|
+
put(out, "post_view_count", source.postViewCount);
|
|
333
|
+
put(out, "enable_image_understanding", source.enableImageUnderstanding);
|
|
334
|
+
put(out, "enable_video_understanding", source.enableVideoUnderstanding);
|
|
335
|
+
break;
|
|
336
|
+
case "web":
|
|
337
|
+
put(out, "country", source.country);
|
|
338
|
+
put(out, "allowed_websites", source.allowedWebsites);
|
|
339
|
+
put(out, "excluded_websites", source.excludedWebsites);
|
|
340
|
+
put(out, "safe_search", source.safeSearch);
|
|
341
|
+
put(out, "enable_image_understanding", source.enableImageUnderstanding);
|
|
342
|
+
break;
|
|
343
|
+
case "news":
|
|
344
|
+
put(out, "country", source.country);
|
|
345
|
+
put(out, "allowed_websites", source.allowedWebsites);
|
|
346
|
+
put(out, "excluded_websites", source.excludedWebsites);
|
|
347
|
+
put(out, "safe_search", source.safeSearch);
|
|
348
|
+
break;
|
|
349
|
+
case "rss":
|
|
350
|
+
put(out, "links", source.links);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
function toWireSearchParameters(sp) {
|
|
356
|
+
const raw = sp;
|
|
357
|
+
const out = {};
|
|
358
|
+
for (const [key, value] of Object.entries(raw)) if (!/[A-Z]/.test(key) && key !== "sources") out[key] = value;
|
|
359
|
+
put(out, "mode", sp.mode);
|
|
360
|
+
if (sp.sources) out.sources = sp.sources.map(sourceToWire);
|
|
361
|
+
put(out, "return_citations", sp.returnCitations);
|
|
362
|
+
put(out, "from_date", sp.fromDate);
|
|
363
|
+
put(out, "to_date", sp.toDate);
|
|
364
|
+
put(out, "max_search_results", sp.maxSearchResults);
|
|
365
|
+
put(out, "max_turns", sp.maxTurns);
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
function toWireTools(tools) {
|
|
369
|
+
return tools.map((tool) => {
|
|
370
|
+
if (tool.type === "x_search") {
|
|
371
|
+
const out = { type: "x_search" };
|
|
372
|
+
put(out, "allowed_x_handles", handles(tool.allowedXHandles));
|
|
373
|
+
put(out, "excluded_x_handles", handles(tool.excludedXHandles));
|
|
374
|
+
put(out, "from_date", tool.fromDate);
|
|
375
|
+
put(out, "to_date", tool.toDate);
|
|
376
|
+
put(out, "enable_image_understanding", tool.enableImageUnderstanding);
|
|
377
|
+
put(out, "enable_video_understanding", tool.enableVideoUnderstanding);
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
if (tool.type === "web_search") {
|
|
381
|
+
const out = { type: "web_search" };
|
|
382
|
+
put(out, "allowed_domains", tool.allowedDomains);
|
|
383
|
+
put(out, "excluded_domains", tool.excludedDomains);
|
|
384
|
+
put(out, "enable_image_understanding", tool.enableImageUnderstanding);
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
return tool;
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/receipt.ts
|
|
392
|
+
function atomicToUsd(amount) {
|
|
393
|
+
return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
|
|
394
|
+
}
|
|
395
|
+
function receiptChargedUsd(receipt) {
|
|
396
|
+
return atomicToUsd(receipt?.extra?.chargedAmount) ?? atomicToUsd(receipt?.amount);
|
|
397
|
+
}
|
|
398
|
+
function decode(header) {
|
|
399
|
+
try {
|
|
400
|
+
const json = typeof Buffer !== "undefined" ? Buffer.from(header, "base64").toString("utf8") : atob(header);
|
|
401
|
+
const parsed = JSON.parse(json);
|
|
402
|
+
return parsed && typeof parsed === "object" ? parsed : void 0;
|
|
403
|
+
} catch {
|
|
404
|
+
return void 0;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function readSettlement(response, quotedUsd, scheme) {
|
|
408
|
+
const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
|
|
409
|
+
const receipt = header ? decode(header) : void 0;
|
|
410
|
+
const extra = receipt?.extra;
|
|
411
|
+
const costUsd = receiptChargedUsd({
|
|
412
|
+
amount: receipt?.amount,
|
|
413
|
+
extra: extra && typeof extra === "object" ? extra : void 0
|
|
414
|
+
}) ?? quotedUsd;
|
|
415
|
+
const receiptTx = typeof receipt?.transaction === "string" && receipt.transaction ? receipt.transaction : void 0;
|
|
416
|
+
const transaction = receiptTx ?? (response.headers.get("X-Payment-Receipt")?.trim() || void 0);
|
|
417
|
+
const settlement = {
|
|
418
|
+
scheme,
|
|
419
|
+
quotedUsd,
|
|
420
|
+
...transaction ? { transaction } : {},
|
|
421
|
+
...typeof receipt?.network === "string" ? { network: receipt.network } : {},
|
|
422
|
+
...receipt?.success === false ? { success: false } : {}
|
|
423
|
+
};
|
|
424
|
+
return { costUsd, settlement };
|
|
425
|
+
}
|
|
426
|
+
function withCost(body, response, bookedUsd, scheme) {
|
|
427
|
+
body.costUsd = bookedUsd;
|
|
428
|
+
body.settlement = readSettlement(response, bookedUsd, scheme).settlement;
|
|
429
|
+
return body;
|
|
430
|
+
}
|
|
431
|
+
|
|
316
432
|
// src/client.ts
|
|
317
433
|
import { privateKeyToAccount } from "viem/accounts";
|
|
318
434
|
|
|
@@ -4099,6 +4215,11 @@ function errSummary(err) {
|
|
|
4099
4215
|
}
|
|
4100
4216
|
return String(err).slice(0, 100);
|
|
4101
4217
|
}
|
|
4218
|
+
function requestsSearch(options) {
|
|
4219
|
+
if (options.tools?.some((tool) => tool.type === "x_search" || tool.type === "web_search")) return true;
|
|
4220
|
+
if (options.searchParameters !== void 0) return options.searchParameters.mode !== "off";
|
|
4221
|
+
return options.search === true;
|
|
4222
|
+
}
|
|
4102
4223
|
function routingProfileForModel(model) {
|
|
4103
4224
|
return AUTO_ROUTING_PROFILES[model.toLowerCase()];
|
|
4104
4225
|
}
|
|
@@ -4126,7 +4247,7 @@ function routingText(messages) {
|
|
|
4126
4247
|
};
|
|
4127
4248
|
}
|
|
4128
4249
|
function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, options = {}) {
|
|
4129
|
-
const tools = options.tools ?? [];
|
|
4250
|
+
const tools = (options.tools ?? []).filter((tool) => tool.type === "function");
|
|
4130
4251
|
const requiresTools = options.toolChoice === "none" ? false : options.toolChoice === "required" || typeof options.toolChoice === "object" ? true : void 0;
|
|
4131
4252
|
const decision = route(prompt, systemPrompt, maxOutputTokens, {
|
|
4132
4253
|
config: DEFAULT_ROUTING_CONFIG,
|
|
@@ -4146,6 +4267,15 @@ function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, o
|
|
|
4146
4267
|
const resolved = !id.startsWith("free/") ? id : modelPricing.has(`nvidia/${id.slice(5)}`) ? `nvidia/${id.slice(5)}` : null;
|
|
4147
4268
|
if (resolved && !callable.includes(resolved)) callable.push(resolved);
|
|
4148
4269
|
}
|
|
4270
|
+
if (requestsSearch(options)) {
|
|
4271
|
+
const searchable = callable.filter((id) => id.startsWith("xai/"));
|
|
4272
|
+
if (searchable.length === 0) {
|
|
4273
|
+
throw new Error(
|
|
4274
|
+
`Search (x_search / web_search tools or searchParameters) runs on xai/* models only, and the ${options.routingProfile ?? "auto"} route has no xai/* candidate for this request. Call an xai/* model directly (e.g. xai/grok-4.3) or drop the search options.`
|
|
4275
|
+
);
|
|
4276
|
+
}
|
|
4277
|
+
callable.splice(0, callable.length, ...searchable);
|
|
4278
|
+
}
|
|
4149
4279
|
const estimatedInputTokens = Math.ceil(
|
|
4150
4280
|
Math.max(options.conversationChars ?? 0, `${systemPrompt ?? ""} ${prompt}`.length) / 4
|
|
4151
4281
|
);
|
|
@@ -4564,7 +4694,7 @@ function getCostSummary() {
|
|
|
4564
4694
|
}
|
|
4565
4695
|
|
|
4566
4696
|
// src/version.ts
|
|
4567
|
-
var SDK_VERSION = "3.
|
|
4697
|
+
var SDK_VERSION = "3.22.0";
|
|
4568
4698
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4569
4699
|
|
|
4570
4700
|
// src/client.ts
|
|
@@ -4710,7 +4840,9 @@ var LLMClient = class _LLMClient {
|
|
|
4710
4840
|
options?.maxOutputTokens ?? options?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
4711
4841
|
{
|
|
4712
4842
|
routingProfile: options?.routingProfile,
|
|
4713
|
-
requiresStructuredOutput: options?.responseFormat !== void 0
|
|
4843
|
+
requiresStructuredOutput: options?.responseFormat !== void 0,
|
|
4844
|
+
search: options?.search,
|
|
4845
|
+
searchParameters: options?.searchParameters
|
|
4714
4846
|
}
|
|
4715
4847
|
);
|
|
4716
4848
|
}
|
|
@@ -4777,6 +4909,8 @@ var LLMClient = class _LLMClient {
|
|
|
4777
4909
|
requiresStructuredOutput: options.responseFormat !== void 0,
|
|
4778
4910
|
tools: options.tools,
|
|
4779
4911
|
toolChoice: options.toolChoice,
|
|
4912
|
+
search: options.search,
|
|
4913
|
+
searchParameters: options.searchParameters,
|
|
4780
4914
|
conversationChars,
|
|
4781
4915
|
hasVision
|
|
4782
4916
|
}
|
|
@@ -4791,6 +4925,7 @@ var LLMClient = class _LLMClient {
|
|
|
4791
4925
|
toolChoice: options.toolChoice,
|
|
4792
4926
|
responseFormat: options.responseFormat,
|
|
4793
4927
|
stop: options.stop,
|
|
4928
|
+
timeout: options.timeout,
|
|
4794
4929
|
// An explicit caller-supplied chain wins over the routed one.
|
|
4795
4930
|
fallbackModels: options.fallbackModels ?? decision.fallbacks
|
|
4796
4931
|
});
|
|
@@ -4867,11 +5002,11 @@ var LLMClient = class _LLMClient {
|
|
|
4867
5002
|
if (options?.temperature !== void 0) body.temperature = options.temperature;
|
|
4868
5003
|
if (options?.topP !== void 0) body.top_p = options.topP;
|
|
4869
5004
|
if (options?.searchParameters !== void 0) {
|
|
4870
|
-
body.search_parameters = options.searchParameters;
|
|
5005
|
+
body.search_parameters = toWireSearchParameters(options.searchParameters);
|
|
4871
5006
|
} else if (options?.search === true) {
|
|
4872
5007
|
body.search_parameters = { mode: "on" };
|
|
4873
5008
|
}
|
|
4874
|
-
if (options?.tools !== void 0) body.tools = options.tools;
|
|
5009
|
+
if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
|
|
4875
5010
|
if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
|
|
4876
5011
|
if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
|
|
4877
5012
|
if (options?.stop !== void 0) body.stop = options.stop;
|
|
@@ -4882,7 +5017,7 @@ var LLMClient = class _LLMClient {
|
|
|
4882
5017
|
for (let i = 0; i < chain.length; i++) {
|
|
4883
5018
|
const candidate = chain[i];
|
|
4884
5019
|
try {
|
|
4885
|
-
return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
|
|
5020
|
+
return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
|
|
4886
5021
|
} catch (err) {
|
|
4887
5022
|
lastErr = err;
|
|
4888
5023
|
const next = chain[i + 1];
|
|
@@ -4953,19 +5088,19 @@ var LLMClient = class _LLMClient {
|
|
|
4953
5088
|
* is `"paid-or-in-doubt"`. With an API key the request itself is billed, so
|
|
4954
5089
|
* only the account API's explicit 4xx refusal is `"unpaid"`.
|
|
4955
5090
|
*/
|
|
4956
|
-
async requestWithPayment(endpoint, body) {
|
|
5091
|
+
async requestWithPayment(endpoint, body, timeoutMs) {
|
|
4957
5092
|
const url = `${this.apiUrl}${endpoint}`;
|
|
4958
5093
|
const unpaid = {
|
|
4959
5094
|
method: "POST",
|
|
4960
5095
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
4961
5096
|
body: JSON.stringify(body)
|
|
4962
5097
|
};
|
|
4963
|
-
const response = await this.sendUnpaid(url, unpaid);
|
|
5098
|
+
const response = await this.sendUnpaid(url, unpaid, timeoutMs);
|
|
4964
5099
|
if (response.status === 502 || response.status === 503) {
|
|
4965
5100
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
4966
|
-
const retryResp = await this.sendUnpaid(url, unpaid);
|
|
5101
|
+
const retryResp = await this.sendUnpaid(url, unpaid, timeoutMs);
|
|
4967
5102
|
if (retryResp.status !== 502 && retryResp.status !== 503) {
|
|
4968
|
-
if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp);
|
|
5103
|
+
if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp, timeoutMs);
|
|
4969
5104
|
if (!retryResp.ok) {
|
|
4970
5105
|
let errorBody;
|
|
4971
5106
|
try {
|
|
@@ -4982,7 +5117,7 @@ var LLMClient = class _LLMClient {
|
|
|
4982
5117
|
}
|
|
4983
5118
|
}
|
|
4984
5119
|
if (response.status === 402) {
|
|
4985
|
-
return this.handlePaymentAndRetry(url, body, response);
|
|
5120
|
+
return this.handlePaymentAndRetry(url, body, response, timeoutMs);
|
|
4986
5121
|
}
|
|
4987
5122
|
if (!response.ok) {
|
|
4988
5123
|
let errorBody;
|
|
@@ -5007,9 +5142,9 @@ var LLMClient = class _LLMClient {
|
|
|
5007
5142
|
* {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
|
|
5008
5143
|
* {@link accountErrorDisposition} (the request itself is billed).
|
|
5009
5144
|
*/
|
|
5010
|
-
async sendUnpaid(url, init) {
|
|
5145
|
+
async sendUnpaid(url, init, timeoutMs) {
|
|
5011
5146
|
try {
|
|
5012
|
-
return await this.fetchWithTimeout(url, init);
|
|
5147
|
+
return await this.fetchWithTimeout(url, init, timeoutMs);
|
|
5013
5148
|
} catch (err) {
|
|
5014
5149
|
throw withDisposition(err, this.apiAuth ? accountErrorDisposition(err) : "unpaid");
|
|
5015
5150
|
}
|
|
@@ -5017,13 +5152,22 @@ var LLMClient = class _LLMClient {
|
|
|
5017
5152
|
/**
|
|
5018
5153
|
* Parse a response to a request that carried no payment: the free tier,
|
|
5019
5154
|
* or, with an API key, the billed request itself.
|
|
5155
|
+
*
|
|
5156
|
+
* With a wallet nothing was paid, so `costUsd` is 0 (a free model). With an
|
|
5157
|
+
* API key the call is billed to the account and `costUsd` stays undefined.
|
|
5020
5158
|
*/
|
|
5021
5159
|
async parseUnpaidChatResponse(response) {
|
|
5160
|
+
let served;
|
|
5022
5161
|
try {
|
|
5023
|
-
|
|
5162
|
+
served = await this.parseChatResponse(response);
|
|
5024
5163
|
} catch (err) {
|
|
5025
5164
|
throw withDisposition(err, this.apiAuth ? "paid-or-in-doubt" : "unpaid");
|
|
5026
5165
|
}
|
|
5166
|
+
if (!this.apiAuth) {
|
|
5167
|
+
served.costUsd = 0;
|
|
5168
|
+
served.settlement = { scheme: "free", quotedUsd: 0 };
|
|
5169
|
+
}
|
|
5170
|
+
return served;
|
|
5027
5171
|
}
|
|
5028
5172
|
/**
|
|
5029
5173
|
* Handle 402 response: parse requirements, sign payment, retry.
|
|
@@ -5031,7 +5175,7 @@ var LLMClient = class _LLMClient {
|
|
|
5031
5175
|
* Anything thrown before the signed payment is sent is `"unpaid"`; anything
|
|
5032
5176
|
* after it, `"paid-or-in-doubt"` (see {@link RetryDisposition}).
|
|
5033
5177
|
*/
|
|
5034
|
-
async handlePaymentAndRetry(url, body, response) {
|
|
5178
|
+
async handlePaymentAndRetry(url, body, response, timeoutMs) {
|
|
5035
5179
|
let signed;
|
|
5036
5180
|
try {
|
|
5037
5181
|
signed = await this.signFrom402(url, response);
|
|
@@ -5039,7 +5183,7 @@ var LLMClient = class _LLMClient {
|
|
|
5039
5183
|
throw withDisposition(err, "unpaid");
|
|
5040
5184
|
}
|
|
5041
5185
|
try {
|
|
5042
|
-
return await this.sendPaid(url, body, signed.paymentPayload, signed.details);
|
|
5186
|
+
return await this.sendPaid(url, body, signed.paymentPayload, signed.details, timeoutMs);
|
|
5043
5187
|
} catch (err) {
|
|
5044
5188
|
throw withDisposition(err, "paid-or-in-doubt");
|
|
5045
5189
|
}
|
|
@@ -5084,7 +5228,7 @@ var LLMClient = class _LLMClient {
|
|
|
5084
5228
|
return { paymentPayload, details };
|
|
5085
5229
|
}
|
|
5086
5230
|
/** Send the signed payment and read the answer. The payment may settle from here on. */
|
|
5087
|
-
async sendPaid(url, body, paymentPayload, details) {
|
|
5231
|
+
async sendPaid(url, body, paymentPayload, details, timeoutMs) {
|
|
5088
5232
|
const retryResponse = await this.fetchWithTimeout(url, {
|
|
5089
5233
|
method: "POST",
|
|
5090
5234
|
headers: {
|
|
@@ -5093,7 +5237,7 @@ var LLMClient = class _LLMClient {
|
|
|
5093
5237
|
"PAYMENT-SIGNATURE": paymentPayload
|
|
5094
5238
|
},
|
|
5095
5239
|
body: JSON.stringify(body)
|
|
5096
|
-
});
|
|
5240
|
+
}, timeoutMs);
|
|
5097
5241
|
if (retryResponse.status === 502 || retryResponse.status === 503) {
|
|
5098
5242
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
5099
5243
|
const retryResp2 = await this.fetchWithTimeout(url, {
|
|
@@ -5104,7 +5248,7 @@ var LLMClient = class _LLMClient {
|
|
|
5104
5248
|
"PAYMENT-SIGNATURE": paymentPayload
|
|
5105
5249
|
},
|
|
5106
5250
|
body: JSON.stringify(body)
|
|
5107
|
-
});
|
|
5251
|
+
}, timeoutMs);
|
|
5108
5252
|
if (retryResp2.status !== 502 && retryResp2.status !== 503) {
|
|
5109
5253
|
if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
|
|
5110
5254
|
if (!retryResp2.ok) {
|
|
@@ -5120,7 +5264,7 @@ var LLMClient = class _LLMClient {
|
|
|
5120
5264
|
this.sessionCalls += 1;
|
|
5121
5265
|
this.sessionTotalUsd += costUsd2;
|
|
5122
5266
|
this.recordCost(url, costUsd2, { body, network: details.network });
|
|
5123
|
-
return this.parseChatResponse(retryResp2);
|
|
5267
|
+
return withCost(await this.parseChatResponse(retryResp2), retryResp2, costUsd2, "exact");
|
|
5124
5268
|
}
|
|
5125
5269
|
}
|
|
5126
5270
|
if (retryResponse.status === 402) {
|
|
@@ -5143,7 +5287,7 @@ var LLMClient = class _LLMClient {
|
|
|
5143
5287
|
this.sessionCalls += 1;
|
|
5144
5288
|
this.sessionTotalUsd += costUsd;
|
|
5145
5289
|
this.recordCost(url, costUsd, { body, network: details.network });
|
|
5146
|
-
return this.parseChatResponse(retryResponse);
|
|
5290
|
+
return withCost(await this.parseChatResponse(retryResponse), retryResponse, costUsd, "exact");
|
|
5147
5291
|
}
|
|
5148
5292
|
/**
|
|
5149
5293
|
* Sign a payment header and return the PAYMENT-SIGNATURE value.
|
|
@@ -5213,7 +5357,7 @@ var LLMClient = class _LLMClient {
|
|
|
5213
5357
|
};
|
|
5214
5358
|
if (options?.temperature !== void 0) body.temperature = options.temperature;
|
|
5215
5359
|
if (options?.topP !== void 0) body.top_p = options.topP;
|
|
5216
|
-
if (options?.tools !== void 0) body.tools = options.tools;
|
|
5360
|
+
if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
|
|
5217
5361
|
if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
|
|
5218
5362
|
if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
|
|
5219
5363
|
if (options?.stop !== void 0) body.stop = options.stop;
|
|
@@ -5611,11 +5755,11 @@ var LLMClient = class _LLMClient {
|
|
|
5611
5755
|
return retryResponse.json();
|
|
5612
5756
|
}
|
|
5613
5757
|
/**
|
|
5614
|
-
* Fetch with timeout
|
|
5758
|
+
* Fetch with timeout: `timeoutMs` (a per-call override), else the client's `timeout`.
|
|
5615
5759
|
*/
|
|
5616
|
-
async fetchWithTimeout(url, options) {
|
|
5760
|
+
async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
|
|
5617
5761
|
const controller = new AbortController();
|
|
5618
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
5762
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
5619
5763
|
try {
|
|
5620
5764
|
const response = await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, {
|
|
5621
5765
|
...options,
|
|
@@ -9329,11 +9473,11 @@ var WALLET_DIR = path4.join(os3.homedir(), ".blockrun");
|
|
|
9329
9473
|
var SOLANA_WALLET_FILE = path4.join(WALLET_DIR, ".solana-session");
|
|
9330
9474
|
async function createSolanaWallet() {
|
|
9331
9475
|
const { Keypair } = await loadSolanaWeb3("Solana wallet operations");
|
|
9332
|
-
const
|
|
9476
|
+
const bs583 = await import("bs58");
|
|
9333
9477
|
const keypair = Keypair.generate();
|
|
9334
9478
|
return {
|
|
9335
9479
|
address: keypair.publicKey.toBase58(),
|
|
9336
|
-
privateKey: (
|
|
9480
|
+
privateKey: (bs583.default ?? bs583).encode(keypair.secretKey)
|
|
9337
9481
|
};
|
|
9338
9482
|
}
|
|
9339
9483
|
async function solanaKeyToBytes(privateKey) {
|
|
@@ -9371,8 +9515,8 @@ async function solanaKeyToBytes(privateKey) {
|
|
|
9371
9515
|
return bytes;
|
|
9372
9516
|
}
|
|
9373
9517
|
try {
|
|
9374
|
-
const
|
|
9375
|
-
const bytes = (
|
|
9518
|
+
const bs583 = await import("bs58");
|
|
9519
|
+
const bytes = (bs583.default ?? bs583).decode(key);
|
|
9376
9520
|
if (bytes.length !== 64) {
|
|
9377
9521
|
throw new Error(`Invalid Solana key length: expected 64 bytes, got ${bytes.length}`);
|
|
9378
9522
|
}
|
|
@@ -9551,15 +9695,56 @@ async function getOrCreateSolanaWallet() {
|
|
|
9551
9695
|
return { address, privateKey, isNew: true };
|
|
9552
9696
|
}
|
|
9553
9697
|
|
|
9698
|
+
// src/poll-ownership.ts
|
|
9699
|
+
import { createPrivateKey, createPublicKey, sign as ed25519Sign } from "crypto";
|
|
9700
|
+
import bs58 from "bs58";
|
|
9701
|
+
var POLL_AUTH_HEADERS = {
|
|
9702
|
+
wallet: "x-poll-wallet",
|
|
9703
|
+
timestamp: "x-poll-timestamp",
|
|
9704
|
+
signature: "x-poll-signature"
|
|
9705
|
+
};
|
|
9706
|
+
function pollAuthMessage(kind, jobId, timestampS) {
|
|
9707
|
+
return `blockrun-poll:v1:${kind}:${jobId}:${timestampS}`;
|
|
9708
|
+
}
|
|
9709
|
+
function pollJobIdFromUrl(pollUrl) {
|
|
9710
|
+
const path7 = new URL(pollUrl, "https://placeholder.invalid").pathname.replace(/\/+$/, "");
|
|
9711
|
+
const segment = path7.slice(path7.lastIndexOf("/") + 1);
|
|
9712
|
+
if (!segment) throw new Error(`poll_url has no job id segment: ${pollUrl}`);
|
|
9713
|
+
return decodeURIComponent(segment);
|
|
9714
|
+
}
|
|
9715
|
+
var ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
9716
|
+
function signPollOwnership(secretKey, kind, jobId, nowMs = Date.now()) {
|
|
9717
|
+
if (secretKey.length !== 64 && secretKey.length !== 32) {
|
|
9718
|
+
throw new Error(`Solana secret key must be 64 bytes (or a 32-byte seed), got ${secretKey.length}`);
|
|
9719
|
+
}
|
|
9720
|
+
const seed = Buffer.from(secretKey.subarray(0, 32));
|
|
9721
|
+
const privateKey = createPrivateKey({
|
|
9722
|
+
key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]),
|
|
9723
|
+
format: "der",
|
|
9724
|
+
type: "pkcs8"
|
|
9725
|
+
});
|
|
9726
|
+
const jwk = createPublicKey(privateKey).export({ format: "jwk" });
|
|
9727
|
+
if (!jwk.x) throw new Error("Could not derive the Ed25519 public key");
|
|
9728
|
+
const publicKey = Buffer.from(jwk.x, "base64url");
|
|
9729
|
+
const timestampS = Math.floor(nowMs / 1e3);
|
|
9730
|
+
const signature = ed25519Sign(null, Buffer.from(pollAuthMessage(kind, jobId, timestampS), "utf8"), privateKey);
|
|
9731
|
+
return {
|
|
9732
|
+
[POLL_AUTH_HEADERS.wallet]: bs58.encode(publicKey),
|
|
9733
|
+
[POLL_AUTH_HEADERS.timestamp]: String(timestampS),
|
|
9734
|
+
[POLL_AUTH_HEADERS.signature]: bs58.encode(signature)
|
|
9735
|
+
};
|
|
9736
|
+
}
|
|
9737
|
+
|
|
9554
9738
|
// src/solana-batch.ts
|
|
9555
9739
|
import { AsyncLocalStorage } from "async_hooks";
|
|
9556
9740
|
import { randomUUID } from "crypto";
|
|
9557
9741
|
import * as fs6 from "fs";
|
|
9558
9742
|
import * as path5 from "path";
|
|
9559
|
-
import
|
|
9743
|
+
import bs582 from "bs58";
|
|
9560
9744
|
var BATCH_SCHEME = "batch-settlement";
|
|
9561
9745
|
var BLOCKRUN_SOL_OPERATOR = "5YKPQUFjw5WQqhSUkEGKNNfYYVqnRRNbpYyL71qQ1vm3";
|
|
9562
9746
|
var DEFAULT_BATCH_RATE_LIMIT = { maxAttempts: 3, maxWaitMs: 6e4 };
|
|
9747
|
+
var DEFAULT_BATCH_IN_DOUBT = { maxWaitMs: 3e5 };
|
|
9563
9748
|
var BatchPaymentUnresolvedError = class extends PaymentError {
|
|
9564
9749
|
/** The {@link SolanaBatchEvent.reason} of the `unresolved` event reported with it. */
|
|
9565
9750
|
reason;
|
|
@@ -10116,7 +10301,7 @@ function decodeChannelAccount(owner, data) {
|
|
|
10116
10301
|
return void 0;
|
|
10117
10302
|
}
|
|
10118
10303
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
10119
|
-
const key = (offset) =>
|
|
10304
|
+
const key = (offset) => bs582.encode(data.subarray(offset, offset + 32));
|
|
10120
10305
|
return {
|
|
10121
10306
|
deposit: view.getBigUint64(CHANNEL.deposit, true),
|
|
10122
10307
|
settled: view.getBigUint64(CHANNEL.settled, true),
|
|
@@ -10236,6 +10421,46 @@ function withoutClientSignedBatch(paymentRequired) {
|
|
|
10236
10421
|
const accepts = paymentRequired.accepts ?? [];
|
|
10237
10422
|
return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
|
|
10238
10423
|
}
|
|
10424
|
+
var PAYMENT_IDENTIFIER = "payment-identifier";
|
|
10425
|
+
function withPaymentIdentifier(paymentRequired) {
|
|
10426
|
+
const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
|
|
10427
|
+
if (typeof declared?.info?.required !== "boolean") return paymentRequired;
|
|
10428
|
+
const id = `pay_${randomUUID().replace(/-/g, "")}`;
|
|
10429
|
+
return {
|
|
10430
|
+
...paymentRequired,
|
|
10431
|
+
extensions: { ...paymentRequired.extensions, [PAYMENT_IDENTIFIER]: { ...declared, info: { ...declared.info, id } } }
|
|
10432
|
+
};
|
|
10433
|
+
}
|
|
10434
|
+
function peekReceipt(http, getHeader) {
|
|
10435
|
+
try {
|
|
10436
|
+
return http.getPaymentSettleResponse(getHeader);
|
|
10437
|
+
} catch {
|
|
10438
|
+
return void 0;
|
|
10439
|
+
}
|
|
10440
|
+
}
|
|
10441
|
+
function receiptSucceeded(http, answer) {
|
|
10442
|
+
return peekReceipt(http, (name) => answer.headers.get(name))?.success === true;
|
|
10443
|
+
}
|
|
10444
|
+
function hasPaymentIdentifier(payload) {
|
|
10445
|
+
const info = payload.extensions?.[PAYMENT_IDENTIFIER]?.info;
|
|
10446
|
+
return typeof info?.id === "string";
|
|
10447
|
+
}
|
|
10448
|
+
var IN_DOUBT_MIN_WAIT_MS = 1e3;
|
|
10449
|
+
var IN_DOUBT_MAX_WAIT_MS = 3e4;
|
|
10450
|
+
function replayMayResolve(doubt) {
|
|
10451
|
+
if (doubt.unanswered) return true;
|
|
10452
|
+
const answer = doubt.held;
|
|
10453
|
+
if (!answer) return false;
|
|
10454
|
+
return answer.status >= 500 || answer.status === 429 || answer.status === 409 && Boolean(answer.headers.get("retry-after")?.trim());
|
|
10455
|
+
}
|
|
10456
|
+
function inDoubtWait(doubt, replay) {
|
|
10457
|
+
const asked = doubt.held ? parseRetryAfter(doubt.held.headers.get("retry-after")) : void 0;
|
|
10458
|
+
return Math.min(IN_DOUBT_MAX_WAIT_MS, Math.max(IN_DOUBT_MIN_WAIT_MS, asked ?? backoffDelay(replay)));
|
|
10459
|
+
}
|
|
10460
|
+
function replayDoubt(outcome) {
|
|
10461
|
+
if (outcome.kind === "charged" || outcome.kind === "in_doubt") return outcome;
|
|
10462
|
+
return { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
|
|
10463
|
+
}
|
|
10239
10464
|
function trustedBatchAccepts(paymentRequired, operators) {
|
|
10240
10465
|
return (paymentRequired.accepts ?? []).filter((candidate) => {
|
|
10241
10466
|
const extra = candidate.extra;
|
|
@@ -10245,9 +10470,6 @@ function trustedBatchAccepts(paymentRequired, operators) {
|
|
|
10245
10470
|
function trustedBatchAccept(paymentRequired, operators) {
|
|
10246
10471
|
return trustedBatchAccepts(paymentRequired, operators)[0];
|
|
10247
10472
|
}
|
|
10248
|
-
function atomicToUsd(amount) {
|
|
10249
|
-
return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
|
|
10250
|
-
}
|
|
10251
10473
|
function chargedUsd(getReceipt) {
|
|
10252
10474
|
let receipt;
|
|
10253
10475
|
try {
|
|
@@ -10255,7 +10477,7 @@ function chargedUsd(getReceipt) {
|
|
|
10255
10477
|
} catch {
|
|
10256
10478
|
return 0;
|
|
10257
10479
|
}
|
|
10258
|
-
return
|
|
10480
|
+
return receiptChargedUsd(receipt) ?? 0;
|
|
10259
10481
|
}
|
|
10260
10482
|
function rebuiltCharge(settled, payload) {
|
|
10261
10483
|
if (settled?.success !== true) return 0n;
|
|
@@ -10375,13 +10597,42 @@ function parseRetryAfter(value, now = Date.now()) {
|
|
|
10375
10597
|
}
|
|
10376
10598
|
return Number.isFinite(ms) && ms <= MAX_RETRY_AFTER_MS ? ms : void 0;
|
|
10377
10599
|
}
|
|
10378
|
-
var
|
|
10600
|
+
var NOT_BROADCAST_REFUSALS = /* @__PURE__ */ new Set([
|
|
10601
|
+
// 429
|
|
10379
10602
|
"batch_account_channel_capacity_exhausted",
|
|
10380
10603
|
"batch_channel_capacity_exhausted",
|
|
10381
|
-
"batch_deposit_rate_limited"
|
|
10604
|
+
"batch_deposit_rate_limited",
|
|
10605
|
+
"batch_deposit_ip_rate_limited",
|
|
10606
|
+
// 503
|
|
10607
|
+
"batch_signer_funding_headroom_exhausted",
|
|
10608
|
+
"batch_new_channels_disabled",
|
|
10609
|
+
"batch_public_new_channels_disabled",
|
|
10610
|
+
// 403
|
|
10611
|
+
"batch_account_admission_paused",
|
|
10612
|
+
"batch_channel_owner_mismatch",
|
|
10613
|
+
"batch_fee_payer_unavailable",
|
|
10614
|
+
"batch_fee_payer_not_dedicated",
|
|
10615
|
+
"batch_fee_payer_lane_not_authorized",
|
|
10616
|
+
"batch_transaction_fee_payer_mismatch",
|
|
10617
|
+
// 400
|
|
10618
|
+
"batch_initial_deposit_out_of_range",
|
|
10619
|
+
"batch_deposit_asset_not_supported",
|
|
10620
|
+
"batch_withdraw_delay_out_of_range"
|
|
10382
10621
|
]);
|
|
10383
10622
|
function provesNothingBroadcast(receipt) {
|
|
10384
|
-
return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" &&
|
|
10623
|
+
return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_REFUSALS.has(receipt.errorReason);
|
|
10624
|
+
}
|
|
10625
|
+
var CONFIG_REFUSALS = /* @__PURE__ */ new Set([
|
|
10626
|
+
"batch_initial_deposit_out_of_range",
|
|
10627
|
+
"batch_deposit_asset_not_supported",
|
|
10628
|
+
"batch_withdraw_delay_out_of_range"
|
|
10629
|
+
]);
|
|
10630
|
+
var REFUSAL_COOLDOWN_MS = 6e4;
|
|
10631
|
+
var CONFIG_REFUSAL_COOLDOWN_MS = 10 * 6e4;
|
|
10632
|
+
var REFUSAL_COOLDOWN_MAX_MS = 15 * 6e4;
|
|
10633
|
+
function refusalCooldownMs(errorReason, retryAfterMs2) {
|
|
10634
|
+
if (retryAfterMs2 !== void 0) return Math.min(retryAfterMs2, REFUSAL_COOLDOWN_MAX_MS);
|
|
10635
|
+
return errorReason !== void 0 && CONFIG_REFUSALS.has(errorReason) ? CONFIG_REFUSAL_COOLDOWN_MS : REFUSAL_COOLDOWN_MS;
|
|
10385
10636
|
}
|
|
10386
10637
|
function backoffDelay(attempt) {
|
|
10387
10638
|
const base = 1e3 * 2 ** Math.max(0, attempt - 1);
|
|
@@ -10514,7 +10765,7 @@ function intentTarget(intent) {
|
|
|
10514
10765
|
var SolanaBatchPayer = class {
|
|
10515
10766
|
constructor(init) {
|
|
10516
10767
|
this.init = init;
|
|
10517
|
-
const { operators, rateLimit } = init.options;
|
|
10768
|
+
const { operators, rateLimit, inDoubt } = init.options;
|
|
10518
10769
|
if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
|
|
10519
10770
|
throw new Error("batch.operators must list at least one base58 operator public key");
|
|
10520
10771
|
}
|
|
@@ -10526,11 +10777,16 @@ var SolanaBatchPayer = class {
|
|
|
10526
10777
|
if (!Number.isFinite(this.maxWaitMs) || this.maxWaitMs < 0) {
|
|
10527
10778
|
throw new Error("batch.rateLimit.maxWaitMs must be a finite number of ms >= 0");
|
|
10528
10779
|
}
|
|
10780
|
+
this.inDoubtMaxWaitMs = inDoubt?.maxWaitMs ?? DEFAULT_BATCH_IN_DOUBT.maxWaitMs;
|
|
10781
|
+
if (!Number.isFinite(this.inDoubtMaxWaitMs) || this.inDoubtMaxWaitMs < 0) {
|
|
10782
|
+
throw new Error("batch.inDoubt.maxWaitMs must be a finite number of ms >= 0");
|
|
10783
|
+
}
|
|
10529
10784
|
}
|
|
10530
10785
|
init;
|
|
10531
10786
|
state;
|
|
10532
10787
|
maxAttempts;
|
|
10533
10788
|
maxWaitMs;
|
|
10789
|
+
inDoubtMaxWaitMs;
|
|
10534
10790
|
counters = {
|
|
10535
10791
|
fallbacks: 0,
|
|
10536
10792
|
fallbacksByReason: {},
|
|
@@ -10899,14 +11155,19 @@ var SolanaBatchPayer = class {
|
|
|
10899
11155
|
* the call's result (`served`, nothing paid); one that fails otherwise
|
|
10900
11156
|
* is the call's error (`failed`, `"unpaid"`), never an exact payment
|
|
10901
11157
|
* against the stale challenge;
|
|
10902
|
-
* - in doubt: ends only with a definitive success receipt from
|
|
10903
|
-
*
|
|
10904
|
-
*
|
|
10905
|
-
*
|
|
10906
|
-
*
|
|
11158
|
+
* - in doubt: ends only with a definitive success receipt from a replay
|
|
11159
|
+
* of the identical payment ({@link resolveInDoubt}: the one replay a
|
|
11160
|
+
* receipt-less 429 is allowed, or the replays of a payment carrying a
|
|
11161
|
+
* payment identifier while the gateway says one can succeed, within
|
|
11162
|
+
* `inDoubt.maxWaitMs`), or a thrown {@link BatchPaymentUnresolvedError}.
|
|
11163
|
+
* No new authorization, no new deposit, no exact payment and no fallback
|
|
11164
|
+
* model is ever paid for a call in doubt.
|
|
10907
11165
|
*
|
|
10908
11166
|
* The wallet stays in cooldown after a 429, so its other calls wait
|
|
10909
|
-
* instead of sending opens of their own.
|
|
11167
|
+
* instead of sending opens of their own. After a deposit the facilitator
|
|
11168
|
+
* refused before broadcasting it (a 402 with a failed `batch_*` admission
|
|
11169
|
+
* receipt), its calls do not wait: they pay exact at once
|
|
11170
|
+
* (`admission_refused`) until the refusal cooldown ends.
|
|
10910
11171
|
*
|
|
10911
11172
|
* @param closesAtStart - {@link closeFence}, taken before the call's first
|
|
10912
11173
|
* (unpaid) request. If a close completed since, the call pays exact
|
|
@@ -10970,6 +11231,15 @@ var SolanaBatchPayer = class {
|
|
|
10970
11231
|
try {
|
|
10971
11232
|
for (; ; ) {
|
|
10972
11233
|
const now = Date.now();
|
|
11234
|
+
if ((wallet.refusedUntil ?? 0) > now) {
|
|
11235
|
+
return fallback({
|
|
11236
|
+
reason: "admission_refused",
|
|
11237
|
+
errorReason: wallet.refusedFor,
|
|
11238
|
+
retryAfterMs: (wallet.refusedUntil ?? 0) - now,
|
|
11239
|
+
attempt: budget.attempt,
|
|
11240
|
+
detail: `the facilitator refused this wallet's last batch deposit (${wallet.refusedFor ?? "unknown"}); batch is skipped until the refusal cooldown ends`
|
|
11241
|
+
});
|
|
11242
|
+
}
|
|
10973
11243
|
if (wallet.cooldownUntil > now && wallet.cooldownUntil !== waitedUntil) {
|
|
10974
11244
|
const wait = wallet.cooldownUntil - now;
|
|
10975
11245
|
if (budget.waited + wait > this.maxWaitMs) {
|
|
@@ -11059,12 +11329,23 @@ var SolanaBatchPayer = class {
|
|
|
11059
11329
|
if (built.kind !== "authorization") {
|
|
11060
11330
|
if (outcome.depositSafe) this.forgetIntent(wallet, built.key);
|
|
11061
11331
|
else this.distrust(wallet, built, "deposit_refused");
|
|
11332
|
+
if (outcome.refusedForMs !== void 0) {
|
|
11333
|
+
const until = Date.now() + outcome.refusedForMs;
|
|
11334
|
+
if (until > (wallet.refusedUntil ?? 0)) {
|
|
11335
|
+
wallet.refusedUntil = until;
|
|
11336
|
+
wallet.refusedFor = outcome.errorReason;
|
|
11337
|
+
}
|
|
11338
|
+
}
|
|
11062
11339
|
}
|
|
11063
11340
|
sent = void 0;
|
|
11064
11341
|
release();
|
|
11065
11342
|
if (!outcome.rateLimited) {
|
|
11066
|
-
const { kind: _kind, rateLimited: _limited, depositSafe: _safe, ...event } = outcome;
|
|
11067
|
-
return fallback({
|
|
11343
|
+
const { kind: _kind, rateLimited: _limited, depositSafe: _safe, refusedForMs, ...event } = outcome;
|
|
11344
|
+
return fallback({
|
|
11345
|
+
...event,
|
|
11346
|
+
...refusedForMs !== void 0 && built.kind !== "authorization" ? { retryAfterMs: refusedForMs } : {},
|
|
11347
|
+
attempt: budget.attempt
|
|
11348
|
+
});
|
|
11068
11349
|
}
|
|
11069
11350
|
const wait = this.coolDown(wallet, outcome.rateLimited.retryAfterMs ?? backoffDelay(budget.attempt));
|
|
11070
11351
|
if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
|
|
@@ -11110,7 +11391,7 @@ var SolanaBatchPayer = class {
|
|
|
11110
11391
|
if (built.kind !== "authorization" && !(built.key && wallet.resyncs.has(built.key))) this.forgetIntent(wallet, built.key);
|
|
11111
11392
|
sent = void 0;
|
|
11112
11393
|
release();
|
|
11113
|
-
await this.report({ type: "recovered", reason: "rate_limited", attempt: budget.attempt });
|
|
11394
|
+
await this.report({ type: "recovered", reason: outcome.status === 429 ? "rate_limited" : "in_doubt", attempt: budget.attempt });
|
|
11114
11395
|
return { kind: "paid", response: resolved.response, chargedUsd: resolved.chargedUsd };
|
|
11115
11396
|
}
|
|
11116
11397
|
return await this.raiseUnresolved(wallet, built, resolved, budget.attempt);
|
|
@@ -11159,21 +11440,24 @@ var SolanaBatchPayer = class {
|
|
|
11159
11440
|
/**
|
|
11160
11441
|
* Try to end a payment in doubt with the gateway's own word.
|
|
11161
11442
|
*
|
|
11162
|
-
* The one source of evidence this SDK accepts
|
|
11163
|
-
*
|
|
11164
|
-
*
|
|
11165
|
-
*
|
|
11166
|
-
*
|
|
11167
|
-
*
|
|
11443
|
+
* The one source of evidence this SDK accepts is a definitive success
|
|
11444
|
+
* receipt on a byte-identical replay of the payment. Any other answer to a
|
|
11445
|
+
* replay (a 402 or `duplicate_settlement`, which mean the original reached
|
|
11446
|
+
* the gateway; a 409; another 429; a 5xx; a timeout) leaves it in doubt.
|
|
11447
|
+
*
|
|
11448
|
+
* - A payment carrying a payment identifier is replayed for as long as
|
|
11449
|
+
* the gateway says a later replay can succeed ({@link replayInDoubt}).
|
|
11450
|
+
* - Any other payment gets a replay only after a 429 without a receipt:
|
|
11451
|
+
* one, after its backoff (the owner's bounded safety attempt), within
|
|
11452
|
+
* `rateLimit`. Every other in-doubt outcome is never replayed.
|
|
11168
11453
|
*
|
|
11169
11454
|
* Chain state is not evidence here: it can settle a deposit (see
|
|
11170
|
-
* `resync`), never whether the gateway charged.
|
|
11171
|
-
* receipt on every response, a request-status endpoint with a fence)
|
|
11172
|
-
* plugs in here, before the caller raises.
|
|
11455
|
+
* `resync`), never whether the gateway charged.
|
|
11173
11456
|
*
|
|
11174
11457
|
* @returns the charged replay, or the doubt to raise.
|
|
11175
11458
|
*/
|
|
11176
11459
|
async resolveInDoubt(wallet, sent, doubt, send, budget) {
|
|
11460
|
+
if (hasPaymentIdentifier(sent.payload)) return this.replayInDoubt(wallet, sent, doubt, send, budget.attempt);
|
|
11177
11461
|
if (!doubt.replayable) return doubt;
|
|
11178
11462
|
const wait = this.coolDown(wallet, doubt.retryAfterMs ?? backoffDelay(budget.attempt));
|
|
11179
11463
|
if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
|
|
@@ -11193,9 +11477,94 @@ var SolanaBatchPayer = class {
|
|
|
11193
11477
|
budget.waited += wait;
|
|
11194
11478
|
await sleep2(wait);
|
|
11195
11479
|
budget.attempt += 1;
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11480
|
+
return replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
|
|
11481
|
+
}
|
|
11482
|
+
/**
|
|
11483
|
+
* Replay a payment in doubt that carries a payment identifier until the
|
|
11484
|
+
* gateway's answer settles it.
|
|
11485
|
+
*
|
|
11486
|
+
* A gateway that declares `payment-identifier` stores each paid response
|
|
11487
|
+
* under its identifier and answers a byte-identical replay of a completed
|
|
11488
|
+
* request with it, the original receipt included; while the original is
|
|
11489
|
+
* still running, it answers 409 with a `Retry-After`. So the identical
|
|
11490
|
+
* payment (the same `PAYMENT-SIGNATURE` header and the same body) is
|
|
11491
|
+
* replayed for as long as the last answer says a later one can succeed
|
|
11492
|
+
* ({@link replayMayResolve}), after {@link inDoubtWait} each time, as long
|
|
11493
|
+
* as the wait fits in `inDoubt.maxWaitMs`, counted from the first doubt:
|
|
11494
|
+
* every wait (as long as it really took) and every replay's round trip. A
|
|
11495
|
+
* replay already sent is not cut short. These replays do not count
|
|
11496
|
+
* against `rateLimit`; a 429 among them still puts the wallet in cooldown.
|
|
11497
|
+
*
|
|
11498
|
+
* Every answer that does not settle it is held back from the scheme
|
|
11499
|
+
* ({@link InDoubt.held}) and handed over only once no replay follows, so
|
|
11500
|
+
* the scheme rolls the payment back once, or takes the receipt that answer
|
|
11501
|
+
* carries. That is the last answer, unless an earlier one carried a
|
|
11502
|
+
* success receipt and the later ones do not: a success receipt is the
|
|
11503
|
+
* gateway's word that it charged, and handing the scheme a receipt-less
|
|
11504
|
+
* refusal of a replay instead would roll a real charge back and desync the
|
|
11505
|
+
* channel's cumulative.
|
|
11506
|
+
*
|
|
11507
|
+
* @returns the charged replay, or the doubt to raise.
|
|
11508
|
+
*/
|
|
11509
|
+
async replayInDoubt(wallet, sent, first, send, attempt) {
|
|
11510
|
+
let doubt = first;
|
|
11511
|
+
let held = first.held;
|
|
11512
|
+
let heldSucceeded = held !== void 0 && receiptSucceeded(sent.http, held);
|
|
11513
|
+
let replays = 0;
|
|
11514
|
+
let spent = 0;
|
|
11515
|
+
const stop = async (note) => {
|
|
11516
|
+
if (!doubt.settled && held) {
|
|
11517
|
+
const answer = held;
|
|
11518
|
+
await this.settle(sent.http, sent.payload, (name) => answer.headers.get(name), answer.status);
|
|
11519
|
+
}
|
|
11520
|
+
const notes = [replays > 0 ? `${replays} replay${replays === 1 ? "" : "s"} did not resolve it` : "", note ?? ""];
|
|
11521
|
+
return {
|
|
11522
|
+
...doubt,
|
|
11523
|
+
settled: doubt.settled || held !== void 0,
|
|
11524
|
+
held: void 0,
|
|
11525
|
+
firstReason: first.reason,
|
|
11526
|
+
detail: [doubt.detail, ...notes].filter(Boolean).join("; ")
|
|
11527
|
+
};
|
|
11528
|
+
};
|
|
11529
|
+
for (; ; ) {
|
|
11530
|
+
if (!replayMayResolve(doubt)) {
|
|
11531
|
+
return stop(doubt.held?.status === 409 ? "the gateway sent no Retry-After, so no replay can resolve it" : void 0);
|
|
11532
|
+
}
|
|
11533
|
+
const wait = inDoubtWait(doubt, replays + 1);
|
|
11534
|
+
const status2 = doubt.held?.status;
|
|
11535
|
+
if (status2 === 429) this.coolDown(wallet, (doubt.held && parseRetryAfter(doubt.held.headers.get("retry-after"))) ?? wait);
|
|
11536
|
+
if (spent + wait > this.inDoubtMaxWaitMs) {
|
|
11537
|
+
return stop(`batch.inDoubt.maxWaitMs (${this.inDoubtMaxWaitMs} ms) leaves no room for ${replays > 0 ? "another replay" : "a replay"}`);
|
|
11538
|
+
}
|
|
11539
|
+
await this.report({
|
|
11540
|
+
type: "backoff",
|
|
11541
|
+
reason: status2 === 429 ? "rate_limited" : "in_doubt",
|
|
11542
|
+
status: status2,
|
|
11543
|
+
errorReason: doubt.errorReason,
|
|
11544
|
+
retryAfterMs: wait,
|
|
11545
|
+
attempt,
|
|
11546
|
+
detail: `replay ${replays + 1} of the payment in doubt`
|
|
11547
|
+
});
|
|
11548
|
+
const sleptFrom = Date.now();
|
|
11549
|
+
await sleep2(wait);
|
|
11550
|
+
spent += Math.max(wait, Date.now() - sleptFrom);
|
|
11551
|
+
replays += 1;
|
|
11552
|
+
const startedAt = Date.now();
|
|
11553
|
+
const replayed = replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
|
|
11554
|
+
spent += Math.max(0, Date.now() - startedAt);
|
|
11555
|
+
if (replayed.kind === "charged") return replayed;
|
|
11556
|
+
doubt = replayed;
|
|
11557
|
+
if (doubt.held) {
|
|
11558
|
+
const succeeded = receiptSucceeded(sent.http, doubt.held);
|
|
11559
|
+
if (succeeded || !heldSucceeded) {
|
|
11560
|
+
held = doubt.held;
|
|
11561
|
+
heldSucceeded = succeeded;
|
|
11562
|
+
}
|
|
11563
|
+
} else if (doubt.settled) {
|
|
11564
|
+
held = void 0;
|
|
11565
|
+
heldSucceeded = false;
|
|
11566
|
+
}
|
|
11567
|
+
}
|
|
11199
11568
|
}
|
|
11200
11569
|
/**
|
|
11201
11570
|
* Raise a payment in doubt: release the scheme's hold on it (the channel
|
|
@@ -11206,10 +11575,11 @@ var SolanaBatchPayer = class {
|
|
|
11206
11575
|
async raiseUnresolved(wallet, sent, doubt, attempt) {
|
|
11207
11576
|
if (!doubt.settled) await this.settle(sent.http, sent.payload, () => null, 0);
|
|
11208
11577
|
if (sent.kind !== "authorization") {
|
|
11578
|
+
const reason = doubt.firstReason ?? doubt.reason;
|
|
11209
11579
|
this.distrust(
|
|
11210
11580
|
wallet,
|
|
11211
11581
|
sent,
|
|
11212
|
-
|
|
11582
|
+
reason === "no_response" ? "deposit_unanswered" : reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
|
|
11213
11583
|
);
|
|
11214
11584
|
}
|
|
11215
11585
|
await this.report({
|
|
@@ -11296,8 +11666,8 @@ var SolanaBatchPayer = class {
|
|
|
11296
11666
|
* here, and only for a first send.
|
|
11297
11667
|
*
|
|
11298
11668
|
* @param retry - count this send in `retries` (it follows this call's own 429).
|
|
11299
|
-
* @param replay - this is
|
|
11300
|
-
*
|
|
11669
|
+
* @param replay - this is a replay of a payment in doubt: only a 2xx with
|
|
11670
|
+
* a success receipt counts, everything else stays in doubt.
|
|
11301
11671
|
*/
|
|
11302
11672
|
async sendOnce(wallet, sent, send, retry, replay) {
|
|
11303
11673
|
if (retry) this.counters.retries += 1;
|
|
@@ -11311,6 +11681,7 @@ var SolanaBatchPayer = class {
|
|
|
11311
11681
|
reason: replay ? "replay_unresolved" : "no_response",
|
|
11312
11682
|
replayable: false,
|
|
11313
11683
|
settled: false,
|
|
11684
|
+
unanswered: true,
|
|
11314
11685
|
detail: `${replay ? "the replay" : "the payment"} got no answer (${err instanceof Error ? `${err.name}: ${err.message}` : String(err)})`,
|
|
11315
11686
|
cause: err
|
|
11316
11687
|
};
|
|
@@ -11335,6 +11706,7 @@ var SolanaBatchPayer = class {
|
|
|
11335
11706
|
const getHeader = (name) => response.headers.get(name);
|
|
11336
11707
|
const receipted = Boolean(getHeader("PAYMENT-RESPONSE") || getHeader("X-PAYMENT-RESPONSE"));
|
|
11337
11708
|
const status2 = response.status;
|
|
11709
|
+
const hold = hasPaymentIdentifier(payload) && (!response.ok || replay && peekReceipt(http, getHeader)?.success !== true);
|
|
11338
11710
|
if (status2 === 429 && !receipted) {
|
|
11339
11711
|
return {
|
|
11340
11712
|
kind: "in_doubt",
|
|
@@ -11344,12 +11716,27 @@ var SolanaBatchPayer = class {
|
|
|
11344
11716
|
status: status2,
|
|
11345
11717
|
retryAfterMs: parseRetryAfter(getHeader("retry-after")),
|
|
11346
11718
|
errorReason: await rateLimitReason(response, void 0),
|
|
11347
|
-
detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt"
|
|
11719
|
+
detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt",
|
|
11720
|
+
...hold ? { held: response } : {}
|
|
11348
11721
|
};
|
|
11349
11722
|
}
|
|
11723
|
+
const outcome = await this.classifyAnswer(wallet, sent, response, replay, hold);
|
|
11724
|
+
if (!hold) return outcome;
|
|
11725
|
+
if (outcome.kind === "in_doubt") return { ...outcome, settled: false, held: response };
|
|
11726
|
+
await this.settle(http, payload, getHeader, status2);
|
|
11727
|
+
return outcome;
|
|
11728
|
+
}
|
|
11729
|
+
/**
|
|
11730
|
+
* {@link classify}, past a receipt-less 429. With `hold`, the answer is not
|
|
11731
|
+
* handed to the scheme: its receipt is only read.
|
|
11732
|
+
*/
|
|
11733
|
+
async classifyAnswer(wallet, sent, response, replay, hold) {
|
|
11734
|
+
const { http, payload } = sent;
|
|
11735
|
+
const getHeader = (name) => response.headers.get(name);
|
|
11736
|
+
const status2 = response.status;
|
|
11350
11737
|
const channelId = payloadChannelId(payload);
|
|
11351
11738
|
const before = channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0;
|
|
11352
|
-
const reconciled = await this.settle(http, payload, getHeader, status2);
|
|
11739
|
+
const reconciled = hold ? { settleResponse: peekReceipt(http, getHeader), ok: false } : await this.settle(http, payload, getHeader, status2);
|
|
11353
11740
|
const receipt = reconciled.settleResponse;
|
|
11354
11741
|
const definitive = receipt?.success === true && reconciled.ok && recordAdvanced(before, channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0, payload, receipt);
|
|
11355
11742
|
if (!response.ok && !replay && sent.kind === "authorization" && cancelledReceipt(receipt)) {
|
|
@@ -11412,7 +11799,15 @@ var SolanaBatchPayer = class {
|
|
|
11412
11799
|
cause: await afterPaymentError(response)
|
|
11413
11800
|
};
|
|
11414
11801
|
}
|
|
11415
|
-
|
|
11802
|
+
const refused = provesNothingBroadcast(receipt);
|
|
11803
|
+
return {
|
|
11804
|
+
kind: "not_charged",
|
|
11805
|
+
reason: "payment_required",
|
|
11806
|
+
status: status2,
|
|
11807
|
+
errorReason: receipt?.errorReason,
|
|
11808
|
+
depositSafe: refused,
|
|
11809
|
+
...refused ? { refusedForMs: refusalCooldownMs(receipt?.errorReason, parseRetryAfter(getHeader("retry-after"))) } : {}
|
|
11810
|
+
};
|
|
11416
11811
|
}
|
|
11417
11812
|
const refusal = await batchRefusal(response);
|
|
11418
11813
|
if (refusal) {
|
|
@@ -11443,7 +11838,7 @@ var SolanaBatchPayer = class {
|
|
|
11443
11838
|
const http = (await this.build(wallet)).http;
|
|
11444
11839
|
try {
|
|
11445
11840
|
await this.prepareTopUp(wallet, paymentRequired);
|
|
11446
|
-
return { http, payload: await http.createPaymentPayload(withoutClientSignedBatch(paymentRequired)) };
|
|
11841
|
+
return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(withoutClientSignedBatch(paymentRequired))) };
|
|
11447
11842
|
} catch (err) {
|
|
11448
11843
|
if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
|
|
11449
11844
|
} finally {
|
|
@@ -11536,13 +11931,7 @@ var SolanaBatchPayer = class {
|
|
|
11536
11931
|
console.error(
|
|
11537
11932
|
`[@blockrun/llm] batch-settlement receipt not reconciled: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
|
|
11538
11933
|
);
|
|
11539
|
-
|
|
11540
|
-
try {
|
|
11541
|
-
settleResponse = http.getPaymentSettleResponse(getHeader);
|
|
11542
|
-
} catch {
|
|
11543
|
-
settleResponse = void 0;
|
|
11544
|
-
}
|
|
11545
|
-
return { settleResponse, ok: false };
|
|
11934
|
+
return { settleResponse: peekReceipt(http, getHeader), ok: false };
|
|
11546
11935
|
}
|
|
11547
11936
|
}
|
|
11548
11937
|
/**
|
|
@@ -11676,6 +12065,39 @@ var DEFAULT_MAX_TOKENS2 = 1024;
|
|
|
11676
12065
|
var DEFAULT_TIMEOUT14 = 6e4;
|
|
11677
12066
|
var STALE_BLOCKHASH_RETRY_BACKOFFS_MS = [500, 2e3];
|
|
11678
12067
|
var MAX_PAYMENT_FAILURE_BYTES = 64 * 1024;
|
|
12068
|
+
var DEFAULT_VIDEO_MODEL = "xai/grok-imagine-video";
|
|
12069
|
+
var DEFAULT_MUSIC_MODEL = "minimax/music-2.5+";
|
|
12070
|
+
var DEFAULT_VIDEO_BUDGET_MS = 9e5;
|
|
12071
|
+
var DEFAULT_MUSIC_BUDGET_MS = 3e5;
|
|
12072
|
+
var VIDEO_POLL_INTERVAL_MS = 5e3;
|
|
12073
|
+
var MAX_PAID_POLL_REFUSALS = 4;
|
|
12074
|
+
var MAX_RETRY_AFTER_MS2 = 12e4;
|
|
12075
|
+
var MEDIA_SUBMIT_TIMEOUT_MS = 12e4;
|
|
12076
|
+
function sleep3(ms) {
|
|
12077
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
12078
|
+
}
|
|
12079
|
+
async function readJsonOrEmpty(response) {
|
|
12080
|
+
try {
|
|
12081
|
+
const parsed = await response.json();
|
|
12082
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
12083
|
+
} catch {
|
|
12084
|
+
return {};
|
|
12085
|
+
}
|
|
12086
|
+
}
|
|
12087
|
+
function isPendingPollStatus(status2) {
|
|
12088
|
+
return status2 === 202 || status2 === 503 || status2 === 504;
|
|
12089
|
+
}
|
|
12090
|
+
function retryAfterMs(response) {
|
|
12091
|
+
const raw = response.headers.get("retry-after");
|
|
12092
|
+
if (!raw || !/^\d+$/.test(raw.trim())) return void 0;
|
|
12093
|
+
return Math.min(Number(raw.trim()) * 1e3, MAX_RETRY_AFTER_MS2);
|
|
12094
|
+
}
|
|
12095
|
+
function pollKindFor(url) {
|
|
12096
|
+
const path7 = new URL(url).pathname;
|
|
12097
|
+
if (path7.includes("/v1/videos")) return "video";
|
|
12098
|
+
if (path7.includes("/v1/audio/")) return "audio";
|
|
12099
|
+
return "image";
|
|
12100
|
+
}
|
|
11679
12101
|
var SafeStaleBlockhashError = class extends PaymentError {
|
|
11680
12102
|
constructor() {
|
|
11681
12103
|
super("Payment verification used an expired Solana blockhash; retrying with a fresh quote.");
|
|
@@ -11892,9 +12314,9 @@ var SolanaLLMClient = class {
|
|
|
11892
12314
|
};
|
|
11893
12315
|
if (options?.temperature !== void 0) body.temperature = options.temperature;
|
|
11894
12316
|
if (options?.topP !== void 0) body.top_p = options.topP;
|
|
11895
|
-
if (options?.searchParameters !== void 0) body.search_parameters = options.searchParameters;
|
|
12317
|
+
if (options?.searchParameters !== void 0) body.search_parameters = toWireSearchParameters(options.searchParameters);
|
|
11896
12318
|
else if (options?.search === true) body.search_parameters = { mode: "on" };
|
|
11897
|
-
if (options?.tools !== void 0) body.tools = options.tools;
|
|
12319
|
+
if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
|
|
11898
12320
|
if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
|
|
11899
12321
|
if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
|
|
11900
12322
|
if (options?.stop !== void 0) body.stop = options.stop;
|
|
@@ -11905,7 +12327,7 @@ var SolanaLLMClient = class {
|
|
|
11905
12327
|
for (let index = 0; index < chain.length; index += 1) {
|
|
11906
12328
|
const candidate = chain[index];
|
|
11907
12329
|
try {
|
|
11908
|
-
return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
|
|
12330
|
+
return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
|
|
11909
12331
|
} catch (error) {
|
|
11910
12332
|
lastError = error;
|
|
11911
12333
|
if (!isTransientError(error) || index === chain.length - 1) throw error;
|
|
@@ -11955,6 +12377,8 @@ var SolanaLLMClient = class {
|
|
|
11955
12377
|
{
|
|
11956
12378
|
routingProfile: options?.routingProfile,
|
|
11957
12379
|
requiresStructuredOutput: options?.responseFormat !== void 0,
|
|
12380
|
+
search: options?.search,
|
|
12381
|
+
searchParameters: options?.searchParameters,
|
|
11958
12382
|
minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
|
|
11959
12383
|
}
|
|
11960
12384
|
);
|
|
@@ -11982,6 +12406,8 @@ var SolanaLLMClient = class {
|
|
|
11982
12406
|
requiresStructuredOutput: options.responseFormat !== void 0,
|
|
11983
12407
|
tools: options.tools,
|
|
11984
12408
|
toolChoice: options.toolChoice,
|
|
12409
|
+
search: options.search,
|
|
12410
|
+
searchParameters: options.searchParameters,
|
|
11985
12411
|
conversationChars,
|
|
11986
12412
|
hasVision,
|
|
11987
12413
|
minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
|
|
@@ -11997,6 +12423,7 @@ var SolanaLLMClient = class {
|
|
|
11997
12423
|
toolChoice: options.toolChoice,
|
|
11998
12424
|
responseFormat: options.responseFormat,
|
|
11999
12425
|
stop: options.stop,
|
|
12426
|
+
timeout: options.timeout,
|
|
12000
12427
|
// An explicit caller-supplied chain wins over the routed one.
|
|
12001
12428
|
fallbackModels: options.fallbackModels ?? decision.fallbacks
|
|
12002
12429
|
});
|
|
@@ -12134,7 +12561,7 @@ var SolanaLLMClient = class {
|
|
|
12134
12561
|
*
|
|
12135
12562
|
* @returns USDC balance as a float
|
|
12136
12563
|
*/
|
|
12137
|
-
async getBalance() {
|
|
12564
|
+
async getBalance(options) {
|
|
12138
12565
|
const usdc_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
12139
12566
|
const address = await this.getWalletAddress();
|
|
12140
12567
|
try {
|
|
@@ -12149,6 +12576,9 @@ var SolanaLLMClient = class {
|
|
|
12149
12576
|
})
|
|
12150
12577
|
});
|
|
12151
12578
|
const data = await response.json();
|
|
12579
|
+
if (options?.strict && (!response.ok || data.error)) {
|
|
12580
|
+
throw new Error(`Solana RPC balance lookup failed: ${data.error?.message ?? `HTTP ${response.status}`}`);
|
|
12581
|
+
}
|
|
12152
12582
|
const accounts = data.result?.value || [];
|
|
12153
12583
|
if (!accounts.length) return 0;
|
|
12154
12584
|
let total = 0;
|
|
@@ -12156,7 +12586,8 @@ var SolanaLLMClient = class {
|
|
|
12156
12586
|
total += acct.account?.data?.parsed?.info?.tokenAmount?.uiAmount || 0;
|
|
12157
12587
|
}
|
|
12158
12588
|
return total;
|
|
12159
|
-
} catch {
|
|
12589
|
+
} catch (error) {
|
|
12590
|
+
if (options?.strict) throw error;
|
|
12160
12591
|
return 0;
|
|
12161
12592
|
}
|
|
12162
12593
|
}
|
|
@@ -12185,6 +12616,54 @@ var SolanaLLMClient = class {
|
|
|
12185
12616
|
const data = await this.requestWithPaymentRaw("/v1/images/image2image", body);
|
|
12186
12617
|
return data;
|
|
12187
12618
|
}
|
|
12619
|
+
/**
|
|
12620
|
+
* Generate a short video on Solana.
|
|
12621
|
+
*
|
|
12622
|
+
* The POST is verified, not charged. The job is then polled with a wallet
|
|
12623
|
+
* signature (no payment) until it finishes, and ONE payment is signed for
|
|
12624
|
+
* the finished video; a failed job is never charged. If the gateway does not
|
|
12625
|
+
* accept poll signatures for the job, it is polled with a payment per poll
|
|
12626
|
+
* instead, which is likewise only charged on completion.
|
|
12627
|
+
*
|
|
12628
|
+
* @param prompt - text description of the video.
|
|
12629
|
+
* @param options - `model` (default `xai/grok-imagine-video`), `budgetMs`
|
|
12630
|
+
* (total wait, default 15 min); any other keys are sent verbatim, so use
|
|
12631
|
+
* the gateway's snake_case names (`duration_seconds`, `image_url`,
|
|
12632
|
+
* `aspect_ratio`, `resolution`, …).
|
|
12633
|
+
*/
|
|
12634
|
+
async video(prompt, options = {}) {
|
|
12635
|
+
const { model, budgetMs, ...rest } = options;
|
|
12636
|
+
const body = { ...rest, model: model || DEFAULT_VIDEO_MODEL, prompt };
|
|
12637
|
+
const data = await this.requestWithPaymentRaw(
|
|
12638
|
+
"/v1/videos/generations",
|
|
12639
|
+
body,
|
|
12640
|
+
{ pollBudgetMs: budgetMs ?? DEFAULT_VIDEO_BUDGET_MS, submitTimeoutMs: Math.max(this.timeout, MEDIA_SUBMIT_TIMEOUT_MS) }
|
|
12641
|
+
);
|
|
12642
|
+
return data;
|
|
12643
|
+
}
|
|
12644
|
+
/**
|
|
12645
|
+
* Generate a music track on Solana. Paid at the POST; a slow track is then
|
|
12646
|
+
* polled with a wallet signature, never another payment.
|
|
12647
|
+
*
|
|
12648
|
+
* @param prompt - music style, mood, or description.
|
|
12649
|
+
* @param options - `model` (default `minimax/music-2.5+`), `instrumental`
|
|
12650
|
+
* (default true), `lyrics` (requires `instrumental: false`), `budgetMs`
|
|
12651
|
+
* (total wait, default 5 min); other keys are sent verbatim.
|
|
12652
|
+
*/
|
|
12653
|
+
async music(prompt, options = {}) {
|
|
12654
|
+
const { model, instrumental: instrumentalOption, lyrics: rawLyrics, budgetMs, ...rest } = options;
|
|
12655
|
+
const instrumental = instrumentalOption ?? true;
|
|
12656
|
+
const lyrics = rawLyrics?.trim();
|
|
12657
|
+
if (instrumental && lyrics) throw new Error("Cannot specify lyrics when instrumental is true");
|
|
12658
|
+
const body = { ...rest, model: model || DEFAULT_MUSIC_MODEL, prompt, instrumental };
|
|
12659
|
+
if (lyrics) body.lyrics = lyrics;
|
|
12660
|
+
const data = await this.requestWithPaymentRaw(
|
|
12661
|
+
"/v1/audio/generations",
|
|
12662
|
+
body,
|
|
12663
|
+
{ pollBudgetMs: budgetMs ?? DEFAULT_MUSIC_BUDGET_MS, submitTimeoutMs: Math.max(this.timeout, MEDIA_SUBMIT_TIMEOUT_MS) }
|
|
12664
|
+
);
|
|
12665
|
+
return data;
|
|
12666
|
+
}
|
|
12188
12667
|
/** Standalone search (Solana payment). */
|
|
12189
12668
|
async search(query, options) {
|
|
12190
12669
|
const body = { query, max_results: options?.maxResults || 10 };
|
|
@@ -12383,15 +12862,16 @@ var SolanaLLMClient = class {
|
|
|
12383
12862
|
* `"paid-or-in-doubt"`, and so is any error with an API key once the billed
|
|
12384
12863
|
* request was sent, except the account API's explicit 4xx refusal.
|
|
12385
12864
|
*/
|
|
12386
|
-
async requestWithPayment(endpoint, body) {
|
|
12865
|
+
async requestWithPayment(endpoint, body, timeoutMs) {
|
|
12387
12866
|
const url = `${this.apiUrl}${endpoint}`;
|
|
12867
|
+
const json = JSON.stringify(body);
|
|
12388
12868
|
const closesAtStart = await this.batchPayer?.closeFence();
|
|
12389
12869
|
for (let staleRetries = 0; ; ) {
|
|
12390
12870
|
const response = await this.sendUnpaid(url, {
|
|
12391
12871
|
method: "POST",
|
|
12392
12872
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
12393
|
-
body:
|
|
12394
|
-
});
|
|
12873
|
+
body: json
|
|
12874
|
+
}, timeoutMs);
|
|
12395
12875
|
if (response.status === 402) {
|
|
12396
12876
|
try {
|
|
12397
12877
|
let paymentRequired;
|
|
@@ -12407,16 +12887,16 @@ var SolanaLLMClient = class {
|
|
|
12407
12887
|
(paymentHeaders) => this.fetchWithTimeout(url, {
|
|
12408
12888
|
method: "POST",
|
|
12409
12889
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
|
|
12410
|
-
body:
|
|
12411
|
-
}),
|
|
12890
|
+
body: json
|
|
12891
|
+
}, timeoutMs),
|
|
12412
12892
|
// A fresh, unpaid challenge after a 429 wait: the request itself,
|
|
12413
12893
|
// sent again without a payment. Every error it throws is unpaid.
|
|
12414
12894
|
async () => {
|
|
12415
12895
|
const challenge = await this.sendUnpaid(url, {
|
|
12416
12896
|
method: "POST",
|
|
12417
12897
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
12418
|
-
body:
|
|
12419
|
-
});
|
|
12898
|
+
body: json
|
|
12899
|
+
}, timeoutMs);
|
|
12420
12900
|
if (challenge.status === 402) {
|
|
12421
12901
|
try {
|
|
12422
12902
|
return { kind: "challenge", paymentRequired: await this.readPaymentRequired(challenge) };
|
|
@@ -12431,13 +12911,14 @@ var SolanaLLMClient = class {
|
|
|
12431
12911
|
);
|
|
12432
12912
|
if (batch.kind === "paid") {
|
|
12433
12913
|
this.recordSettlement(batch.chargedUsd);
|
|
12434
|
-
|
|
12914
|
+
const paid = await this.readPaidJson(batch.response);
|
|
12915
|
+
return withCost(paid, batch.response, batch.chargedUsd, "batch");
|
|
12435
12916
|
}
|
|
12436
|
-
if (batch.kind === "served") return await this.readUnpaidJson(batch.response);
|
|
12917
|
+
if (batch.kind === "served") return this.withUnpaidCost(await this.readUnpaidJson(batch.response));
|
|
12437
12918
|
if (batch.kind === "failed") throw batch.error;
|
|
12438
12919
|
exactRequired = batch.paymentRequired ?? paymentRequired;
|
|
12439
12920
|
}
|
|
12440
|
-
return await this.handlePaymentAndRetry(url,
|
|
12921
|
+
return await this.handlePaymentAndRetry(url, json, exactRequired, staleRetries > 0, timeoutMs);
|
|
12441
12922
|
} catch (error) {
|
|
12442
12923
|
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
12443
12924
|
await waitForStaleRetry(staleRetries++);
|
|
@@ -12445,8 +12926,20 @@ var SolanaLLMClient = class {
|
|
|
12445
12926
|
}
|
|
12446
12927
|
}
|
|
12447
12928
|
if (!response.ok) throw await unpaidApiError(response);
|
|
12448
|
-
return await this.readUnpaidJson(response);
|
|
12929
|
+
return this.withUnpaidCost(await this.readUnpaidJson(response));
|
|
12930
|
+
}
|
|
12931
|
+
}
|
|
12932
|
+
/**
|
|
12933
|
+
* Cost of a chat response served without a payment: 0 with a wallet (a free
|
|
12934
|
+
* model). With an API key the call is billed to the account, so costUsd stays
|
|
12935
|
+
* undefined (see the dashboard).
|
|
12936
|
+
*/
|
|
12937
|
+
withUnpaidCost(served) {
|
|
12938
|
+
if (!this.apiAuth) {
|
|
12939
|
+
served.costUsd = 0;
|
|
12940
|
+
served.settlement = { scheme: "free", quotedUsd: 0 };
|
|
12449
12941
|
}
|
|
12942
|
+
return served;
|
|
12450
12943
|
}
|
|
12451
12944
|
/**
|
|
12452
12945
|
* Read the JSON body of a 2xx served without a payment: the free tier, or
|
|
@@ -12464,9 +12957,9 @@ var SolanaLLMClient = class {
|
|
|
12464
12957
|
* {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
|
|
12465
12958
|
* {@link accountErrorDisposition} (the request itself is billed).
|
|
12466
12959
|
*/
|
|
12467
|
-
async sendUnpaid(url, init) {
|
|
12960
|
+
async sendUnpaid(url, init, timeoutMs) {
|
|
12468
12961
|
try {
|
|
12469
|
-
return await this.fetchWithTimeout(url, init);
|
|
12962
|
+
return await this.fetchWithTimeout(url, init, timeoutMs);
|
|
12470
12963
|
} catch (error) {
|
|
12471
12964
|
throw withDisposition(error, this.apiAuth ? accountErrorDisposition(error) : "unpaid");
|
|
12472
12965
|
}
|
|
@@ -12480,106 +12973,221 @@ var SolanaLLMClient = class {
|
|
|
12480
12973
|
}
|
|
12481
12974
|
}
|
|
12482
12975
|
/**
|
|
12483
|
-
*
|
|
12484
|
-
*
|
|
12485
|
-
*
|
|
12486
|
-
*
|
|
12487
|
-
*
|
|
12488
|
-
*
|
|
12489
|
-
*
|
|
12490
|
-
*
|
|
12491
|
-
*
|
|
12492
|
-
*
|
|
12493
|
-
*
|
|
12494
|
-
*
|
|
12495
|
-
*
|
|
12496
|
-
*
|
|
12497
|
-
*
|
|
12498
|
-
*
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
*
|
|
12502
|
-
*
|
|
12503
|
-
*
|
|
12504
|
-
*
|
|
12505
|
-
*
|
|
12506
|
-
*
|
|
12507
|
-
*
|
|
12508
|
-
*
|
|
12509
|
-
*
|
|
12510
|
-
*
|
|
12511
|
-
*
|
|
12512
|
-
*
|
|
12513
|
-
*
|
|
12514
|
-
* it would book spend for jobs that were never charged.
|
|
12515
|
-
* 2. **Every poll re-signs.** One authorization cannot be replayed across a
|
|
12516
|
-
* long render. The gateway binds the job to the payer address rather than
|
|
12517
|
-
* to the signature, so a fresh signature from the same wallet is accepted
|
|
12518
|
-
* and is never charged again.
|
|
12976
|
+
* Follow a 202 `{ id, poll_url }` to completion on a Solana media route
|
|
12977
|
+
* (image, music, video).
|
|
12978
|
+
*
|
|
12979
|
+
* **Polls prove ownership, they do not pay.** A Solana payment is a
|
|
12980
|
+
* transaction pinned to a recent blockhash (~60s), so paying every poll of a
|
|
12981
|
+
* multi-minute job meant signing a fresh transaction every few seconds. Each
|
|
12982
|
+
* poll here instead carries the `x-poll-*` headers: the submitting wallet's
|
|
12983
|
+
* ed25519 signature over `blockrun-poll:v1:<kind>:<job id>:<unix seconds>`
|
|
12984
|
+
* (see `poll-ownership.ts`). That proof never charges.
|
|
12985
|
+
*
|
|
12986
|
+
* **Who pays when:**
|
|
12987
|
+
* - image / music: the POST already settled (sol settles at submit because the
|
|
12988
|
+
* signed transaction would be dead by completion). The caller recorded the
|
|
12989
|
+
* cost at POST; this loop only fetches the result and never signs a payment.
|
|
12990
|
+
* A batch-billed image job still answers its proof poll with 402: that job
|
|
12991
|
+
* falls back to the per-poll payment below.
|
|
12992
|
+
* - video: settles on completion, and a failed job is never charged. A
|
|
12993
|
+
* finished, unpaid job answers the proof poll with 402 `{status:"completed"}`
|
|
12994
|
+
* and a normal x402 challenge; ONE payment is signed from that challenge and
|
|
12995
|
+
* the same poll is re-sent with it. The cost is recorded here, on delivery.
|
|
12996
|
+
* A paid poll the gateway answers 402 (settlement in progress on another
|
|
12997
|
+
* poll, outcome unknown, stale blockhash) is re-polled after its
|
|
12998
|
+
* Retry-After — the gateway's per-job settlement claim makes a later fresh
|
|
12999
|
+
* payment safe — up to `MAX_PAID_POLL_REFUSALS` times.
|
|
13000
|
+
*
|
|
13001
|
+
* **Fallback, once per job:** if the gateway refuses the proof with its "no
|
|
13002
|
+
* such job" answer (video 400, image/music 404) or answers it with a plain
|
|
13003
|
+
* payment challenge (a gateway or job that predates poll proofs), the rest of
|
|
13004
|
+
* this job is polled the old way — an x-payment signed per poll, which the
|
|
13005
|
+
* gateway verifies against the job's payer and does not charge until a video
|
|
13006
|
+
* completes. Logged to stderr; never switches back.
|
|
12519
13007
|
*/
|
|
12520
|
-
async followSolanaJob(submitBody, budgetMs, intervalMs = 2e3) {
|
|
13008
|
+
async followSolanaJob(submitBody, budgetMs, kind, intervalMs = kind === "video" ? VIDEO_POLL_INTERVAL_MS : 2e3) {
|
|
12521
13009
|
const id = submitBody.id;
|
|
12522
13010
|
const pollPath = submitBody.poll_url;
|
|
12523
13011
|
if (!id || !pollPath) return submitBody;
|
|
12524
|
-
const pollUrl =
|
|
13012
|
+
const pollUrl = this.resolvePollUrl(pollPath);
|
|
13013
|
+
const jobId = pollJobIdFromUrl(pollUrl);
|
|
12525
13014
|
const deadline = Date.now() + budgetMs;
|
|
12526
13015
|
let lastStatus = submitBody.status || "queued";
|
|
13016
|
+
let ownershipPolls = true;
|
|
13017
|
+
let waitMs = intervalMs;
|
|
13018
|
+
let paidRefusalsLeft = MAX_PAID_POLL_REFUSALS;
|
|
13019
|
+
let videoCostUsd;
|
|
13020
|
+
let settleAttempted = false;
|
|
13021
|
+
const fallBack = (why) => {
|
|
13022
|
+
ownershipPolls = false;
|
|
13023
|
+
waitMs = 0;
|
|
13024
|
+
console.warn(
|
|
13025
|
+
`[@blockrun/llm] ${kind} job ${id}: ${why}; falling back to x-payment polling for this job`
|
|
13026
|
+
);
|
|
13027
|
+
};
|
|
13028
|
+
const deliver = (data, response) => {
|
|
13029
|
+
if (kind === "video") {
|
|
13030
|
+
if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd);
|
|
13031
|
+
const txHash = response.headers.get("x-payment-receipt");
|
|
13032
|
+
if (txHash) data.txHash = txHash;
|
|
13033
|
+
}
|
|
13034
|
+
return data;
|
|
13035
|
+
};
|
|
13036
|
+
const failIfFailed = (data, status2) => {
|
|
13037
|
+
if (lastStatus === "failed") {
|
|
13038
|
+
throw new APIError(
|
|
13039
|
+
`Upstream job failed: ${data.error || "unknown"}`,
|
|
13040
|
+
status2,
|
|
13041
|
+
sanitizeErrorResponse(data)
|
|
13042
|
+
);
|
|
13043
|
+
}
|
|
13044
|
+
};
|
|
12527
13045
|
while (Date.now() < deadline) {
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
|
|
12532
|
-
|
|
12533
|
-
|
|
12534
|
-
|
|
12535
|
-
|
|
13046
|
+
if (waitMs > 0) await sleep3(Math.min(waitMs, Math.max(0, deadline - Date.now())));
|
|
13047
|
+
waitMs = intervalMs;
|
|
13048
|
+
let challenge;
|
|
13049
|
+
if (ownershipPolls) {
|
|
13050
|
+
const proof = signPollOwnership(await solanaKeyToBytes(this.privateKey), kind, jobId);
|
|
13051
|
+
const polled = await this.fetchWithTimeout(pollUrl, {
|
|
13052
|
+
method: "GET",
|
|
13053
|
+
headers: { "User-Agent": USER_AGENT, ...proof }
|
|
13054
|
+
});
|
|
13055
|
+
const data2 = await readJsonOrEmpty(polled);
|
|
13056
|
+
if (polled.status === 402) {
|
|
13057
|
+
if (kind === "video" && data2.status === "completed") {
|
|
13058
|
+
lastStatus = "completed";
|
|
13059
|
+
settleAttempted = true;
|
|
13060
|
+
challenge = new Response(JSON.stringify(data2), { status: 402, headers: polled.headers });
|
|
13061
|
+
} else {
|
|
13062
|
+
fallBack(
|
|
13063
|
+
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)"
|
|
13064
|
+
);
|
|
13065
|
+
continue;
|
|
13066
|
+
}
|
|
13067
|
+
} else if (polled.status === (kind === "video" ? 400 : 404)) {
|
|
13068
|
+
fallBack(`the gateway refused the poll proof (HTTP ${polled.status})`);
|
|
13069
|
+
continue;
|
|
13070
|
+
} else {
|
|
13071
|
+
lastStatus = data2.status || lastStatus;
|
|
13072
|
+
failIfFailed(data2, polled.status);
|
|
13073
|
+
if (polled.status === 200 && lastStatus === "completed") return deliver(data2, polled);
|
|
13074
|
+
if (!isPendingPollStatus(polled.status)) {
|
|
13075
|
+
throw new APIError(`Poll failed: ${polled.status}`, polled.status, sanitizeErrorResponse(data2));
|
|
13076
|
+
}
|
|
13077
|
+
waitMs = retryAfterMs(polled) ?? intervalMs;
|
|
13078
|
+
continue;
|
|
13079
|
+
}
|
|
12536
13080
|
}
|
|
12537
|
-
if (challenge
|
|
12538
|
-
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
}
|
|
12542
|
-
|
|
13081
|
+
if (!challenge) {
|
|
13082
|
+
const unpaid = await this.fetchWithTimeout(pollUrl, {
|
|
13083
|
+
method: "GET",
|
|
13084
|
+
headers: { "User-Agent": USER_AGENT }
|
|
13085
|
+
});
|
|
13086
|
+
if (unpaid.status === 200) {
|
|
13087
|
+
return deliver(await unpaid.json(), unpaid);
|
|
13088
|
+
}
|
|
13089
|
+
if (unpaid.status !== 402) {
|
|
13090
|
+
let errorBody;
|
|
13091
|
+
try {
|
|
13092
|
+
errorBody = await unpaid.json();
|
|
13093
|
+
} catch {
|
|
13094
|
+
errorBody = { error: "Poll failed" };
|
|
13095
|
+
}
|
|
13096
|
+
throw new APIError(`Poll failed: ${unpaid.status}`, unpaid.status, sanitizeErrorResponse(errorBody));
|
|
12543
13097
|
}
|
|
12544
|
-
|
|
13098
|
+
challenge = unpaid;
|
|
12545
13099
|
}
|
|
12546
|
-
const { paymentPayload } = await this.signPaymentFrom402(
|
|
13100
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(
|
|
12547
13101
|
pollUrl,
|
|
12548
13102
|
challenge,
|
|
12549
13103
|
true,
|
|
12550
13104
|
// always a fresh blockhash; the last one is stale by now
|
|
12551
13105
|
pollUrl
|
|
12552
13106
|
);
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
headers: { "User-Agent": USER_AGENT, "PAYMENT-SIGNATURE": paymentPayload }
|
|
12556
|
-
});
|
|
12557
|
-
let data = {};
|
|
13107
|
+
if (kind === "video") videoCostUsd = costUsd;
|
|
13108
|
+
let paid;
|
|
12558
13109
|
try {
|
|
12559
|
-
|
|
12560
|
-
|
|
13110
|
+
paid = await this.fetchWithTimeout(pollUrl, {
|
|
13111
|
+
method: "GET",
|
|
13112
|
+
headers: { "User-Agent": USER_AGENT, "PAYMENT-SIGNATURE": paymentPayload }
|
|
13113
|
+
});
|
|
13114
|
+
} catch (error) {
|
|
13115
|
+
if (kind === "video" && lastStatus === "completed") settleAttempted = true;
|
|
13116
|
+
if (--paidRefusalsLeft <= 0) {
|
|
13117
|
+
throw withDisposition(
|
|
13118
|
+
new PaymentError(
|
|
13119
|
+
`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.`
|
|
13120
|
+
),
|
|
13121
|
+
"paid-or-in-doubt"
|
|
13122
|
+
);
|
|
13123
|
+
}
|
|
13124
|
+
continue;
|
|
12561
13125
|
}
|
|
13126
|
+
const data = await readJsonOrEmpty(paid);
|
|
12562
13127
|
lastStatus = data.status || lastStatus;
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
13128
|
+
failIfFailed(data, paid.status);
|
|
13129
|
+
if (paid.status === 200 && lastStatus === "completed") return deliver(data, paid);
|
|
13130
|
+
if (paid.status === 402 && kind === "video") {
|
|
13131
|
+
if (data.error === "Payment verification failed" && !await isSafeStaleBlockhashResponse(new Response(JSON.stringify(data), { status: 402 }))) {
|
|
13132
|
+
throw new PaymentError(
|
|
13133
|
+
`Payment for the finished video was rejected. Check your Solana USDC balance, then resume the job by polling ${pollUrl} with the same wallet.`
|
|
13134
|
+
);
|
|
13135
|
+
}
|
|
13136
|
+
settleAttempted = true;
|
|
13137
|
+
if (--paidRefusalsLeft <= 0) {
|
|
13138
|
+
throw withDisposition(
|
|
13139
|
+
new PaymentError(
|
|
13140
|
+
`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.`
|
|
13141
|
+
),
|
|
13142
|
+
"paid-or-in-doubt"
|
|
13143
|
+
);
|
|
13144
|
+
}
|
|
13145
|
+
waitMs = retryAfterMs(paid) ?? intervalMs;
|
|
13146
|
+
continue;
|
|
12572
13147
|
}
|
|
12573
|
-
if (paid.status
|
|
13148
|
+
if (!isPendingPollStatus(paid.status) && paid.status !== 200) {
|
|
12574
13149
|
throw new APIError(`Poll failed: ${paid.status}`, paid.status, sanitizeErrorResponse(data));
|
|
12575
13150
|
}
|
|
13151
|
+
waitMs = retryAfterMs(paid) ?? intervalMs;
|
|
12576
13152
|
}
|
|
13153
|
+
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.";
|
|
12577
13154
|
throw new APIError(
|
|
12578
|
-
`Job did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}).
|
|
13155
|
+
`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.`,
|
|
12579
13156
|
504,
|
|
12580
|
-
{ id, last_status: lastStatus }
|
|
13157
|
+
{ id, last_status: lastStatus, poll_url: pollUrl }
|
|
12581
13158
|
);
|
|
12582
13159
|
}
|
|
13160
|
+
/**
|
|
13161
|
+
* Absolute URL for a job's `poll_url`. The gateway returns `/api/v1/...`;
|
|
13162
|
+
* {@link buildUrl} drops the `/api` that `apiUrl` already ends with. An
|
|
13163
|
+
* absolute poll_url must be on the gateway's own origin: the poll carries a
|
|
13164
|
+
* wallet signature (and, for a finished video, a payment).
|
|
13165
|
+
*/
|
|
13166
|
+
resolvePollUrl(pollPath) {
|
|
13167
|
+
if (!/^https?:\/\//i.test(pollPath)) return this.buildUrl(pollPath);
|
|
13168
|
+
if (new URL(pollPath).origin !== new URL(this.apiUrl).origin) {
|
|
13169
|
+
throw new APIError(`poll_url is not on the gateway origin: ${pollPath}`, 502, { poll_url: pollPath });
|
|
13170
|
+
}
|
|
13171
|
+
return pollPath;
|
|
13172
|
+
}
|
|
13173
|
+
/**
|
|
13174
|
+
* Turn a `402` into a signed Solana payment payload.
|
|
13175
|
+
*
|
|
13176
|
+
* Extracted because four call sites need it — chat, the raw POST helpers, the
|
|
13177
|
+
* raw GET helper, and {@link SolanaLLMClient.stream} — and it had been
|
|
13178
|
+
* written out three times before this. That mattered more than ordinary
|
|
13179
|
+
* duplication: this is the code that signs a transfer of the caller's USDC,
|
|
13180
|
+
* so three copies meant every fix to it had to be applied three times or
|
|
13181
|
+
* quietly apply to two thirds of the paths.
|
|
13182
|
+
*
|
|
13183
|
+
* @param url - the request being paid for.
|
|
13184
|
+
* @param response - the gateway's `402`, not yet consumed.
|
|
13185
|
+
* @param forceFreshBlockhash - set on a re-sign after a stale-blockhash
|
|
13186
|
+
* rejection, so the retry cannot produce byte-identical transaction bytes.
|
|
13187
|
+
* @param resourceFallback - resource URL to claim when the 402 states none.
|
|
13188
|
+
* @returns the header value to replay with, and what it will settle for.
|
|
13189
|
+
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
13190
|
+
*/
|
|
12583
13191
|
async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
|
|
12584
13192
|
return this.signExactPayment(
|
|
12585
13193
|
url,
|
|
@@ -12620,7 +13228,7 @@ var SolanaLLMClient = class {
|
|
|
12620
13228
|
if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
|
|
12621
13229
|
const fromAddress = await this.getWalletAddress();
|
|
12622
13230
|
const secretKey = await solanaKeyToBytes(this.privateKey);
|
|
12623
|
-
const extensions = paymentRequired.extensions;
|
|
13231
|
+
const extensions = withPaymentIdentifier(paymentRequired).extensions;
|
|
12624
13232
|
const paymentPayload = await createSolanaPaymentPayload(
|
|
12625
13233
|
secretKey,
|
|
12626
13234
|
fromAddress,
|
|
@@ -12643,7 +13251,7 @@ var SolanaLLMClient = class {
|
|
|
12643
13251
|
);
|
|
12644
13252
|
return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
|
|
12645
13253
|
}
|
|
12646
|
-
async handlePaymentAndRetry(url,
|
|
13254
|
+
async handlePaymentAndRetry(url, json, paymentRequired, forceFreshBlockhash = false, timeoutMs) {
|
|
12647
13255
|
let signed;
|
|
12648
13256
|
try {
|
|
12649
13257
|
signed = await this.signExactPayment(url, paymentRequired, forceFreshBlockhash, `${this.apiUrl}/v1/chat/completions`);
|
|
@@ -12660,26 +13268,28 @@ var SolanaLLMClient = class {
|
|
|
12660
13268
|
"User-Agent": USER_AGENT,
|
|
12661
13269
|
"PAYMENT-SIGNATURE": paymentPayload
|
|
12662
13270
|
},
|
|
12663
|
-
body:
|
|
12664
|
-
});
|
|
13271
|
+
body: json
|
|
13272
|
+
}, timeoutMs);
|
|
12665
13273
|
} catch (error) {
|
|
12666
13274
|
throw withDisposition(error, "paid-or-in-doubt");
|
|
12667
13275
|
}
|
|
12668
13276
|
await this.assertPaid(retryResponse);
|
|
12669
13277
|
this.recordSettlement(costUsd);
|
|
12670
|
-
|
|
13278
|
+
const paid = await this.readPaidJson(retryResponse);
|
|
13279
|
+
return withCost(paid, retryResponse, costUsd, "exact");
|
|
12671
13280
|
}
|
|
12672
|
-
async requestWithPaymentRaw(endpoint, body) {
|
|
13281
|
+
async requestWithPaymentRaw(endpoint, body, media = {}) {
|
|
12673
13282
|
const url = `${this.apiUrl}${endpoint}`;
|
|
13283
|
+
const pollBudgetMs = media.pollBudgetMs ?? this.timeout;
|
|
12674
13284
|
for (let staleRetries = 0; ; ) {
|
|
12675
13285
|
const response = await this.fetchWithTimeout(url, {
|
|
12676
13286
|
method: "POST",
|
|
12677
13287
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
12678
13288
|
body: JSON.stringify(body)
|
|
12679
|
-
});
|
|
13289
|
+
}, media.submitTimeoutMs);
|
|
12680
13290
|
if (response.status === 402) {
|
|
12681
13291
|
try {
|
|
12682
|
-
return await this.handlePaymentAndRetryRaw(url, body, response, staleRetries > 0);
|
|
13292
|
+
return await this.handlePaymentAndRetryRaw(url, body, response, staleRetries > 0, media);
|
|
12683
13293
|
} catch (error) {
|
|
12684
13294
|
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
12685
13295
|
await waitForStaleRetry(staleRetries++);
|
|
@@ -12695,26 +13305,38 @@ var SolanaLLMClient = class {
|
|
|
12695
13305
|
}
|
|
12696
13306
|
throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
|
|
12697
13307
|
}
|
|
12698
|
-
if (this.apiAuth &&
|
|
13308
|
+
if (this.apiAuth && /^\/v1\/(images|videos|audio)\//.test(endpoint)) {
|
|
13309
|
+
return this.apiAuth.poll(
|
|
13310
|
+
response,
|
|
13311
|
+
pollBudgetMs,
|
|
13312
|
+
endpoint.startsWith("/v1/videos/") ? VIDEO_POLL_INTERVAL_MS : 2e3
|
|
13313
|
+
);
|
|
13314
|
+
}
|
|
12699
13315
|
return response.json();
|
|
12700
13316
|
}
|
|
12701
13317
|
}
|
|
12702
|
-
async handlePaymentAndRetryRaw(url, body, response, forceFreshBlockhash = false) {
|
|
13318
|
+
async handlePaymentAndRetryRaw(url, body, response, forceFreshBlockhash = false, media = {}) {
|
|
12703
13319
|
const { paymentPayload, costUsd } = await this.signPaymentFrom402(url, response, forceFreshBlockhash);
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
13320
|
+
let retryResponse;
|
|
13321
|
+
try {
|
|
13322
|
+
retryResponse = await this.fetchWithTimeout(url, {
|
|
13323
|
+
method: "POST",
|
|
13324
|
+
headers: {
|
|
13325
|
+
"Content-Type": "application/json",
|
|
13326
|
+
"User-Agent": USER_AGENT,
|
|
13327
|
+
"PAYMENT-SIGNATURE": paymentPayload
|
|
13328
|
+
},
|
|
13329
|
+
body: JSON.stringify(body)
|
|
13330
|
+
}, media.submitTimeoutMs);
|
|
13331
|
+
} catch (error) {
|
|
13332
|
+
throw withDisposition(error, "paid-or-in-doubt");
|
|
13333
|
+
}
|
|
12713
13334
|
await this.assertPaid(retryResponse);
|
|
12714
13335
|
if (retryResponse.status === 202) {
|
|
12715
|
-
|
|
13336
|
+
const kind = pollKindFor(url);
|
|
13337
|
+
if (kind !== "video") this.recordSettlement(costUsd);
|
|
12716
13338
|
const submitted = await retryResponse.json();
|
|
12717
|
-
return this.followSolanaJob(submitted, this.timeout);
|
|
13339
|
+
return this.followSolanaJob(submitted, media.pollBudgetMs ?? this.timeout, kind);
|
|
12718
13340
|
}
|
|
12719
13341
|
this.recordSettlement(costUsd);
|
|
12720
13342
|
return retryResponse.json();
|
|
@@ -12803,9 +13425,9 @@ var SolanaLLMClient = class {
|
|
|
12803
13425
|
this.sessionCalls += 1;
|
|
12804
13426
|
this.sessionTotalUsd += costUsd;
|
|
12805
13427
|
}
|
|
12806
|
-
async fetchWithTimeout(url, options) {
|
|
13428
|
+
async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
|
|
12807
13429
|
const controller = new AbortController();
|
|
12808
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
13430
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
12809
13431
|
try {
|
|
12810
13432
|
return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...options, signal: controller.signal });
|
|
12811
13433
|
} finally {
|
|
@@ -13369,6 +13991,7 @@ export {
|
|
|
13369
13991
|
BatchPaymentUnresolvedError,
|
|
13370
13992
|
BlockrunClient,
|
|
13371
13993
|
BlockrunError,
|
|
13994
|
+
DEFAULT_BATCH_IN_DOUBT,
|
|
13372
13995
|
DEFAULT_BATCH_RATE_LIMIT,
|
|
13373
13996
|
EVM_NETWORKS,
|
|
13374
13997
|
ImageClient,
|
|
@@ -13432,6 +14055,7 @@ export {
|
|
|
13432
14055
|
loadWallet,
|
|
13433
14056
|
logCost,
|
|
13434
14057
|
parsePaymentRequired,
|
|
14058
|
+
readSettlement,
|
|
13435
14059
|
retryDisposition,
|
|
13436
14060
|
saveSolanaWallet,
|
|
13437
14061
|
saveToCache,
|
|
@@ -13446,6 +14070,8 @@ export {
|
|
|
13446
14070
|
solanaKeyToBytes,
|
|
13447
14071
|
solanaPublicKey,
|
|
13448
14072
|
status,
|
|
14073
|
+
toWireSearchParameters,
|
|
14074
|
+
toWireTools,
|
|
13449
14075
|
validateMaxTokens,
|
|
13450
14076
|
validateModel,
|
|
13451
14077
|
validateTemperature,
|