routstrd 0.1.1 → 0.1.4

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.
@@ -1,7 +1,14 @@
1
- import { LOG_FILE } from "./utils/config";
2
1
  import { logger } from "./utils/logger";
3
- import { existsSync, mkdirSync } from "fs";
4
- import { dirname } from "path";
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 log directory exists
37
- const logDir = dirname(LOG_FILE);
38
- if (!existsSync(logDir)) {
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
- // Use shell redirection to append stdout/stderr to log file
43
- // Bun.file() overwrites, so we need shell >> for appending
44
48
  const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
45
- const shellCmd = `bun run "${daemonScript}" ${args.map(a => `'${a}'`).join(" ")} >> "${LOG_FILE}" 2>&1`;
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 at ${LOG_FILE}`,
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 at ${LOG_FILE}`,
93
+ `Daemon failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds. Check logs in ${LOGS_DIR}`,
89
94
  );
90
95
  }
@@ -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(1000);
90
+ stats = await fetchUsage(10000);
91
91
  balance = await fetchBalance();
92
92
  status = await fetchStatus();
93
93
  shouldUpdate = false;
@@ -74,7 +74,7 @@ export async function fetchBalance(): Promise<BalanceInfo | null> {
74
74
  }
75
75
  }
76
76
 
77
- export async function fetchUsage(limit = 1000): Promise<UsageStats | null> {
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
- const output = result.output as {
86
- entries?: UsageTrackingEntry[];
87
- totalEntries?: number;
88
- totalSatsCost?: number;
89
- recentSatsCost?: number;
90
- limit?: number;
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: output?.entries || [],
95
- totalEntries: output?.totalEntries || 0,
96
- totalSatsCost: output?.totalSatsCost || 0,
97
- recentSatsCost: output?.recentSatsCost || 0,
98
- limit: output?.limit || 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,
@@ -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 entryCount = stats.entries.length;
121
- const totalVisibleCost = totals.satsCost;
122
- const avgCost = entryCount > 0 ? totalVisibleCost / entryCount : 0;
123
- const avgTokens = entryCount > 0 ? totals.totalTokens / entryCount : 0;
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(entryCount)}`,
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
- const totalCost = getTotals(stats.entries).satsCost;
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
- const totalCost = getTotals(stats.entries).satsCost;
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
 
@@ -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 LOG_FILE = `${CONFIG_DIR}/routstrd.log`;
8
+ export const LOGS_DIR = `${CONFIG_DIR}/logs`;
9
9
 
10
10
  export interface RoutstrdConfig {
11
11
  port: number;
@@ -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 LOG_FILE = join(LOG_DIR, "routstrd.log");
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(LOG_DIR)) {
11
- await mkdir(LOG_DIR, { recursive: true });
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(LOG_FILE, line);
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
- }