routstrd 0.1.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/.claude/settings.local.json +7 -0
- package/README.md +191 -0
- package/bun.lock +376 -0
- package/dist/index.js +27019 -0
- package/package.json +34 -0
- package/routstr-cost-logging.md +71 -0
- package/src/TUI refactor.md +113 -0
- package/src/cli-shared.ts +204 -0
- package/src/cli.ts +650 -0
- package/src/daemon/args.ts +19 -0
- package/src/daemon/config-store.ts +36 -0
- package/src/daemon/http/index.ts +608 -0
- package/src/daemon/index.ts +151 -0
- package/src/daemon/models.ts +49 -0
- package/src/daemon/sse.ts +98 -0
- package/src/daemon/types.ts +25 -0
- package/src/daemon/wallet/index.ts +207 -0
- package/src/daemon.ts +1 -0
- package/src/index.ts +4 -0
- package/src/integrations/index.ts +67 -0
- package/src/integrations/openclaw.ts +177 -0
- package/src/integrations/opencode.ts +120 -0
- package/src/integrations/pi.ts +116 -0
- package/src/start-daemon.ts +90 -0
- package/src/tui/usage/app.ts +247 -0
- package/src/tui/usage/constants.ts +42 -0
- package/src/tui/usage/data.ts +228 -0
- package/src/tui/usage/index.ts +1 -0
- package/src/tui/usage/render.ts +539 -0
- package/src/tui/usage/state.ts +100 -0
- package/src/tui/usage/terminal.ts +39 -0
- package/src/tui/usage/types.ts +65 -0
- package/src/utils/config.ts +22 -0
- package/src/utils/logger.ts +54 -0
- package/test_box.ts +15 -0
- package/test_curl.sh +11 -0
- package/test_split_box.ts +17 -0
- package/test_split_box2.ts +23 -0
- package/tsconfig.json +20 -0
- package/v1-messages-format-report.md +223 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "http";
|
|
2
|
+
import {
|
|
3
|
+
routeRequestsToNodeResponse,
|
|
4
|
+
InsufficientBalanceError,
|
|
5
|
+
} from "@routstr/sdk";
|
|
6
|
+
import type { UsageTrackingDriver } from "@routstr/sdk";
|
|
7
|
+
import { logger } from "../../utils/logger";
|
|
8
|
+
|
|
9
|
+
async function readBody(req: IncomingMessage): Promise<string> {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
let data = "";
|
|
12
|
+
req.on("data", (chunk) => {
|
|
13
|
+
data += chunk.toString();
|
|
14
|
+
});
|
|
15
|
+
req.on("end", () => resolve(data));
|
|
16
|
+
req.on("error", reject);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseLimit(value: string | null, fallback = 10): number {
|
|
21
|
+
const requested = Number.parseInt(value || String(fallback), 10);
|
|
22
|
+
return Number.isFinite(requested) && requested > 0
|
|
23
|
+
? Math.min(requested, 1000)
|
|
24
|
+
: fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createDaemonRequestHandler(deps: {
|
|
28
|
+
provider: string | null;
|
|
29
|
+
server: { close(cb?: () => void): void };
|
|
30
|
+
store: any;
|
|
31
|
+
walletAdapter: any;
|
|
32
|
+
storageAdapter: any;
|
|
33
|
+
providerRegistry: any;
|
|
34
|
+
discoveryAdapter: any;
|
|
35
|
+
modelManager: any;
|
|
36
|
+
ensureProvidersBootstrapped: () => Promise<void>;
|
|
37
|
+
getRoutstr21Models: (forceRefresh?: boolean) => Promise<any[]>;
|
|
38
|
+
runWalletCommand: (args: string[]) => Promise<string>;
|
|
39
|
+
parseBalances: (output: string) => Record<string, number>;
|
|
40
|
+
mode?: "xcashu" | "apikeys";
|
|
41
|
+
usageTrackingDriver: UsageTrackingDriver;
|
|
42
|
+
}) {
|
|
43
|
+
return async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
44
|
+
const host = req.headers.host || "localhost";
|
|
45
|
+
const url = new URL(req.url || "/", `http://${host}`);
|
|
46
|
+
|
|
47
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
48
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
49
|
+
res.end(JSON.stringify({ ok: true }));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (req.method === "GET" && url.pathname === "/ping") {
|
|
54
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
55
|
+
res.end(JSON.stringify({ output: "pong" }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (req.method === "GET" && url.pathname === "/status") {
|
|
60
|
+
try {
|
|
61
|
+
const balancesOutput = await deps.runWalletCommand(["balance"]);
|
|
62
|
+
const balances = deps.parseBalances(balancesOutput);
|
|
63
|
+
const state = deps.store.getState();
|
|
64
|
+
const mode = state.mode || deps.mode || "apikeys";
|
|
65
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
66
|
+
res.end(
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
output: {
|
|
69
|
+
daemon: "running",
|
|
70
|
+
wallet: "connected",
|
|
71
|
+
mode,
|
|
72
|
+
balances,
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
78
|
+
res.end(
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
output: {
|
|
81
|
+
daemon: "running",
|
|
82
|
+
wallet: "error",
|
|
83
|
+
error: String(error),
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (req.method === "GET" && url.pathname === "/models") {
|
|
92
|
+
try {
|
|
93
|
+
const forceRefresh =
|
|
94
|
+
url.searchParams.get("refresh")?.toLowerCase() === "true";
|
|
95
|
+
const models = await deps.getRoutstr21Models(forceRefresh);
|
|
96
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
97
|
+
res.end(JSON.stringify({ output: { models } }));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
100
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (req.method === "GET" && url.pathname === "/v1/models") {
|
|
106
|
+
try {
|
|
107
|
+
const forceRefresh =
|
|
108
|
+
url.searchParams.get("refresh")?.toLowerCase() === "true";
|
|
109
|
+
const models = await deps.getRoutstr21Models(forceRefresh);
|
|
110
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
111
|
+
res.end(
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
object: "list",
|
|
114
|
+
data: models.map((model) => ({ ...model, object: "model" })),
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
119
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (req.method === "POST" && url.pathname === "/stop") {
|
|
125
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
126
|
+
res.end(JSON.stringify({ output: "stopping" }));
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
deps.server.close(() => {
|
|
129
|
+
process.exit(0);
|
|
130
|
+
});
|
|
131
|
+
}, 50);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (req.method === "POST" && url.pathname === "/refund") {
|
|
136
|
+
try {
|
|
137
|
+
const bodyText = await readBody(req);
|
|
138
|
+
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
139
|
+
const mintUrl = body.mintUrl as string | undefined;
|
|
140
|
+
|
|
141
|
+
if (!mintUrl) {
|
|
142
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
143
|
+
res.end(
|
|
144
|
+
JSON.stringify({ error: "Missing required 'mintUrl' field." }),
|
|
145
|
+
);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const state = deps.store.getState();
|
|
150
|
+
const pendingDistribution = (state.cachedTokens || []).map(
|
|
151
|
+
(t: { baseUrl: string; balance?: number }) => ({
|
|
152
|
+
baseUrl: t.baseUrl,
|
|
153
|
+
amount: t.balance || 0,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
const apiKeysStored = (state.apiKeys || []).map(
|
|
157
|
+
(k: { baseUrl: string; balance?: number }) => ({
|
|
158
|
+
baseUrl: k.baseUrl,
|
|
159
|
+
amount: k.balance || 0,
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
|
|
164
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
165
|
+
res.end(
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
output: { message: "No pending tokens to refund", results: [] },
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const refundBaseUrls = pendingDistribution
|
|
174
|
+
.map((p: { baseUrl: string }) => p.baseUrl)
|
|
175
|
+
.concat(apiKeysStored.map((p: { baseUrl: string }) => p.baseUrl));
|
|
176
|
+
|
|
177
|
+
const { RoutstrClient } = await import("@routstr/sdk");
|
|
178
|
+
const client = new RoutstrClient(
|
|
179
|
+
deps.walletAdapter,
|
|
180
|
+
deps.storageAdapter,
|
|
181
|
+
deps.providerRegistry,
|
|
182
|
+
"min",
|
|
183
|
+
"apikeys",
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const spender = client.getCashuSpender();
|
|
187
|
+
const results = await spender.refundProviders(mintUrl);
|
|
188
|
+
|
|
189
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
190
|
+
res.end(
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
output: {
|
|
193
|
+
message: `Refunded to ${mintUrl}`,
|
|
194
|
+
pendingTokens: pendingDistribution.length,
|
|
195
|
+
apiKeys: apiKeysStored.length,
|
|
196
|
+
results: results.map(
|
|
197
|
+
(r: { baseUrl: string; success: boolean }) => ({
|
|
198
|
+
baseUrl: r.baseUrl,
|
|
199
|
+
success: r.success,
|
|
200
|
+
}),
|
|
201
|
+
),
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
logger.error(`Refund error: ${message}`);
|
|
208
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
209
|
+
res.end(JSON.stringify({ error: message }));
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (req.method === "GET" && url.pathname === "/balance") {
|
|
215
|
+
try {
|
|
216
|
+
const balances = await deps.walletAdapter.getBalances();
|
|
217
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
218
|
+
res.end(
|
|
219
|
+
JSON.stringify({
|
|
220
|
+
output: {
|
|
221
|
+
balances,
|
|
222
|
+
unit: "sat",
|
|
223
|
+
activeMint: deps.walletAdapter.getActiveMintUrl(),
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
229
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (req.method === "GET" && url.pathname === "/keys/balance") {
|
|
235
|
+
try {
|
|
236
|
+
const walletBalances = await deps.walletAdapter.getBalances();
|
|
237
|
+
const totalWallet = Object.values(walletBalances).reduce<number>(
|
|
238
|
+
(sum, balance) => sum + Number(balance),
|
|
239
|
+
0,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const state = deps.store.getState();
|
|
243
|
+
const cachedTokens = state.cachedTokens || [];
|
|
244
|
+
const totalCached = cachedTokens.reduce(
|
|
245
|
+
(sum: number, t: { balance?: number }) => sum + (t.balance || 0),
|
|
246
|
+
0,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const apiKeys = state.apiKeys || [];
|
|
250
|
+
const totalApiKeys = apiKeys.reduce(
|
|
251
|
+
(sum: number, k: { balance?: number }) => sum + (k.balance || 0),
|
|
252
|
+
0,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const keys: Array<{ id: string; name: string; balance: number }> = [
|
|
256
|
+
{ id: "wallet", name: "Wallet", balance: totalWallet },
|
|
257
|
+
...cachedTokens.map((t: { baseUrl: string; balance?: number }) => ({
|
|
258
|
+
id: `cached:${t.baseUrl}`,
|
|
259
|
+
name: `Cached: ${t.baseUrl}`,
|
|
260
|
+
balance: t.balance || 0,
|
|
261
|
+
})),
|
|
262
|
+
...apiKeys.map((k: { baseUrl: string; balance?: number }) => ({
|
|
263
|
+
id: `apikey:${k.baseUrl}`,
|
|
264
|
+
name: `API Key: ${k.baseUrl}`,
|
|
265
|
+
balance: k.balance || 0,
|
|
266
|
+
})),
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
270
|
+
res.end(
|
|
271
|
+
JSON.stringify({
|
|
272
|
+
output: {
|
|
273
|
+
keys,
|
|
274
|
+
total: totalWallet + totalCached + totalApiKeys,
|
|
275
|
+
unit: "sat",
|
|
276
|
+
apikeysCalled: apiKeys.length,
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
282
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (req.method === "GET" && url.pathname === "/providers") {
|
|
288
|
+
try {
|
|
289
|
+
const state = deps.store.getState();
|
|
290
|
+
const baseUrlsList: string[] = state.baseUrlsList || [];
|
|
291
|
+
const disabledProviders: string[] = state.disabledProviders || [];
|
|
292
|
+
|
|
293
|
+
const providers = baseUrlsList.map((baseUrl, index) => ({
|
|
294
|
+
index,
|
|
295
|
+
baseUrl,
|
|
296
|
+
disabled: disabledProviders.includes(baseUrl),
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
300
|
+
res.end(
|
|
301
|
+
JSON.stringify({
|
|
302
|
+
output: {
|
|
303
|
+
providers,
|
|
304
|
+
disabledCount: disabledProviders.length,
|
|
305
|
+
totalCount: baseUrlsList.length,
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
311
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (req.method === "POST" && url.pathname === "/providers/disable") {
|
|
317
|
+
try {
|
|
318
|
+
const bodyText = await readBody(req);
|
|
319
|
+
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
320
|
+
const indices = body.indices as number[] | undefined;
|
|
321
|
+
|
|
322
|
+
if (!Array.isArray(indices)) {
|
|
323
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
324
|
+
res.end(
|
|
325
|
+
JSON.stringify({
|
|
326
|
+
error: "Missing or invalid 'indices' field (expected number[]).",
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const state = deps.store.getState();
|
|
333
|
+
const baseUrlsList: string[] = state.baseUrlsList || [];
|
|
334
|
+
const disabledProviders: string[] = [
|
|
335
|
+
...(state.disabledProviders || []),
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
const toDisable: string[] = [];
|
|
339
|
+
for (const idx of indices) {
|
|
340
|
+
if (
|
|
341
|
+
typeof idx === "number" &&
|
|
342
|
+
idx >= 0 &&
|
|
343
|
+
idx < baseUrlsList.length
|
|
344
|
+
) {
|
|
345
|
+
const baseUrl = baseUrlsList[idx]!;
|
|
346
|
+
if (!disabledProviders.includes(baseUrl)) {
|
|
347
|
+
disabledProviders.push(baseUrl);
|
|
348
|
+
toDisable.push(baseUrl);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
deps.store.getState().setDisabledProviders(disabledProviders);
|
|
354
|
+
|
|
355
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
356
|
+
res.end(
|
|
357
|
+
JSON.stringify({
|
|
358
|
+
output: {
|
|
359
|
+
message: `Disabled ${toDisable.length} provider(s)`,
|
|
360
|
+
disabled: toDisable,
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
366
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (req.method === "POST" && url.pathname === "/providers/enable") {
|
|
372
|
+
try {
|
|
373
|
+
const bodyText = await readBody(req);
|
|
374
|
+
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
375
|
+
const indices = body.indices as number[] | undefined;
|
|
376
|
+
|
|
377
|
+
if (!Array.isArray(indices)) {
|
|
378
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
379
|
+
res.end(
|
|
380
|
+
JSON.stringify({
|
|
381
|
+
error: "Missing or invalid 'indices' field (expected number[]).",
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const state = deps.store.getState();
|
|
388
|
+
const baseUrlsList: string[] = state.baseUrlsList || [];
|
|
389
|
+
const disabledProviders: string[] = [
|
|
390
|
+
...(state.disabledProviders || []),
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
const toEnable: string[] = [];
|
|
394
|
+
for (const idx of indices) {
|
|
395
|
+
if (
|
|
396
|
+
typeof idx === "number" &&
|
|
397
|
+
idx >= 0 &&
|
|
398
|
+
idx < baseUrlsList.length
|
|
399
|
+
) {
|
|
400
|
+
const baseUrl = baseUrlsList[idx]!;
|
|
401
|
+
const pos = disabledProviders.indexOf(baseUrl);
|
|
402
|
+
if (pos !== -1) {
|
|
403
|
+
disabledProviders.splice(pos, 1);
|
|
404
|
+
toEnable.push(baseUrl);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
deps.store.getState().setDisabledProviders(disabledProviders);
|
|
410
|
+
|
|
411
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
412
|
+
res.end(
|
|
413
|
+
JSON.stringify({
|
|
414
|
+
output: {
|
|
415
|
+
message: `Enabled ${toEnable.length} provider(s)`,
|
|
416
|
+
enabled: toEnable,
|
|
417
|
+
},
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
422
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (req.method === "GET" && url.pathname === "/usage") {
|
|
428
|
+
try {
|
|
429
|
+
const usageDriver = deps.usageTrackingDriver;
|
|
430
|
+
const limit = parseLimit(url.searchParams.get("limit"));
|
|
431
|
+
const entries = await usageDriver.list({ limit });
|
|
432
|
+
const totalEntries = await usageDriver.count();
|
|
433
|
+
const totalSatsCost = (await usageDriver.list()).reduce(
|
|
434
|
+
(sum, entry) => sum + (entry.satsCost || 0),
|
|
435
|
+
0,
|
|
436
|
+
);
|
|
437
|
+
const recentSatsCost = entries.reduce(
|
|
438
|
+
(sum, entry) => sum + (entry.satsCost || 0),
|
|
439
|
+
0,
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
443
|
+
res.end(
|
|
444
|
+
JSON.stringify({
|
|
445
|
+
output: {
|
|
446
|
+
entries,
|
|
447
|
+
totalEntries,
|
|
448
|
+
totalSatsCost,
|
|
449
|
+
recentSatsCost,
|
|
450
|
+
limit,
|
|
451
|
+
},
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
456
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (req.method === "GET" && url.pathname === "/usagePi") {
|
|
462
|
+
try {
|
|
463
|
+
const timestamp = (url.searchParams.get("timestamp") || "").trim();
|
|
464
|
+
if (!timestamp) {
|
|
465
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
466
|
+
res.end(
|
|
467
|
+
JSON.stringify({
|
|
468
|
+
error: "Missing required 'timestamp' query parameter.",
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const usageDriver = deps.usageTrackingDriver;
|
|
475
|
+
const limit = parseLimit(url.searchParams.get("limit"));
|
|
476
|
+
const allMatching = await usageDriver.list();
|
|
477
|
+
const requestIdPrefix = `gen-${timestamp}-`;
|
|
478
|
+
const filtered = allMatching.filter((entry) =>
|
|
479
|
+
entry.requestId.startsWith(requestIdPrefix),
|
|
480
|
+
);
|
|
481
|
+
const entries = filtered.slice(0, limit);
|
|
482
|
+
const totalEntries = filtered.length;
|
|
483
|
+
const totalSatsCost = filtered.reduce(
|
|
484
|
+
(sum, entry) => sum + (entry.satsCost || 0),
|
|
485
|
+
0,
|
|
486
|
+
);
|
|
487
|
+
const recentSatsCost = entries.reduce(
|
|
488
|
+
(sum, entry) => sum + (entry.satsCost || 0),
|
|
489
|
+
0,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
493
|
+
res.end(
|
|
494
|
+
JSON.stringify({
|
|
495
|
+
output: {
|
|
496
|
+
entries,
|
|
497
|
+
totalEntries,
|
|
498
|
+
totalSatsCost,
|
|
499
|
+
recentSatsCost,
|
|
500
|
+
limit,
|
|
501
|
+
timestamp,
|
|
502
|
+
},
|
|
503
|
+
}),
|
|
504
|
+
);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
507
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (req.method !== "POST") {
|
|
513
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
514
|
+
res.end(JSON.stringify({ error: "Only POST is supported." }));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let requestBody: unknown = {};
|
|
519
|
+
try {
|
|
520
|
+
const bodyText = await readBody(req);
|
|
521
|
+
requestBody = bodyText ? JSON.parse(bodyText) : {};
|
|
522
|
+
} catch (error) {
|
|
523
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
524
|
+
res.end(
|
|
525
|
+
JSON.stringify({
|
|
526
|
+
error: "Invalid JSON body.",
|
|
527
|
+
details: error instanceof Error ? error.message : String(error),
|
|
528
|
+
}),
|
|
529
|
+
);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const bodyObj = requestBody as Record<string, unknown>;
|
|
534
|
+
const modelId = typeof bodyObj.model === "string" ? bodyObj.model : "";
|
|
535
|
+
|
|
536
|
+
if (!modelId) {
|
|
537
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
538
|
+
res.end(JSON.stringify({ error: "Missing required 'model' field." }));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const forcedProvider: string | undefined =
|
|
543
|
+
url.searchParams.get("provider") ||
|
|
544
|
+
(req.headers["x-routstr-provider"] as string | undefined) ||
|
|
545
|
+
deps.provider ||
|
|
546
|
+
undefined;
|
|
547
|
+
|
|
548
|
+
// Convert req.headers to Record<string, string>
|
|
549
|
+
const incomingHeaders: Record<string, string> = {};
|
|
550
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
551
|
+
if (typeof value === "string") {
|
|
552
|
+
incomingHeaders[key] = value;
|
|
553
|
+
} else if (Array.isArray(value) && value.length > 0) {
|
|
554
|
+
incomingHeaders[key] = value[0]!;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
await deps.ensureProvidersBootstrapped();
|
|
560
|
+
logger.log("Routing request with path: ", url.pathname);
|
|
561
|
+
await routeRequestsToNodeResponse({
|
|
562
|
+
modelId,
|
|
563
|
+
requestBody,
|
|
564
|
+
path: url.pathname,
|
|
565
|
+
forcedProvider,
|
|
566
|
+
headers: incomingHeaders,
|
|
567
|
+
walletAdapter: deps.walletAdapter,
|
|
568
|
+
storageAdapter: deps.storageAdapter,
|
|
569
|
+
providerRegistry: deps.providerRegistry,
|
|
570
|
+
discoveryAdapter: deps.discoveryAdapter,
|
|
571
|
+
modelManager: deps.modelManager,
|
|
572
|
+
debugLevel: "DEBUG",
|
|
573
|
+
mode: deps.mode,
|
|
574
|
+
usageTrackingDriver: deps.usageTrackingDriver,
|
|
575
|
+
sdkStore: deps.store,
|
|
576
|
+
res,
|
|
577
|
+
});
|
|
578
|
+
return;
|
|
579
|
+
} catch (error) {
|
|
580
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
581
|
+
logger.error(`[daemon] Error: ${message}`);
|
|
582
|
+
|
|
583
|
+
if (error instanceof InsufficientBalanceError) {
|
|
584
|
+
const balanceError = error as {
|
|
585
|
+
required?: number;
|
|
586
|
+
available?: number;
|
|
587
|
+
maxMintBalance?: number;
|
|
588
|
+
maxMintUrl?: string;
|
|
589
|
+
};
|
|
590
|
+
res.writeHead(402, { "Content-Type": "application/json" });
|
|
591
|
+
res.end(
|
|
592
|
+
JSON.stringify({
|
|
593
|
+
error: message,
|
|
594
|
+
error_type: "insufficient_balance",
|
|
595
|
+
required: balanceError.required,
|
|
596
|
+
available: balanceError.available,
|
|
597
|
+
maxMintBalance: balanceError.maxMintBalance,
|
|
598
|
+
maxMintUrl: balanceError.maxMintUrl,
|
|
599
|
+
}),
|
|
600
|
+
);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
605
|
+
res.end(JSON.stringify({ error: message }));
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|