routstrd 0.1.0 → 0.1.3
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/SKILL.md +260 -0
- package/bun.lock +94 -38
- package/dist/daemon/index.js +35522 -0
- package/dist/index.js +4968 -382
- package/package.json +6 -5
- package/refund.js +33 -0
- package/refund_new.js +20 -0
- package/src/cli-shared.ts +8 -5
- package/src/cli.ts +462 -74
- package/src/daemon/http/index.ts +768 -140
- package/src/daemon/index.ts +106 -16
- package/src/daemon/wallet/cocod-client.ts +340 -0
- package/src/daemon/wallet/index.ts +56 -141
- package/src/integrations/index.ts +5 -3
- package/src/integrations/openclaw.ts +16 -26
- package/src/integrations/opencode.ts +15 -24
- package/src/integrations/pi.ts +15 -25
- package/src/integrations/registry.ts +71 -0
- package/src/start-daemon.ts +18 -13
- package/src/tui/usage/app.ts +1 -1
- package/src/tui/usage/data.ts +24 -14
- package/src/tui/usage/render.ts +10 -7
- package/src/utils/config.ts +1 -1
- package/src/utils/logger.ts +15 -4
- package/test_chat.sh +29 -0
- package/src/daemon/sse.ts +0 -98
package/src/start-daemon.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { LOG_FILE } from "./utils/config";
|
|
2
1
|
import { logger } from "./utils/logger";
|
|
3
|
-
import { existsSync
|
|
4
|
-
import {
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { LOGS_DIR } from "./utils/config";
|
|
4
|
+
|
|
5
|
+
function getTodayLogFile(): string {
|
|
6
|
+
const now = new Date();
|
|
7
|
+
const year = now.getFullYear();
|
|
8
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
9
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
10
|
+
return `${LOGS_DIR}/${year}-${month}-${day}.log`;
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
export async function startDaemon(
|
|
7
14
|
options: { port?: string; provider?: string } = {},
|
|
@@ -33,16 +40,14 @@ export async function startDaemon(
|
|
|
33
40
|
args.push("--provider", options.provider);
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
// Ensure
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
mkdirSync(logDir, { recursive: true });
|
|
43
|
+
// Ensure logs directory exists (logger handles date-based files)
|
|
44
|
+
if (!existsSync(LOGS_DIR)) {
|
|
45
|
+
await Bun.$`mkdir -p ${LOGS_DIR}`;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const shellCmd = `bun run "${daemonScript}" ${args.map(a => `'${a}'`).join(" ")} >> "${LOG_FILE}" 2>&1`;
|
|
48
|
+
const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
|
|
49
|
+
const todayLogFile = getTodayLogFile();
|
|
50
|
+
const shellCmd = `bun run "${daemonScript}" ${args.map(a => `'${a}'`).join(" ")} >> "${todayLogFile}" 2>&1`;
|
|
46
51
|
|
|
47
52
|
const proc = Bun.spawn(["sh", "-c", shellCmd], {
|
|
48
53
|
stdout: "inherit",
|
|
@@ -64,7 +69,7 @@ export async function startDaemon(
|
|
|
64
69
|
|
|
65
70
|
if (exitCode !== null) {
|
|
66
71
|
throw new Error(
|
|
67
|
-
`Daemon process exited early with code ${exitCode}. Check logs
|
|
72
|
+
`Daemon process exited early with code ${exitCode}. Check logs in ${LOGS_DIR}`,
|
|
68
73
|
);
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -85,6 +90,6 @@ export async function startDaemon(
|
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
throw new Error(
|
|
88
|
-
`Daemon failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds. Check logs
|
|
93
|
+
`Daemon failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds. Check logs in ${LOGS_DIR}`,
|
|
89
94
|
);
|
|
90
95
|
}
|
package/src/tui/usage/app.ts
CHANGED
|
@@ -87,7 +87,7 @@ export async function runUsageTui(): Promise<void> {
|
|
|
87
87
|
const height = getHeight();
|
|
88
88
|
|
|
89
89
|
if (forceFetch || shouldUpdate) {
|
|
90
|
-
stats = await fetchUsage(
|
|
90
|
+
stats = await fetchUsage(10000);
|
|
91
91
|
balance = await fetchBalance();
|
|
92
92
|
status = await fetchStatus();
|
|
93
93
|
shouldUpdate = false;
|
package/src/tui/usage/data.ts
CHANGED
|
@@ -74,7 +74,7 @@ export async function fetchBalance(): Promise<BalanceInfo | null> {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export async function fetchUsage(limit =
|
|
77
|
+
export async function fetchUsage(limit = 10000): Promise<UsageStats | null> {
|
|
78
78
|
try {
|
|
79
79
|
const running = await isDaemonRunning();
|
|
80
80
|
if (!running) return null;
|
|
@@ -82,20 +82,27 @@ export async function fetchUsage(limit = 1000): Promise<UsageStats | null> {
|
|
|
82
82
|
const result = await callDaemon(`/usage?limit=${limit}`);
|
|
83
83
|
if (result.error) return null;
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
// The daemon returns { output: [...] } where output is the entries array directly
|
|
86
|
+
const entries = result.output as UsageTrackingEntry[] | undefined;
|
|
87
|
+
const entriesArray = Array.isArray(entries) ? entries : [];
|
|
88
|
+
|
|
89
|
+
// Calculate totals from entries
|
|
90
|
+
const totals = entriesArray.reduce(
|
|
91
|
+
(acc, entry) => ({
|
|
92
|
+
promptTokens: acc.promptTokens + entry.promptTokens,
|
|
93
|
+
completionTokens: acc.completionTokens + entry.completionTokens,
|
|
94
|
+
totalTokens: acc.totalTokens + entry.totalTokens,
|
|
95
|
+
satsCost: acc.satsCost + entry.satsCost,
|
|
96
|
+
}),
|
|
97
|
+
{ promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 },
|
|
98
|
+
);
|
|
92
99
|
|
|
93
100
|
return {
|
|
94
|
-
entries:
|
|
95
|
-
totalEntries:
|
|
96
|
-
totalSatsCost:
|
|
97
|
-
recentSatsCost:
|
|
98
|
-
limit
|
|
101
|
+
entries: entriesArray,
|
|
102
|
+
totalEntries: entriesArray.length,
|
|
103
|
+
totalSatsCost: totals.satsCost,
|
|
104
|
+
recentSatsCost: totals.satsCost, // For now, recent = total since we don't have time window
|
|
105
|
+
limit,
|
|
99
106
|
};
|
|
100
107
|
} catch {
|
|
101
108
|
return null;
|
|
@@ -215,7 +222,10 @@ export function getClientStats(entries: UsageTrackingEntry[]): ClientStats[] {
|
|
|
215
222
|
return Array.from(clients.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
216
223
|
}
|
|
217
224
|
|
|
218
|
-
export function getTotals(entries: UsageTrackingEntry[]) {
|
|
225
|
+
export function getTotals(entries: UsageTrackingEntry[] | undefined) {
|
|
226
|
+
if (!entries || !Array.isArray(entries)) {
|
|
227
|
+
return { promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 };
|
|
228
|
+
}
|
|
219
229
|
return entries.reduce(
|
|
220
230
|
(acc, entry) => ({
|
|
221
231
|
promptTokens: acc.promptTokens + entry.promptTokens,
|
package/src/tui/usage/render.ts
CHANGED
|
@@ -116,15 +116,16 @@ export function renderBarChart(
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, status: StatusInfo | null, width: number): string {
|
|
119
|
+
// Use the server-calculated totals (all entries) instead of summing limited entries
|
|
119
120
|
const totals = getTotals(stats.entries);
|
|
120
|
-
const
|
|
121
|
-
const totalVisibleCost =
|
|
122
|
-
const avgCost =
|
|
123
|
-
const avgTokens =
|
|
121
|
+
const totalRequests = stats.totalEntries; // Use server's total count, not entries.length
|
|
122
|
+
const totalVisibleCost = stats.totalSatsCost; // <-- Use server's total, not client-side sum of limited entries
|
|
123
|
+
const avgCost = totalRequests > 0 ? totalVisibleCost / totalRequests : 0;
|
|
124
|
+
const avgTokens = totalRequests > 0 ? totals.totalTokens / totalRequests : 0;
|
|
124
125
|
|
|
125
126
|
const leftBox = [
|
|
126
127
|
`${COLORS.bold}Total Spent:${COLORS.reset} ${COLORS.green}${formatCost(totalVisibleCost)} sats${COLORS.reset}`,
|
|
127
|
-
`${COLORS.bold}Total Requests:${COLORS.reset} ${formatReqs(
|
|
128
|
+
`${COLORS.bold}Total Requests:${COLORS.reset} ${formatReqs(totalRequests)}`,
|
|
128
129
|
`${COLORS.bold}Avg Cost/Req:${COLORS.reset} ${formatCost(avgCost)} sats`,
|
|
129
130
|
];
|
|
130
131
|
|
|
@@ -344,7 +345,8 @@ export function renderModels(stats: UsageStats, width: number): string {
|
|
|
344
345
|
const modelStats = getModelStats(stats.entries);
|
|
345
346
|
if (modelStats.length === 0) return renderBox(["No model data available"], width, "Models");
|
|
346
347
|
|
|
347
|
-
|
|
348
|
+
// Use totalSatsCost (all-time) for percentage calculations to match header
|
|
349
|
+
const totalCost = stats.totalSatsCost;
|
|
348
350
|
const maxCost = modelStats[0]!.satsCost;
|
|
349
351
|
const maxModelLabel = Math.max(...modelStats.map((m) => m.modelId.length));
|
|
350
352
|
const lines: string[] = [];
|
|
@@ -431,7 +433,8 @@ export function renderClients(stats: UsageStats, width: number): string {
|
|
|
431
433
|
const clientStats = getClientStats(stats.entries);
|
|
432
434
|
if (clientStats.length === 0) return renderBox(["No client data available (API key auth not used)"], width, "Client Breakdown");
|
|
433
435
|
|
|
434
|
-
|
|
436
|
+
// Use totalSatsCost (all-time) for percentage calculations to match header
|
|
437
|
+
const totalCost = stats.totalSatsCost;
|
|
435
438
|
const maxCost = clientStats[0]!.satsCost;
|
|
436
439
|
const lines: string[] = [];
|
|
437
440
|
|
package/src/utils/config.ts
CHANGED
|
@@ -5,7 +5,7 @@ export const SOCKET_PATH = process.env.ROUTSTRD_SOCKET || `${CONFIG_DIR}/routstr
|
|
|
5
5
|
export const PID_FILE = process.env.ROUTSTRD_PID || `${CONFIG_DIR}/routstrd.pid`;
|
|
6
6
|
export const DB_PATH = `${CONFIG_DIR}/routstr.db`;
|
|
7
7
|
export const CONFIG_FILE = `${CONFIG_DIR}/config.json`;
|
|
8
|
-
export const
|
|
8
|
+
export const LOGS_DIR = `${CONFIG_DIR}/logs`;
|
|
9
9
|
|
|
10
10
|
export interface RoutstrdConfig {
|
|
11
11
|
port: number;
|
package/src/utils/logger.ts
CHANGED
|
@@ -4,11 +4,18 @@ import { join } from "path";
|
|
|
4
4
|
|
|
5
5
|
const HOME = process.env.HOME || process.env.USERPROFILE || "";
|
|
6
6
|
const LOG_DIR = process.env.ROUTSTRD_DIR || `${HOME}/.routstrd`;
|
|
7
|
-
const
|
|
7
|
+
const LOGS_DIR = join(LOG_DIR, "logs");
|
|
8
|
+
|
|
9
|
+
function getLogFileForDate(date: Date = new Date()): string {
|
|
10
|
+
const year = date.getFullYear();
|
|
11
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
12
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
13
|
+
return join(LOGS_DIR, `${year}-${month}-${day}.log`);
|
|
14
|
+
}
|
|
8
15
|
|
|
9
16
|
async function ensureLogDir() {
|
|
10
|
-
if (!existsSync(
|
|
11
|
-
await mkdir(
|
|
17
|
+
if (!existsSync(LOGS_DIR)) {
|
|
18
|
+
await mkdir(LOGS_DIR, { recursive: true });
|
|
12
19
|
}
|
|
13
20
|
}
|
|
14
21
|
|
|
@@ -31,8 +38,9 @@ async function writeLog(level: string, ...args: unknown[]) {
|
|
|
31
38
|
})
|
|
32
39
|
.join(" ");
|
|
33
40
|
const line = `[${timestamp}] [${level}] ${message}\n`;
|
|
41
|
+
const logFile = getLogFileForDate(new Date(timestamp));
|
|
34
42
|
try {
|
|
35
|
-
await appendFile(
|
|
43
|
+
await appendFile(logFile, line);
|
|
36
44
|
} catch (error) {
|
|
37
45
|
console.error("Failed to write log:", error);
|
|
38
46
|
}
|
|
@@ -43,6 +51,9 @@ export const logger = {
|
|
|
43
51
|
console.log(...args);
|
|
44
52
|
writeLog("INFO", ...args);
|
|
45
53
|
},
|
|
54
|
+
debug: (...args: unknown[]) => {
|
|
55
|
+
writeLog("DEBUG", ...args);
|
|
56
|
+
},
|
|
46
57
|
error: (...args: unknown[]) => {
|
|
47
58
|
console.error(...args);
|
|
48
59
|
writeLog("ERROR", ...args);
|
package/test_chat.sh
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
AUTH="Bearer sk-a2d0981bd84ebf214f4bfb861a273873bfe3e10e6af97533"
|
|
4
|
+
BASE_URL="http://localhost:8008/v1/chat/completions"
|
|
5
|
+
|
|
6
|
+
echo "=== Testing GPT-4 ==="
|
|
7
|
+
curl -s "$BASE_URL" \
|
|
8
|
+
-H "Content-Type: application/json" \
|
|
9
|
+
-H "Authorization: $AUTH" \
|
|
10
|
+
-d '{
|
|
11
|
+
"model": "gpt-4",
|
|
12
|
+
"messages": [
|
|
13
|
+
{"role": "user", "content": "Hello, how are you?"}
|
|
14
|
+
],
|
|
15
|
+
"max_tokens": 128
|
|
16
|
+
}' | python3 -m json.tool
|
|
17
|
+
|
|
18
|
+
echo ""
|
|
19
|
+
echo "=== Testing GLM-4.7 ==="
|
|
20
|
+
curl -s "$BASE_URL" \
|
|
21
|
+
-H "Content-Type: application/json" \
|
|
22
|
+
-H "Authorization: $AUTH" \
|
|
23
|
+
-d '{
|
|
24
|
+
"model": "glm-4.7",
|
|
25
|
+
"messages": [
|
|
26
|
+
{"role": "user", "content": "Hello, how are you?"}
|
|
27
|
+
],
|
|
28
|
+
"max_tokens": 128
|
|
29
|
+
}' | python3 -m json.tool
|
package/src/daemon/sse.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { Transform } from "stream";
|
|
2
|
-
import type { UsageData } from "./types";
|
|
3
|
-
|
|
4
|
-
export function createSSEParserTransform(
|
|
5
|
-
onUsage: (usage: UsageData) => void,
|
|
6
|
-
onResponseId?: (responseId: string) => void,
|
|
7
|
-
): Transform {
|
|
8
|
-
let buffer = "";
|
|
9
|
-
|
|
10
|
-
const maybeCaptureUsageFromJson = (jsonText: string): void => {
|
|
11
|
-
try {
|
|
12
|
-
const data = JSON.parse(jsonText) as any;
|
|
13
|
-
const responseId = data.id;
|
|
14
|
-
if (typeof responseId === "string" && responseId.trim().length > 0) {
|
|
15
|
-
onResponseId?.(responseId.trim());
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (data.usage) {
|
|
19
|
-
const usageCost = data.usage.cost;
|
|
20
|
-
const cost =
|
|
21
|
-
typeof usageCost === "number"
|
|
22
|
-
? usageCost
|
|
23
|
-
: usageCost?.total_usd ??
|
|
24
|
-
data.metadata?.routstr?.cost?.total_usd ??
|
|
25
|
-
0;
|
|
26
|
-
const msats =
|
|
27
|
-
data.metadata?.routstr?.cost?.total_msats ??
|
|
28
|
-
(typeof data.usage.cost_sats === "number"
|
|
29
|
-
? data.usage.cost_sats * 1000
|
|
30
|
-
: 0);
|
|
31
|
-
onUsage({
|
|
32
|
-
promptTokens: data.usage.prompt_tokens ?? 0,
|
|
33
|
-
completionTokens: data.usage.completion_tokens ?? 0,
|
|
34
|
-
totalTokens: data.usage.total_tokens ?? 0,
|
|
35
|
-
cost,
|
|
36
|
-
satsCost: msats / 1000,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
} catch {
|
|
40
|
-
// Ignore non-JSON lines/events.
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const processLine = (self: Transform, line: string): void => {
|
|
45
|
-
const trimmed = line.trim();
|
|
46
|
-
if (!trimmed) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (trimmed === "data: [DONE]" || trimmed === "[DONE]") {
|
|
51
|
-
self.push("data: [DONE]\n\n");
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (trimmed.startsWith("data:")) {
|
|
56
|
-
const dataStr = trimmed.startsWith("data: ")
|
|
57
|
-
? trimmed.slice(6)
|
|
58
|
-
: trimmed.slice(5).trimStart();
|
|
59
|
-
if (dataStr === "[DONE]") {
|
|
60
|
-
self.push("data: [DONE]\n\n");
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
maybeCaptureUsageFromJson(dataStr);
|
|
64
|
-
self.push(`data: ${dataStr}\n\n`);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (trimmed.startsWith("{")) {
|
|
69
|
-
maybeCaptureUsageFromJson(trimmed);
|
|
70
|
-
self.push(`data: ${trimmed}\n\n`);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
self.push(line + "\n");
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
return new Transform({
|
|
78
|
-
transform(chunk, encoding, callback) {
|
|
79
|
-
buffer += chunk.toString();
|
|
80
|
-
|
|
81
|
-
const lines = buffer.split(/\r?\n/);
|
|
82
|
-
buffer = lines.pop() || "";
|
|
83
|
-
|
|
84
|
-
for (const line of lines) {
|
|
85
|
-
processLine(this, line);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
callback();
|
|
89
|
-
},
|
|
90
|
-
flush(callback) {
|
|
91
|
-
if (buffer.trim()) {
|
|
92
|
-
processLine(this, buffer);
|
|
93
|
-
}
|
|
94
|
-
buffer = "";
|
|
95
|
-
callback();
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
}
|