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/CHANGELOG.md +453 -0
- package/LICENSE +201 -0
- package/README.md +35 -0
- package/dist/commands/wallet.js +149 -0
- package/dist/commands.js +218 -0
- package/dist/defaults.js +129 -0
- package/dist/handler.js +319 -0
- package/dist/history.js +124 -0
- package/dist/lib/config.js +47 -0
- package/dist/lib/debug-log.js +29 -0
- package/dist/lib/derive.js +77 -0
- package/dist/lib/env.js +9 -0
- package/dist/lib/optimized-svm-scheme.js +86 -0
- package/dist/lib/output.js +13 -0
- package/dist/lib/wallet-resolution.js +76 -0
- package/dist/openclaw/plugin.d.ts +15 -0
- package/dist/openclaw/plugin.js +156 -0
- package/dist/openclaw.plugin.json +112 -0
- package/dist/route.js +671 -0
- package/dist/solana.js +93 -0
- package/dist/tools.js +269 -0
- package/dist/wallet.js +15 -0
- package/openclaw.plugin.json +112 -0
- package/package.json +93 -0
- package/skills/SKILL.md +183 -0
- package/skills/references/library.md +85 -0
- package/skills/references/openclaw-plugin.md +145 -0
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 };
|