Files
routstrd/src/daemon/http/index.ts
T

1155 lines
34 KiB
TypeScript

import { randomBytes } from "crypto";
import { type IncomingMessage, type ServerResponse } from "http";
import { Readable } from "stream";
import {
routeRequests,
InsufficientBalanceError,
ProviderManager,
} from "@routstr/sdk";
import type { UsageTrackingDriver, SdkLogger } from "@routstr/sdk";
import { logger } from "../../utils/logger";
import {
CocodHttpError,
type CocodClient,
type CocodState,
} from "../wallet/cocod-client";
import { decodeCashuTokenAmount } from "../wallet";
import { getClientsFromStore } from "../../utils/clients";
type ClientMode = "xcashu" | "lazyrefund" | "apikeys";
type WalletStatusOutput = {
daemon: "running";
wallet: "connected" | "error";
walletState: CocodState;
balances?: Record<string, number>;
mode: ClientMode;
error?: string;
};
type DaemonDeps = {
provider: string | null;
server: { close(cb?: () => void): void };
store: any;
walletClient: CocodClient;
walletAdapter: any;
storageAdapter: any;
providerRegistry: any;
discoveryAdapter: any;
modelManager: any;
ensureProvidersBootstrapped: () => Promise<void>;
getRoutstr21Models: (forceRefresh?: boolean) => Promise<any[]>;
getModelProviders: (modelId: string) => Promise<any>;
mode?: ClientMode;
/** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */
routstrPubkey?: string;
providerManager: ProviderManager;
};
/**
* Extracts the client ID from an incoming request by looking up the API key
* in the store's clientIds list.
*/
function getClientIdFromRequest(
req: IncomingMessage,
store: { getState(): any },
): string | undefined {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return undefined;
}
const apiKey = authHeader.slice(7); // Remove "Bearer " prefix
if (!apiKey.startsWith("sk-")) {
return undefined;
}
const clients = getClientsFromStore(store);
const matchingClient = clients.find((c) => c.apiKey === apiKey);
return matchingClient?.clientId;
}
function generateApiKey(): string {
const bytes = randomBytes(24);
return `sk-${bytes.toString("hex")}`;
}
async function readBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let data = "";
req.on("data", (chunk) => {
data += chunk.toString();
});
req.on("end", () => resolve(data));
req.on("error", reject);
});
}
async function readJsonBody(
req: IncomingMessage,
): Promise<Record<string, unknown>> {
const bodyText = await readBody(req);
if (!bodyText) {
return {};
}
try {
return JSON.parse(bodyText) as Record<string, unknown>;
} catch {
throw new CocodHttpError(400, "Invalid JSON body.");
}
}
function parseLimit(value: string | null, fallback = 10): number {
const requested = Number.parseInt(value || String(fallback), 10);
return Number.isFinite(requested) && requested > 0
? Math.min(requested, 100000) // Cap at 100k entries
: fallback;
}
function sendJson(
res: ServerResponse,
status: number,
payload: Record<string, unknown>,
): void {
res.writeHead(status, { "Content-Type": "application/json" });
res.end(JSON.stringify(payload));
}
function toErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function getWalletStateMessage(state: CocodState): string {
switch (state) {
case "LOCKED":
return "Wallet is locked. Unlock it before performing wallet operations.";
case "UNINITIALIZED":
return "Wallet is not initialized. Run 'routstrd onboard' first.";
case "ERROR":
return "Wallet is in an error state.";
default:
return "Wallet is unavailable.";
}
}
function respondWithError(
res: ServerResponse,
error: unknown,
fallbackStatus = 500,
): void {
if (error instanceof CocodHttpError) {
sendJson(res, error.status, { error: error.message });
return;
}
sendJson(res, fallbackStatus, { error: toErrorMessage(error) });
}
async function respond(
res: ServerResponse,
getPayload: () => Promise<Record<string, unknown>>,
): Promise<void> {
try {
sendJson(res, 200, await getPayload());
} catch (error) {
respondWithError(res, error);
}
}
function requireStringField(
body: Record<string, unknown>,
field: string,
): string | null {
const value = body[field];
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function getRequiredStringField(
body: Record<string, unknown>,
field: string,
): string {
const value = requireStringField(body, field);
if (!value) {
throw new CocodHttpError(400, `Missing required '${field}' field.`);
}
return value;
}
function getRequiredPositiveNumberField(
body: Record<string, unknown>,
field: string,
): number {
const value = body[field];
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
return value;
}
if (typeof value === "string" && value.trim()) {
const parsed = Number.parseInt(value.trim(), 10);
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
}
throw new CocodHttpError(400, `Missing required '${field}' field.`);
}
function optionalStringField(
body: Record<string, unknown>,
field: string,
): string | undefined {
const value = body[field];
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function getCurrentMode(deps: DaemonDeps): ClientMode {
const stateMode = deps.store.getState()?.mode;
return stateMode || deps.mode || "apikeys";
}
async function buildStatusOutput(
deps: DaemonDeps,
): Promise<WalletStatusOutput> {
const mode = getCurrentMode(deps);
try {
const walletState = await deps.walletClient.getStatus();
if (walletState !== "UNLOCKED") {
return {
daemon: "running",
wallet: "error",
walletState,
mode,
error: getWalletStateMessage(walletState),
};
}
const balances = await deps.walletAdapter.getBalances();
return {
daemon: "running",
wallet: "connected",
walletState,
balances,
mode,
};
} catch (error) {
return {
daemon: "running",
wallet: "error",
walletState: "ERROR",
mode,
error: toErrorMessage(error),
};
}
}
async function buildWalletDetails(deps: DaemonDeps): Promise<{
state: CocodState;
ready: boolean;
balances?: Record<string, number>;
unit?: "sat";
activeMint?: string | null;
}> {
const state = await deps.walletClient.getStatus();
if (state !== "UNLOCKED") {
return { state, ready: false };
}
const balances = await deps.walletAdapter.getBalances();
return {
state,
ready: true,
balances,
unit: "sat",
activeMint: deps.walletAdapter.getActiveMintUrl(),
};
}
function makeSdkLogger(prefix?: string): SdkLogger {
const tag = prefix ? `[${prefix}]` : undefined;
const fmt = (...args: unknown[]) => (tag ? [tag, ...args] : args);
return {
log: (...args: unknown[]) => logger.log(...fmt(...args)),
warn: (...args: unknown[]) => logger.log(...fmt(...args)),
error: (...args: unknown[]) => logger.error(...fmt(...args)),
debug: (...args: unknown[]) => logger.debug(...fmt(...args)),
child: (p: string) => makeSdkLogger(prefix ? `${prefix}:${p}` : p),
};
}
const sdkLogger: SdkLogger = makeSdkLogger();
export function createDaemonRequestHandler(deps: {
provider: string | null;
server: { close(cb?: () => void): void };
store: any;
walletClient: CocodClient;
walletAdapter: any;
storageAdapter: any;
providerRegistry: any;
discoveryAdapter: any;
modelManager: any;
ensureProvidersBootstrapped: () => Promise<void>;
getRoutstr21Models: (forceRefresh?: boolean) => Promise<any[]>;
getModelProviders: (modelId: string) => Promise<any>;
mode?: "xcashu" | "apikeys";
/** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */
routstrPubkey?: string;
usageTrackingDriver: UsageTrackingDriver;
providerManager: ProviderManager;
}) {
return async function handler(req: IncomingMessage, res: ServerResponse) {
const host = req.headers.host || "localhost";
const url = new URL(req.url || "/", `http://${host}`);
if (req.method === "GET" && url.pathname === "/health") {
sendJson(res, 200, { ok: true });
return;
}
if (req.method === "GET" && url.pathname === "/ping") {
sendJson(res, 200, { output: "pong" });
return;
}
if (req.method === "GET" && url.pathname === "/status") {
const output = await buildStatusOutput(deps);
sendJson(res, 200, { output });
return;
}
if (req.method === "GET" && url.pathname === "/wallet/status") {
await respond(res, async () => ({
output: await buildWalletDetails(deps),
}));
return;
}
if (req.method === "POST" && url.pathname === "/wallet/unlock") {
await respond(res, async () => {
const body = await readJsonBody(req);
const passphrase = getRequiredStringField(body, "passphrase");
const message = await deps.walletClient.unlock(passphrase);
const state = await deps.walletClient.getStatus();
return { output: { message, state } };
});
return;
}
if (req.method === "GET" && url.pathname === "/wallet/balance") {
await respond(res, async () => {
const balances = await deps.walletAdapter.getBalances();
return {
output: {
balances,
unit: "sat",
activeMint: deps.walletAdapter.getActiveMintUrl(),
walletState: "UNLOCKED",
},
};
});
return;
}
if (req.method === "POST" && url.pathname === "/wallet/receive/cashu") {
await respond(res, async () => {
const body = await readJsonBody(req);
const token = getRequiredStringField(body, "token");
const message = await deps.walletClient.receiveCashu(token);
const { amount, unit } = decodeCashuTokenAmount(token);
return { output: { message, amount, unit } };
});
return;
}
if (req.method === "POST" && url.pathname === "/wallet/receive/bolt11") {
await respond(res, async () => {
const body = await readJsonBody(req);
const amount = getRequiredPositiveNumberField(body, "amount");
const mintUrl = optionalStringField(body, "mintUrl");
const invoice = await deps.walletClient.receiveBolt11(amount, mintUrl);
return { output: { invoice, amount, mintUrl } };
});
return;
}
if (req.method === "POST" && url.pathname === "/wallet/send/cashu") {
await respond(res, async () => {
const body = await readJsonBody(req);
const amount = getRequiredPositiveNumberField(body, "amount");
const mintUrl = optionalStringField(body, "mintUrl");
const token = await deps.walletClient.sendCashu(amount, mintUrl);
return { output: { token, amount, mintUrl } };
});
return;
}
if (req.method === "POST" && url.pathname === "/wallet/send/bolt11") {
await respond(res, async () => {
const body = await readJsonBody(req);
const invoice = getRequiredStringField(body, "invoice");
const mintUrl = optionalStringField(body, "mintUrl");
const message = await deps.walletClient.sendBolt11(invoice, mintUrl);
return { output: { message, invoice, mintUrl } };
});
return;
}
if (req.method === "GET" && url.pathname === "/wallet/mints") {
await respond(res, async () => {
const mints = await deps.walletClient.listMints();
return {
output: {
mints,
activeMint: mints[0] || null,
},
};
});
return;
}
if (req.method === "POST" && url.pathname === "/wallet/mints") {
await respond(res, async () => {
const body = await readJsonBody(req);
const mintUrl = getRequiredStringField(body, "url");
const message = await deps.walletClient.addMint(mintUrl);
return { output: { message, url: mintUrl } };
});
return;
}
if (req.method === "POST" && url.pathname === "/wallet/mints/info") {
await respond(res, async () => {
const body = await readJsonBody(req);
const mintUrl = getRequiredStringField(body, "url");
const info = await deps.walletClient.getMintInfo(mintUrl);
return { output: { url: mintUrl, info } };
});
return;
}
if (req.method === "GET" && url.pathname === "/models") {
try {
const forceRefresh =
url.searchParams.get("refresh")?.toLowerCase() === "true";
const models = await deps.getRoutstr21Models(forceRefresh);
sendJson(res, 200, { output: { models } });
} catch (error) {
sendJson(res, 500, { error: toErrorMessage(error) });
}
return;
}
// Get providers for a specific model
const modelProvidersMatch = url.pathname.match(/^\/models\/([^\/]+)\/providers$/);
if (req.method === "GET" && modelProvidersMatch && modelProvidersMatch[1]) {
try {
const modelId = decodeURIComponent(modelProvidersMatch[1]);
const modelWithProviders = await deps.getModelProviders(modelId);
if (!modelWithProviders) {
sendJson(res, 404, { error: `Model '${modelId}' not found` });
return;
}
sendJson(res, 200, { output: modelWithProviders });
} catch (error) {
sendJson(res, 500, { error: toErrorMessage(error) });
}
return;
}
if (req.method === "GET" && url.pathname === "/v1/models") {
try {
const forceRefresh =
url.searchParams.get("refresh")?.toLowerCase() === "true";
const models = await deps.getRoutstr21Models(forceRefresh);
sendJson(res, 200, {
object: "list",
data: models.map((model) => ({ ...model, object: "model" })),
});
} catch (error) {
sendJson(res, 500, { error: toErrorMessage(error) });
}
return;
}
if (req.method === "POST" && url.pathname === "/stop") {
sendJson(res, 200, { output: "stopping" });
setTimeout(() => {
deps.server.close(() => {
process.exit(0);
});
}, 50);
return;
}
if (req.method === "POST" && url.pathname === "/refund") {
try {
const body = await readJsonBody(req);
const mintUrl = getRequiredStringField(body, "mintUrl");
const state = deps.store.getState();
const pendingDistribution = (state.cachedTokens || []).map(
(t: { baseUrl: string; balance?: number }) => ({
baseUrl: t.baseUrl,
amount: t.balance || 0,
}),
);
const apiKeysStored = (state.apiKeys || []).map(
(k: { baseUrl: string; balance?: number }) => ({
baseUrl: k.baseUrl,
amount: k.balance || 0,
}),
);
if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
sendJson(res, 200, {
output: { message: "No pending tokens to refund", results: [] },
});
return;
}
const refundBaseUrls = pendingDistribution
.map((p: { baseUrl: string }) => p.baseUrl)
.concat(apiKeysStored.map((p: { baseUrl: string }) => p.baseUrl));
const { RoutstrClient } = await import("@routstr/sdk");
const client = new RoutstrClient(
deps.walletAdapter,
deps.storageAdapter,
deps.providerRegistry,
"min",
"apikeys",
);
const spender = client.getCashuSpender();
const results = await spender.refundProviders(mintUrl, true);
sendJson(res, 200, {
output: {
message: `Refunded to ${mintUrl}`,
pendingTokens: pendingDistribution.length,
apiKeys: apiKeysStored.length,
results: results.map(
(r: { baseUrl: string; success: boolean }) => ({
baseUrl: r.baseUrl,
success: r.success,
}),
),
},
});
} catch (error) {
logger.error(`Refund error: ${toErrorMessage(error)}`);
respondWithError(res, error);
}
return;
}
if (req.method === "GET" && url.pathname === "/balance") {
try {
const balances = await deps.walletAdapter.getBalances();
sendJson(res, 200, {
output: {
balances,
unit: "sat",
activeMint: deps.walletAdapter.getActiveMintUrl(),
},
});
} catch (error) {
respondWithError(res, error);
}
return;
}
if (req.method === "GET" && url.pathname === "/keys/balance") {
try {
const walletBalances = await deps.walletAdapter.getBalances();
const totalWallet = Object.values(walletBalances).reduce<number>(
(sum, balance) => sum + Number(balance),
0,
);
const state = deps.store.getState();
const cachedTokens = state.cachedTokens || [];
const totalCached = cachedTokens.reduce(
(sum: number, t: { balance?: number }) => sum + (t.balance || 0),
0,
);
const apiKeys = state.apiKeys || [];
const totalApiKeys = apiKeys.reduce(
(sum: number, k: { balance?: number }) => sum + (k.balance || 0),
0,
);
const keys: Array<{ id: string; name: string; balance: number }> = [
{ id: "wallet", name: "Wallet", balance: totalWallet },
...cachedTokens.map((t: { baseUrl: string; balance?: number }) => ({
id: `cached:${t.baseUrl}`,
name: `Cached: ${t.baseUrl}`,
balance: t.balance || 0,
})),
...apiKeys.map((k: { baseUrl: string; balance?: number }) => ({
id: `apikey:${k.baseUrl}`,
name: `API Key: ${k.baseUrl}`,
balance: k.balance || 0,
})),
];
sendJson(res, 200, {
output: {
keys,
total: totalWallet + totalCached + totalApiKeys,
unit: "sat",
apikeysCalled: apiKeys.length,
},
});
} catch (error) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: String(error) }));
}
return;
}
if (req.method === "POST" && url.pathname === "/providers/disable") {
try {
const bodyText = await readBody(req);
const body = bodyText ? JSON.parse(bodyText) : {};
const indices = body.indices as number[] | undefined;
if (!Array.isArray(indices)) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error: "Missing or invalid 'indices' field (expected number[]).",
}),
);
return;
}
const state = deps.store.getState();
const baseUrlsList: string[] = state.baseUrlsList || [];
const disabledProviders: string[] = [
...(state.disabledProviders || []),
];
const toDisable: string[] = [];
for (const idx of indices) {
if (
typeof idx === "number" &&
idx >= 0 &&
idx < baseUrlsList.length
) {
const baseUrl = baseUrlsList[idx]!;
if (!disabledProviders.includes(baseUrl)) {
disabledProviders.push(baseUrl);
toDisable.push(baseUrl);
}
}
}
deps.store.getState().setDisabledProviders(disabledProviders);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
output: {
message: `Disabled ${toDisable.length} provider(s)`,
disabled: toDisable,
},
}),
);
} catch (error) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: String(error) }));
}
return;
}
if (req.method === "POST" && url.pathname === "/providers/enable") {
try {
const bodyText = await readBody(req);
const body = bodyText ? JSON.parse(bodyText) : {};
const indices = body.indices as number[] | undefined;
if (!Array.isArray(indices)) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error: "Missing or invalid 'indices' field (expected number[]).",
}),
);
return;
}
const state = deps.store.getState();
const baseUrlsList: string[] = state.baseUrlsList || [];
const disabledProviders: string[] = [
...(state.disabledProviders || []),
];
const toEnable: string[] = [];
for (const idx of indices) {
if (
typeof idx === "number" &&
idx >= 0 &&
idx < baseUrlsList.length
) {
const baseUrl = baseUrlsList[idx]!;
const pos = disabledProviders.indexOf(baseUrl);
if (pos !== -1) {
disabledProviders.splice(pos, 1);
toEnable.push(baseUrl);
}
}
}
deps.store.getState().setDisabledProviders(disabledProviders);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
output: {
message: `Enabled ${toEnable.length} provider(s)`,
enabled: toEnable,
},
}),
);
} catch (error) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: String(error) }));
}
return;
}
// Client management endpoints
if (req.method === "GET" && url.pathname === "/clients") {
try {
const clients = getClientsFromStore(deps.store).map((c) => ({
id: c.clientId,
name: c.name,
apiKey: c.apiKey,
createdAt: c.createdAt,
lastUsed: c.lastUsed,
ownerNpub: c.ownerNpub,
}));
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
output: {
clients,
totalCount: clients.length,
},
}),
);
} catch (error) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: String(error) }));
}
return;
}
if (req.method === "POST" && url.pathname === "/clients/add") {
try {
const bodyText = await readBody(req);
const body = bodyText ? JSON.parse(bodyText) : {};
const name = body.name as string | undefined;
const explicitId = body.id as string | undefined;
const ownerNpub = body.ownerNpub as string | undefined;
if (!name || typeof name !== "string" || name.trim() === "") {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error:
"Missing required 'name' field (must be a non-empty string).",
}),
);
return;
}
if (!explicitId || typeof explicitId !== "string" || explicitId.trim() === "") {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error:
"Missing required 'id' field (must be a non-empty string).",
}),
);
return;
}
const sanitizeId = (value: string) =>
value
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
const clientId = sanitizeId(explicitId);
if (!clientId) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error:
"Invalid client id. Must contain alphanumeric characters.",
}),
);
return;
}
const existingClients = getClientsFromStore(deps.store);
const existingClient = existingClients.find(
(c) => c.clientId === clientId,
);
if (existingClient) {
res.writeHead(409, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error: `Client with id '${clientId}' already exists.`,
}),
);
return;
}
const apiKey = generateApiKey();
const newClient = {
clientId,
name: name.trim(),
apiKey,
createdAt: Date.now(),
...(ownerNpub && typeof ownerNpub === "string" ? { ownerNpub } : {}),
};
deps.store
.getState()
.setClientIds((prev: typeof existingClients) => [
...(prev || []),
newClient,
]);
logger.log(`Added client '${name}' with id '${clientId}'`);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
output: {
message: `Client '${name}' added successfully`,
client: {
id: clientId,
name: name.trim(),
apiKey,
createdAt: newClient.createdAt,
ownerNpub: newClient.ownerNpub,
},
},
}),
);
} catch (error) {
respondWithError(res, error);
}
return;
}
if (req.method === "POST" && url.pathname === "/clients/delete") {
try {
const bodyText = await readBody(req);
const body = bodyText ? JSON.parse(bodyText) : {};
const id = body.id as string | undefined;
if (!id || typeof id !== "string" || id.trim() === "") {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error:
"Missing required 'id' field (must be a non-empty string).",
}),
);
return;
}
const existingClients = getClientsFromStore(deps.store);
const index = existingClients.findIndex(
(c) => c.clientId === id,
);
if (index === -1) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error: `Client with id '${id}' not found.`,
}),
);
return;
}
const removedClient = existingClients[index]!;
const updatedClients = existingClients.filter(
(_c: unknown, i: number) => i !== index,
);
deps.store.getState().setClientIds(updatedClients);
logger.log(
`Deleted client '${removedClient.name}' with id '${id}'`,
);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
output: {
message: `Client '${removedClient.name}' deleted successfully`,
id,
},
}),
);
} catch (error) {
respondWithError(res, error);
}
return;
}
if (req.method === "GET" && url.pathname === "/providers") {
try {
const state = deps.store.getState();
const baseUrlsList: string[] = state.baseUrlsList || [];
const disabledProviders: string[] = state.disabledProviders || [];
const providers = baseUrlsList.map((baseUrl, index) => ({
index,
baseUrl,
disabled: disabledProviders.includes(baseUrl),
}));
// Only count disabled providers that are actually in the current list
// (filter out stale entries from previously disabled providers that are no longer present)
const activeDisabledCount = providers.filter((p) => p.disabled).length;
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
output: {
providers,
disabledCount: activeDisabledCount,
totalCount: baseUrlsList.length,
},
}),
);
} catch (error) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: String(error) }));
}
return;
}
if (req.method === "GET" && url.pathname === "/usage") {
try {
const output = await deps.usageTrackingDriver.list({
limit: parseLimit(url.searchParams.get("limit")),
});
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ output }));
} catch (error) {
sendJson(res, 500, { error: toErrorMessage(error) });
}
return;
}
if (req.method === "GET" && url.pathname === "/usagePi") {
try {
const timestamp = (url.searchParams.get("timestamp") || "").trim();
if (!timestamp) {
sendJson(res, 400, {
error: "Missing required 'timestamp' query parameter.",
});
return;
}
const usageDriver = deps.usageTrackingDriver;
const limit = parseLimit(url.searchParams.get("limit"));
const allMatching = await usageDriver.list();
const requestIdPrefix = `gen-${timestamp}-`;
const filtered = allMatching.filter((entry) =>
entry.requestId.startsWith(requestIdPrefix),
);
const entries = filtered.slice(0, limit);
const totalEntries = filtered.length;
const totalSatsCost = filtered.reduce(
(sum, entry) => sum + (entry.satsCost || 0),
0,
);
const recentSatsCost = entries.reduce(
(sum, entry) => sum + (entry.satsCost || 0),
0,
);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
output: {
entries,
totalEntries,
totalSatsCost,
recentSatsCost,
limit,
timestamp,
},
}),
);
} catch (error) {
sendJson(res, 500, { error: toErrorMessage(error) });
}
return;
}
// Allow client management endpoints through
if (req.method !== "POST" && !url.pathname.startsWith("/clients")) {
res.writeHead(405, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Only POST is supported." }));
return;
}
let requestBody: unknown = {};
try {
requestBody = await readJsonBody(req);
} catch (error) {
sendJson(res, 400, {
error: "Invalid JSON body.",
details: toErrorMessage(error),
});
return;
}
const bodyObj = requestBody as Record<string, unknown>;
const modelId = typeof bodyObj.model === "string" ? bodyObj.model : "";
if (!modelId) {
sendJson(res, 400, { error: "Missing required 'model' field." });
return;
}
const forcedProvider: string | undefined =
url.searchParams.get("provider") ||
(req.headers["x-routstr-provider"] as string | undefined) ||
deps.provider ||
undefined;
// Convert req.headers to Record<string, string>
const incomingHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(req.headers)) {
if (typeof value === "string") {
incomingHeaders[key] = value;
} else if (Array.isArray(value) && value.length > 0) {
incomingHeaders[key] = value[0]!;
}
}
try {
await deps.ensureProvidersBootstrapped();
const reqId = randomBytes(4).toString("hex");
const reqLogger = sdkLogger.child(`req:${reqId}`);
logger.log(`[req:${reqId}] Routing request with path: `, url.pathname);
const response = await routeRequests({
modelId,
requestBody,
path: url.pathname,
forcedProvider,
headers: incomingHeaders,
walletAdapter: deps.walletAdapter,
storageAdapter: deps.storageAdapter,
providerRegistry: deps.providerRegistry,
discoveryAdapter: deps.discoveryAdapter,
modelManager: deps.modelManager,
debugLevel: "DEBUG",
mode: deps.mode,
usageTrackingDriver: deps.usageTrackingDriver,
sdkStore: deps.store,
providerManager: deps.providerManager,
logger: reqLogger,
...(deps.routstrPubkey ? { routstrPubkey: deps.routstrPubkey } : {}),
});
// Bridge the Web `Response` to the Node `ServerResponse` with no
// transforms: status + headers + pipe(body → res).
res.statusCode = response.status;
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
const finalize = (response as any).finalize as
| (() => Promise<number>)
| undefined;
if (!response.body) {
res.end();
if (finalize) {
try {
await finalize();
} catch (err) {
logger.error(`[daemon] finalize error: ${toErrorMessage(err)}`);
}
}
return;
}
const nodeReadable = Readable.fromWeb(response.body as any);
await new Promise<void>((resolve, reject) => {
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
resolve();
};
const fail = (err: unknown) => {
if (settled) return;
settled = true;
reject(err);
};
res.once("finish", finish);
res.once("close", finish);
res.once("error", fail);
nodeReadable.once("error", fail);
nodeReadable.pipe(res);
});
if (finalize) {
try {
await finalize();
} catch (err) {
logger.error(`[daemon] finalize error: ${toErrorMessage(err)}`);
}
}
return;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(`[daemon] Error: ${message}`);
if (error instanceof InsufficientBalanceError) {
const balanceError = error as {
required?: number;
available?: number;
maxMintBalance?: number;
maxMintUrl?: string;
};
res.writeHead(402, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error: message,
error_type: "insufficient_balance",
required: balanceError.required,
available: balanceError.available,
maxMintBalance: balanceError.maxMintBalance,
maxMintUrl: balanceError.maxMintUrl,
}),
);
return;
}
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: message }));
}
};
}