x402-proxy-openclaw 0.10.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/route.js ADDED
@@ -0,0 +1,671 @@
1
+ import { isDebugEnabled } from "./lib/env.js";
2
+ import { TEMPO_NETWORK, extractTxSignature } from "./handler.js";
3
+ import { appendHistory } from "./history.js";
4
+ import { SOL_MAINNET, parseMppAmount, paymentAmount } from "./tools.js";
5
+ import { writeDebugLog } from "./lib/debug-log.js";
6
+ import { randomUUID } from "node:crypto";
7
+ //#region packages/x402-proxy/src/openclaw/route.ts
8
+ function dbg(msg) {
9
+ if (isDebugEnabled()) process.stderr.write(`[x402-proxy] ${msg}\n`);
10
+ }
11
+ function createDownstreamAbort(req, res) {
12
+ const controller = new AbortController();
13
+ const abort = () => {
14
+ if (!controller.signal.aborted) controller.abort();
15
+ };
16
+ const onReqAborted = () => abort();
17
+ const onResClose = () => abort();
18
+ req.once("aborted", onReqAborted);
19
+ res.once("close", onResClose);
20
+ return {
21
+ signal: controller.signal,
22
+ cleanup() {
23
+ req.off("aborted", onReqAborted);
24
+ res.off("close", onResClose);
25
+ }
26
+ };
27
+ }
28
+ function createInferenceProxyRouteHandler(opts) {
29
+ const { providers, getX402Proxy, getMppHandler, getWalletAddress, getWalletAddressForNetwork, historyPath, allModels, logger } = opts;
30
+ const sortedProviders = providers.slice().sort((left, right) => right.baseUrl.length - left.baseUrl.length);
31
+ return async (req, res) => {
32
+ const downstream = createDownstreamAbort(req, res);
33
+ const requestId = randomUUID().slice(0, 8);
34
+ const url = new URL(req.url ?? "/", "http://localhost");
35
+ const provider = sortedProviders.find((entry) => entry.baseUrl === "/" || url.pathname.startsWith(entry.baseUrl));
36
+ if (!provider) {
37
+ res.writeHead(404, { "Content-Type": "application/json" });
38
+ res.end(JSON.stringify({ error: {
39
+ message: "Unknown inference route",
40
+ code: "not_found"
41
+ } }));
42
+ return true;
43
+ }
44
+ const walletAddress = getWalletAddress();
45
+ if (!walletAddress) {
46
+ res.writeHead(503, { "Content-Type": "application/json" });
47
+ res.end(JSON.stringify({ error: {
48
+ message: "Wallet not loaded yet",
49
+ code: "not_ready"
50
+ } }));
51
+ return true;
52
+ }
53
+ const pathSuffix = provider.baseUrl === "/" ? url.pathname : url.pathname.slice(provider.baseUrl.length);
54
+ const upstreamUrl = `${provider.upstreamUrl.replace(/\/+$/, "")}${pathSuffix.startsWith("/") ? pathSuffix : `/${pathSuffix}`}${url.search}`;
55
+ dbg(`${req.method} ${url.pathname} -> ${upstreamUrl}`);
56
+ logger.info(`proxy: intercepting ${upstreamUrl.substring(0, 80)}`);
57
+ const chunks = [];
58
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
59
+ let body = Buffer.concat(chunks).toString("utf-8");
60
+ const HOP_BY_HOP = new Set([
61
+ "authorization",
62
+ "host",
63
+ "connection",
64
+ "content-length",
65
+ "transfer-encoding",
66
+ "keep-alive",
67
+ "te",
68
+ "upgrade"
69
+ ]);
70
+ const headers = {};
71
+ for (const [key, val] of Object.entries(req.headers)) {
72
+ if (HOP_BY_HOP.has(key)) continue;
73
+ if (typeof val === "string") headers[key] = val;
74
+ }
75
+ const isChatCompletion = pathSuffix.includes("/chat/completions");
76
+ const isMessagesApi = pathSuffix.includes("/messages");
77
+ const isLlmEndpoint = isChatCompletion || isMessagesApi;
78
+ let thinkingMode;
79
+ if (isChatCompletion && body) try {
80
+ const parsed = JSON.parse(body);
81
+ if (parsed.reasoning_effort) thinkingMode = String(parsed.reasoning_effort);
82
+ if (!parsed.stream_options) {
83
+ parsed.stream_options = { include_usage: true };
84
+ body = JSON.stringify(parsed);
85
+ }
86
+ } catch {}
87
+ if (isMessagesApi && body) try {
88
+ const thinking = JSON.parse(body).thinking;
89
+ if (thinking?.type === "enabled" && thinking.budget_tokens) thinkingMode = `budget_${thinking.budget_tokens}`;
90
+ } catch {}
91
+ const method = req.method ?? "GET";
92
+ const startMs = Date.now();
93
+ writeDebugLog("proxy.request.start", {
94
+ requestId,
95
+ method,
96
+ path: url.pathname,
97
+ upstreamUrl,
98
+ protocol: provider.protocol,
99
+ isMessagesApi,
100
+ isLlmEndpoint
101
+ });
102
+ try {
103
+ const requestInit = {
104
+ method,
105
+ headers,
106
+ body: ["GET", "HEAD"].includes(method) ? void 0 : body,
107
+ signal: downstream.signal
108
+ };
109
+ const useMpp = provider.protocol === "mpp" || provider.protocol === "auto";
110
+ const wantsStreaming = isLlmEndpoint && /"stream"\s*:\s*true/.test(body);
111
+ if (useMpp) {
112
+ const mpp = getMppHandler();
113
+ if (!mpp) {
114
+ res.writeHead(503, { "Content-Type": "application/json" });
115
+ res.end(JSON.stringify({ error: {
116
+ message: "MPP inference requires an EVM wallet. Configure X402_PROXY_WALLET_MNEMONIC or X402_PROXY_WALLET_EVM_KEY.",
117
+ code: "mpp_wallet_missing"
118
+ } }));
119
+ return true;
120
+ }
121
+ const mppWalletAddress = getWalletAddressForNetwork?.("eip155:4217") ?? walletAddress;
122
+ return await handleMppRequest({
123
+ res,
124
+ upstreamUrl,
125
+ requestInit,
126
+ abortSignal: downstream.signal,
127
+ isLlmEndpoint,
128
+ requestId,
129
+ walletAddress: mppWalletAddress,
130
+ historyPath,
131
+ logger,
132
+ allModels,
133
+ thinkingMode,
134
+ wantsStreaming,
135
+ isMessagesApi,
136
+ startMs,
137
+ mpp
138
+ });
139
+ }
140
+ const proxy = getX402Proxy();
141
+ if (!proxy) {
142
+ res.writeHead(503, { "Content-Type": "application/json" });
143
+ res.end(JSON.stringify({ error: {
144
+ message: "x402 wallet not loaded yet. Configure a Solana wallet or switch the provider to mpp.",
145
+ code: "x402_wallet_missing"
146
+ } }));
147
+ return true;
148
+ }
149
+ const response = await proxy.x402Fetch(upstreamUrl, requestInit);
150
+ if (response.status === 402) {
151
+ const responseBody = await response.text();
152
+ logger.error(`x402: payment failed, raw response: ${responseBody}`);
153
+ const payment = proxy.shiftPayment();
154
+ const amount = paymentAmount(payment);
155
+ const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
156
+ const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
157
+ if (isLlmEndpoint) appendHistory(historyPath, {
158
+ t: Date.now(),
159
+ ok: false,
160
+ kind: "x402_inference",
161
+ net: paymentNetwork,
162
+ from: paymentFrom,
163
+ to: payment?.payTo,
164
+ amount,
165
+ token: amount != null ? "USDC" : void 0,
166
+ ms: Date.now() - startMs,
167
+ error: "payment_required"
168
+ });
169
+ let userMessage;
170
+ if (responseBody.includes("simulation") || responseBody.includes("Simulation")) userMessage = `Insufficient USDC or SOL in wallet ${walletAddress}. Fund it with USDC (SPL token) to pay for inference.`;
171
+ else if (responseBody.includes("insufficient") || responseBody.includes("balance")) userMessage = `Insufficient funds in wallet ${walletAddress}. Top up with USDC on Solana mainnet.`;
172
+ else userMessage = `x402 payment failed: ${responseBody.substring(0, 200) || "unknown error"}. Wallet: ${walletAddress}`;
173
+ writeErrorResponse(res, 402, userMessage, "x402_payment_error", "payment_failed", isMessagesApi);
174
+ return true;
175
+ }
176
+ if (!response.ok && isLlmEndpoint) {
177
+ const responseBody = await response.text();
178
+ logger.error(`x402: upstream error ${response.status}: ${responseBody.substring(0, 300)}`);
179
+ const payment = proxy.shiftPayment();
180
+ const amount = paymentAmount(payment);
181
+ const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
182
+ const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
183
+ appendHistory(historyPath, {
184
+ t: Date.now(),
185
+ ok: false,
186
+ kind: "x402_inference",
187
+ net: paymentNetwork,
188
+ from: paymentFrom,
189
+ to: payment?.payTo,
190
+ amount,
191
+ token: amount != null ? "USDC" : void 0,
192
+ ms: Date.now() - startMs,
193
+ error: `upstream_${response.status}`
194
+ });
195
+ writeErrorResponse(res, 502, `LLM provider temporarily unavailable (HTTP ${response.status}). Try again shortly.`, "x402_upstream_error", "upstream_failed", isMessagesApi);
196
+ return true;
197
+ }
198
+ logger.info(`x402: response ${response.status}`);
199
+ const txSig = extractTxSignature(response);
200
+ const payment = proxy.shiftPayment();
201
+ const amount = paymentAmount(payment);
202
+ const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
203
+ const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
204
+ const resHeaders = {};
205
+ for (const [key, val] of response.headers.entries()) resHeaders[key] = val;
206
+ res.writeHead(response.status, resHeaders);
207
+ if (!response.body) {
208
+ res.end();
209
+ if (shouldAppendInferenceHistory({
210
+ isLlmEndpoint,
211
+ amount
212
+ })) appendHistory(historyPath, {
213
+ t: Date.now(),
214
+ ok: true,
215
+ kind: "x402_inference",
216
+ net: paymentNetwork,
217
+ from: paymentFrom,
218
+ to: payment?.payTo,
219
+ tx: txSig,
220
+ amount,
221
+ token: "USDC",
222
+ ms: Date.now() - startMs
223
+ });
224
+ return true;
225
+ }
226
+ const ct = response.headers.get("content-type") || "";
227
+ const sse = isLlmEndpoint && ct.includes("text/event-stream") ? createSseTracker() : null;
228
+ const reader = response.body.getReader();
229
+ const decoder = new TextDecoder();
230
+ let sawFirstChunk = false;
231
+ let sawFirstWrite = false;
232
+ try {
233
+ while (true) {
234
+ if (downstream.signal.aborted) {
235
+ writeDebugLog("proxy.x402.aborted", { requestId });
236
+ await reader.cancel().catch(() => {});
237
+ break;
238
+ }
239
+ const { done, value } = await reader.read();
240
+ if (done) break;
241
+ if (!sawFirstChunk) {
242
+ sawFirstChunk = true;
243
+ writeDebugLog("proxy.x402.first_chunk", {
244
+ requestId,
245
+ ms: Date.now() - startMs
246
+ });
247
+ }
248
+ res.write(value);
249
+ if (!sawFirstWrite) {
250
+ sawFirstWrite = true;
251
+ writeDebugLog("proxy.x402.first_write", {
252
+ requestId,
253
+ ms: Date.now() - startMs
254
+ });
255
+ }
256
+ sse?.push(decoder.decode(value, { stream: true }));
257
+ if (isMessagesApi && sse?.sawAnthropicMessageStop) {
258
+ writeDebugLog("proxy.x402.message_stop", {
259
+ requestId,
260
+ ms: Date.now() - startMs
261
+ });
262
+ await reader.cancel().catch(() => {});
263
+ writeDebugLog("proxy.x402.semantic_close", {
264
+ requestId,
265
+ ms: Date.now() - startMs
266
+ });
267
+ break;
268
+ }
269
+ }
270
+ } finally {
271
+ reader.releaseLock();
272
+ }
273
+ if (!res.writableEnded) res.end();
274
+ writeDebugLog("proxy.x402.end", {
275
+ requestId,
276
+ ms: Date.now() - startMs,
277
+ hasUsage: sse?.result != null
278
+ });
279
+ if (shouldAppendInferenceHistory({
280
+ isLlmEndpoint,
281
+ amount,
282
+ usage: sse?.result
283
+ })) appendInferenceHistory({
284
+ historyPath,
285
+ allModels,
286
+ walletAddress: paymentFrom,
287
+ paymentNetwork,
288
+ paymentTo: payment?.payTo,
289
+ tx: txSig,
290
+ amount,
291
+ thinkingMode,
292
+ usage: sse?.result,
293
+ durationMs: Date.now() - startMs
294
+ });
295
+ return true;
296
+ } catch (err) {
297
+ const msg = String(err);
298
+ writeDebugLog("proxy.request.error", {
299
+ requestId,
300
+ ms: Date.now() - startMs,
301
+ error: msg.substring(0, 200)
302
+ });
303
+ logger.error(`x402: fetch threw: ${msg}`);
304
+ getX402Proxy()?.shiftPayment();
305
+ if (isLlmEndpoint) appendHistory(historyPath, {
306
+ t: Date.now(),
307
+ ok: false,
308
+ kind: "x402_inference",
309
+ net: SOL_MAINNET,
310
+ from: walletAddress,
311
+ ms: Date.now() - startMs,
312
+ error: msg.substring(0, 200)
313
+ });
314
+ let userMessage;
315
+ if (msg.includes("Simulation failed") || msg.includes("simulation")) userMessage = `Insufficient USDC or SOL in wallet ${walletAddress}. Fund it with USDC and SOL to pay for inference.`;
316
+ else if (msg.includes("Failed to create payment")) userMessage = `x402 payment creation failed: ${msg}. Wallet: ${walletAddress}`;
317
+ else userMessage = `x402 request failed: ${msg}`;
318
+ if (!res.headersSent) writeErrorResponse(res, 402, userMessage, "x402_payment_error", "payment_failed", isMessagesApi);
319
+ return true;
320
+ } finally {
321
+ writeDebugLog("proxy.request.finish", {
322
+ requestId,
323
+ ms: Date.now() - startMs,
324
+ aborted: downstream.signal.aborted
325
+ });
326
+ downstream.cleanup();
327
+ }
328
+ };
329
+ }
330
+ function writeErrorResponse(res, status, message, type, code, isAnthropicFormat) {
331
+ res.writeHead(status, { "Content-Type": "application/json" });
332
+ if (isAnthropicFormat) res.end(JSON.stringify({
333
+ type: "error",
334
+ error: {
335
+ type,
336
+ message
337
+ }
338
+ }));
339
+ else res.end(JSON.stringify({ error: {
340
+ message,
341
+ type,
342
+ code
343
+ } }));
344
+ }
345
+ function shouldAppendInferenceHistory(opts) {
346
+ if (!opts.isLlmEndpoint) return false;
347
+ return opts.amount != null || opts.usage != null;
348
+ }
349
+ function createSseTracker() {
350
+ let residual = "";
351
+ let anthropicModel = "";
352
+ let anthropicInputTokens = 0;
353
+ let anthropicOutputTokens = 0;
354
+ let anthropicCacheRead;
355
+ let anthropicCacheWrite;
356
+ let anthropicMessageStop = false;
357
+ let lastOpenAiData = "";
358
+ let isAnthropic = false;
359
+ function processJson(json) {
360
+ let parsed;
361
+ try {
362
+ parsed = JSON.parse(json);
363
+ } catch {
364
+ return;
365
+ }
366
+ const type = parsed.type;
367
+ if (type === "message_start") {
368
+ isAnthropic = true;
369
+ const msg = parsed.message;
370
+ anthropicModel = msg?.model ?? "";
371
+ const u = msg?.usage;
372
+ if (u) {
373
+ anthropicInputTokens = u.input_tokens ?? 0;
374
+ anthropicCacheWrite = u.cache_creation_input_tokens ?? void 0;
375
+ anthropicCacheRead = u.cache_read_input_tokens ?? void 0;
376
+ }
377
+ return;
378
+ }
379
+ if (type === "message_delta") {
380
+ isAnthropic = true;
381
+ const u = parsed.usage;
382
+ if (u?.output_tokens != null) anthropicOutputTokens = u.output_tokens;
383
+ return;
384
+ }
385
+ if (type === "message_stop") {
386
+ isAnthropic = true;
387
+ anthropicMessageStop = true;
388
+ return;
389
+ }
390
+ if (type === "message") {
391
+ isAnthropic = true;
392
+ anthropicModel = parsed.model ?? "";
393
+ const u = parsed.usage;
394
+ if (u) {
395
+ anthropicInputTokens = u.input_tokens ?? 0;
396
+ anthropicOutputTokens = u.output_tokens ?? 0;
397
+ anthropicCacheWrite = u.cache_creation_input_tokens ?? void 0;
398
+ anthropicCacheRead = u.cache_read_input_tokens ?? void 0;
399
+ }
400
+ return;
401
+ }
402
+ if (parsed.usage || parsed.model) lastOpenAiData = json;
403
+ }
404
+ return {
405
+ push(text) {
406
+ const lines = (residual + text).split("\n");
407
+ residual = lines.pop() ?? "";
408
+ for (const line of lines) if (line.startsWith("data: ") && line !== "data: [DONE]") processJson(line.slice(6));
409
+ },
410
+ pushJson(text) {
411
+ processJson(text);
412
+ },
413
+ get sawAnthropicMessageStop() {
414
+ return anthropicMessageStop;
415
+ },
416
+ get result() {
417
+ if (isAnthropic) {
418
+ if (!anthropicModel && !anthropicInputTokens && !anthropicOutputTokens) return void 0;
419
+ return {
420
+ model: anthropicModel,
421
+ inputTokens: anthropicInputTokens,
422
+ outputTokens: anthropicOutputTokens,
423
+ cacheRead: anthropicCacheRead,
424
+ cacheWrite: anthropicCacheWrite
425
+ };
426
+ }
427
+ if (!lastOpenAiData) return void 0;
428
+ try {
429
+ const parsed = JSON.parse(lastOpenAiData);
430
+ return {
431
+ model: parsed.model ?? "",
432
+ inputTokens: parsed.usage?.prompt_tokens ?? 0,
433
+ outputTokens: parsed.usage?.completion_tokens ?? 0,
434
+ reasoningTokens: parsed.usage?.completion_tokens_details?.reasoning_tokens,
435
+ cacheRead: parsed.usage?.prompt_tokens_details?.cached_tokens,
436
+ cacheWrite: parsed.usage?.prompt_tokens_details?.cache_creation_input_tokens
437
+ };
438
+ } catch {
439
+ return;
440
+ }
441
+ }
442
+ };
443
+ }
444
+ async function handleMppRequest(opts) {
445
+ const { res, upstreamUrl, requestInit, abortSignal, isLlmEndpoint, requestId, walletAddress, historyPath, logger, allModels, thinkingMode, wantsStreaming, isMessagesApi, startMs, mpp } = opts;
446
+ try {
447
+ if (wantsStreaming) {
448
+ res.writeHead(200, {
449
+ "Content-Type": "text/event-stream; charset=utf-8",
450
+ "Cache-Control": "no-cache, no-transform",
451
+ Connection: "keep-alive"
452
+ });
453
+ const sse = createSseTracker();
454
+ dbg(`mpp.sse() calling ${upstreamUrl}`);
455
+ writeDebugLog("proxy.mpp.sse.open", {
456
+ requestId,
457
+ upstreamUrl
458
+ });
459
+ const stream = await mpp.sse(upstreamUrl, requestInit);
460
+ dbg("mpp.sse() resolved, iterating stream");
461
+ const iterator = stream[Symbol.asyncIterator]();
462
+ let semanticComplete = false;
463
+ let sawFirstChunk = false;
464
+ let sawFirstWrite = false;
465
+ try {
466
+ if (isMessagesApi) while (true) {
467
+ if (abortSignal.aborted) {
468
+ writeDebugLog("proxy.mpp.aborted", { requestId });
469
+ break;
470
+ }
471
+ const { done, value } = await iterator.next();
472
+ if (done) break;
473
+ const text = String(value);
474
+ if (!sawFirstChunk) {
475
+ sawFirstChunk = true;
476
+ writeDebugLog("proxy.mpp.first_chunk", {
477
+ requestId,
478
+ ms: Date.now() - startMs
479
+ });
480
+ }
481
+ let eventType = "unknown";
482
+ try {
483
+ eventType = JSON.parse(text).type ?? "unknown";
484
+ } catch {}
485
+ res.write(`event: ${eventType}\ndata: ${text}\n\n`);
486
+ if (!sawFirstWrite) {
487
+ sawFirstWrite = true;
488
+ writeDebugLog("proxy.mpp.first_write", {
489
+ requestId,
490
+ ms: Date.now() - startMs
491
+ });
492
+ }
493
+ sse.pushJson(text);
494
+ if (sse.sawAnthropicMessageStop) {
495
+ semanticComplete = true;
496
+ writeDebugLog("proxy.mpp.message_stop", {
497
+ requestId,
498
+ ms: Date.now() - startMs
499
+ });
500
+ break;
501
+ }
502
+ }
503
+ else {
504
+ while (true) {
505
+ if (abortSignal.aborted) {
506
+ writeDebugLog("proxy.mpp.aborted", { requestId });
507
+ break;
508
+ }
509
+ const { done, value } = await iterator.next();
510
+ if (done) break;
511
+ const text = String(value);
512
+ if (!sawFirstChunk) {
513
+ sawFirstChunk = true;
514
+ writeDebugLog("proxy.mpp.first_chunk", {
515
+ requestId,
516
+ ms: Date.now() - startMs
517
+ });
518
+ }
519
+ res.write(`data: ${text}\n\n`);
520
+ if (!sawFirstWrite) {
521
+ sawFirstWrite = true;
522
+ writeDebugLog("proxy.mpp.first_write", {
523
+ requestId,
524
+ ms: Date.now() - startMs
525
+ });
526
+ }
527
+ sse.pushJson(text);
528
+ }
529
+ if (!abortSignal.aborted) res.write("data: [DONE]\n\n");
530
+ }
531
+ } catch (err) {
532
+ const msg = err instanceof Error ? err.message : String(err);
533
+ writeDebugLog("proxy.mpp.stream_error", {
534
+ requestId,
535
+ ms: Date.now() - startMs,
536
+ error: msg.substring(0, 200),
537
+ semanticComplete
538
+ });
539
+ if (!(abortSignal.aborted || semanticComplete) || !msg.includes("terminated")) throw err;
540
+ } finally {
541
+ await iterator.return?.().catch(() => {});
542
+ }
543
+ dbg(`stream done, ${sse.result ? `${sse.result.model} ${sse.result.inputTokens}+${sse.result.outputTokens}t` : "no usage"}`);
544
+ if (!res.writableEnded) res.end();
545
+ if (semanticComplete) writeDebugLog("proxy.mpp.semantic_close", {
546
+ requestId,
547
+ ms: Date.now() - startMs
548
+ });
549
+ mpp.shiftPayment();
550
+ if (shouldAppendInferenceHistory({
551
+ isLlmEndpoint,
552
+ usage: sse.result
553
+ })) appendInferenceHistory({
554
+ historyPath,
555
+ allModels,
556
+ walletAddress,
557
+ paymentNetwork: TEMPO_NETWORK,
558
+ paymentTo: void 0,
559
+ thinkingMode,
560
+ usage: sse.result,
561
+ durationMs: Date.now() - startMs
562
+ });
563
+ writeDebugLog("proxy.mpp.end", {
564
+ requestId,
565
+ ms: Date.now() - startMs,
566
+ hasUsage: sse.result != null
567
+ });
568
+ return true;
569
+ }
570
+ const response = await mpp.fetch(upstreamUrl, requestInit);
571
+ writeDebugLog("proxy.mpp.fetch.response", {
572
+ requestId,
573
+ status: response.status,
574
+ ms: Date.now() - startMs
575
+ });
576
+ const tx = extractTxSignature(response);
577
+ if (response.status === 402) {
578
+ const responseBody = await response.text();
579
+ logger.error(`mpp: payment failed, raw response: ${responseBody}`);
580
+ if (isLlmEndpoint) appendHistory(historyPath, {
581
+ t: Date.now(),
582
+ ok: false,
583
+ kind: "x402_inference",
584
+ net: TEMPO_NETWORK,
585
+ from: walletAddress,
586
+ ms: Date.now() - startMs,
587
+ error: "payment_required"
588
+ });
589
+ writeErrorResponse(res, 402, `MPP payment failed: ${responseBody.substring(0, 200) || "unknown error"}. Wallet: ${walletAddress}`, "mpp_payment_error", "payment_failed", isMessagesApi);
590
+ return true;
591
+ }
592
+ const resHeaders = {};
593
+ for (const [key, value] of response.headers.entries()) resHeaders[key] = value;
594
+ res.writeHead(response.status, resHeaders);
595
+ const responseBody = await response.text();
596
+ res.end(responseBody);
597
+ const usageTracker = createSseTracker();
598
+ usageTracker.pushJson(responseBody);
599
+ const payment = mpp.shiftPayment();
600
+ const amount = parseMppAmount(payment?.amount);
601
+ if (shouldAppendInferenceHistory({
602
+ isLlmEndpoint,
603
+ amount,
604
+ usage: usageTracker.result
605
+ })) appendInferenceHistory({
606
+ historyPath,
607
+ allModels,
608
+ walletAddress,
609
+ paymentNetwork: payment?.network ?? "eip155:4217",
610
+ paymentTo: void 0,
611
+ tx: tx ?? payment?.receipt?.reference,
612
+ amount,
613
+ thinkingMode,
614
+ usage: usageTracker.result,
615
+ durationMs: Date.now() - startMs
616
+ });
617
+ return true;
618
+ } catch (err) {
619
+ dbg(`mpp error: ${String(err)}`);
620
+ logger.error(`mpp: fetch threw: ${String(err)}`);
621
+ if (isLlmEndpoint) appendHistory(historyPath, {
622
+ t: Date.now(),
623
+ ok: false,
624
+ kind: "x402_inference",
625
+ net: TEMPO_NETWORK,
626
+ from: walletAddress,
627
+ ms: Date.now() - startMs,
628
+ error: String(err).substring(0, 200)
629
+ });
630
+ if (!res.headersSent) writeErrorResponse(res, 402, `MPP request failed: ${String(err)}`, "mpp_payment_error", "payment_failed", isMessagesApi);
631
+ else if (!res.writableEnded) res.end();
632
+ return true;
633
+ }
634
+ }
635
+ function appendInferenceHistory(opts) {
636
+ const { historyPath, allModels, walletAddress, paymentNetwork, paymentTo, tx, amount, thinkingMode, usage, durationMs } = opts;
637
+ if (usage) appendHistory(historyPath, {
638
+ t: Date.now(),
639
+ ok: true,
640
+ kind: "x402_inference",
641
+ net: paymentNetwork,
642
+ from: walletAddress,
643
+ to: paymentTo,
644
+ tx,
645
+ amount,
646
+ token: "USDC",
647
+ provider: allModels.find((entry) => entry.id === usage.model || `${entry.provider}/${entry.id}` === usage.model)?.provider,
648
+ model: usage.model,
649
+ inputTokens: usage.inputTokens,
650
+ outputTokens: usage.outputTokens,
651
+ reasoningTokens: usage.reasoningTokens,
652
+ cacheRead: usage.cacheRead,
653
+ cacheWrite: usage.cacheWrite,
654
+ thinking: thinkingMode,
655
+ ms: durationMs
656
+ });
657
+ else appendHistory(historyPath, {
658
+ t: Date.now(),
659
+ ok: true,
660
+ kind: "x402_inference",
661
+ net: paymentNetwork,
662
+ from: walletAddress,
663
+ to: paymentTo,
664
+ tx,
665
+ amount,
666
+ token: "USDC",
667
+ ms: durationMs
668
+ });
669
+ }
670
+ //#endregion
671
+ export { createInferenceProxyRouteHandler };