routstrd 0.4.4 → 0.4.5
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/bun.lock +316 -2
- package/dist/daemon/index.js +857 -837
- package/dist/index.js +127 -8
- package/package.json +1 -1
- package/src/cli.test.ts +138 -1
- package/src/cli.ts +226 -34
package/dist/index.js
CHANGED
|
@@ -27170,7 +27170,7 @@ zoo`.split(`
|
|
|
27170
27170
|
// package.json
|
|
27171
27171
|
var package_default = {
|
|
27172
27172
|
name: "routstrd",
|
|
27173
|
-
version: "0.4.
|
|
27173
|
+
version: "0.4.5",
|
|
27174
27174
|
module: "src/index.ts",
|
|
27175
27175
|
type: "module",
|
|
27176
27176
|
private: false,
|
|
@@ -28588,14 +28588,17 @@ function getLogFileForDate2(logsDir, date = new Date) {
|
|
|
28588
28588
|
const day = String(date.getDate()).padStart(2, "0");
|
|
28589
28589
|
return `${logsDir}/${year}-${month}-${day}.log`;
|
|
28590
28590
|
}
|
|
28591
|
-
function
|
|
28591
|
+
function readAllLines(file) {
|
|
28592
28592
|
const content = readFileSync5(file, "utf8");
|
|
28593
28593
|
const allLines = content.replace(/\r\n/g, `
|
|
28594
28594
|
`).split(`
|
|
28595
28595
|
`);
|
|
28596
28596
|
if (allLines.at(-1) === "")
|
|
28597
28597
|
allLines.pop();
|
|
28598
|
-
return allLines
|
|
28598
|
+
return allLines;
|
|
28599
|
+
}
|
|
28600
|
+
function readLastLines(file, lines) {
|
|
28601
|
+
return readAllLines(file).slice(-lines).join(`
|
|
28599
28602
|
`);
|
|
28600
28603
|
}
|
|
28601
28604
|
async function followLogFile(file, lines) {
|
|
@@ -28621,8 +28624,102 @@ async function followLogFile(file, lines) {
|
|
|
28621
28624
|
position = size;
|
|
28622
28625
|
}
|
|
28623
28626
|
}
|
|
28624
|
-
|
|
28627
|
+
function parseStructuredLogLine(line) {
|
|
28628
|
+
const out = { isRouting: false };
|
|
28629
|
+
const head = line.match(/^\[([^\]]+)\]\s+\[([^\]]+)\]/);
|
|
28630
|
+
if (head) {
|
|
28631
|
+
out.timestamp = head[1];
|
|
28632
|
+
out.level = head[2];
|
|
28633
|
+
}
|
|
28634
|
+
const req = line.match(/\[req:([^\]\:]+)/);
|
|
28635
|
+
if (req) {
|
|
28636
|
+
out.requestId = req[1].trim();
|
|
28637
|
+
}
|
|
28638
|
+
const model = line.match(/\[model:([^\]\:]+)\]/);
|
|
28639
|
+
if (model) {
|
|
28640
|
+
out.modelId = model[1].trim();
|
|
28641
|
+
} else {
|
|
28642
|
+
const jsonModel = line.match(/"modelId"\s*:\s*"([^"]+)"/);
|
|
28643
|
+
if (jsonModel) {
|
|
28644
|
+
out.modelId = jsonModel[1];
|
|
28645
|
+
}
|
|
28646
|
+
}
|
|
28647
|
+
out.isRouting = !!out.requestId && line.includes("Routing request with path");
|
|
28648
|
+
return out;
|
|
28649
|
+
}
|
|
28650
|
+
function collectRecentRequestsFromLines(lines, limit) {
|
|
28651
|
+
const result = [];
|
|
28652
|
+
const seen = new Set;
|
|
28653
|
+
for (let i2 = lines.length - 1;i2 >= 0; i2--) {
|
|
28654
|
+
const parsed = parseStructuredLogLine(lines[i2]);
|
|
28655
|
+
if (!parsed.isRouting || !parsed.requestId)
|
|
28656
|
+
continue;
|
|
28657
|
+
if (seen.has(parsed.requestId))
|
|
28658
|
+
continue;
|
|
28659
|
+
seen.add(parsed.requestId);
|
|
28660
|
+
result.push({
|
|
28661
|
+
requestId: parsed.requestId,
|
|
28662
|
+
modelId: parsed.modelId ?? "unknown",
|
|
28663
|
+
timestamp: parsed.timestamp
|
|
28664
|
+
});
|
|
28665
|
+
if (result.length >= limit)
|
|
28666
|
+
break;
|
|
28667
|
+
}
|
|
28668
|
+
const byRequestId = new Map(result.map((r) => [r.requestId, r]));
|
|
28669
|
+
for (const line of lines) {
|
|
28670
|
+
const parsed = parseStructuredLogLine(line);
|
|
28671
|
+
if (!parsed.requestId || !parsed.modelId)
|
|
28672
|
+
continue;
|
|
28673
|
+
const record = byRequestId.get(parsed.requestId);
|
|
28674
|
+
if (record && record.modelId === "unknown") {
|
|
28675
|
+
record.modelId = parsed.modelId;
|
|
28676
|
+
}
|
|
28677
|
+
}
|
|
28678
|
+
return result;
|
|
28679
|
+
}
|
|
28680
|
+
function collectRecentRequests(files, limit) {
|
|
28681
|
+
const result = [];
|
|
28682
|
+
const seen = new Set;
|
|
28683
|
+
for (const file of [...files].reverse()) {
|
|
28684
|
+
if (!existsSync14(file))
|
|
28685
|
+
continue;
|
|
28686
|
+
const remaining = limit - result.length;
|
|
28687
|
+
if (remaining <= 0)
|
|
28688
|
+
break;
|
|
28689
|
+
for (const request of collectRecentRequestsFromLines(readAllLines(file), remaining)) {
|
|
28690
|
+
if (seen.has(request.requestId))
|
|
28691
|
+
continue;
|
|
28692
|
+
seen.add(request.requestId);
|
|
28693
|
+
result.push(request);
|
|
28694
|
+
if (result.length >= limit)
|
|
28695
|
+
return result;
|
|
28696
|
+
}
|
|
28697
|
+
}
|
|
28698
|
+
return result;
|
|
28699
|
+
}
|
|
28700
|
+
function filterLinesByRequestId(files, requestId) {
|
|
28701
|
+
const matches = [];
|
|
28702
|
+
for (const file of files) {
|
|
28703
|
+
if (!existsSync14(file))
|
|
28704
|
+
continue;
|
|
28705
|
+
for (const line of readAllLines(file)) {
|
|
28706
|
+
if (parseStructuredLogLine(line).requestId === requestId) {
|
|
28707
|
+
matches.push(line);
|
|
28708
|
+
}
|
|
28709
|
+
}
|
|
28710
|
+
}
|
|
28711
|
+
return matches;
|
|
28712
|
+
}
|
|
28713
|
+
function parseLinesOption(value) {
|
|
28714
|
+
const parsed = Number.parseInt(value, 10);
|
|
28715
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 50;
|
|
28716
|
+
}
|
|
28717
|
+
program.command("logs").description("View daemon logs").option("-f, --follow", "Follow log output", false).option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false).option("-n, --lines <number>", "Number of lines to show", "50").option("-r, --recent", "List recent request ids with their model", false).option("-i, --request-id <id>", "Only show log lines for a specific request id").action(async (options) => {
|
|
28625
28718
|
await requireLocalDaemon();
|
|
28719
|
+
if (options.recent && options.requestId) {
|
|
28720
|
+
console.error("Cannot combine --recent (-r) with --request-id (-i).");
|
|
28721
|
+
process.exit(1);
|
|
28722
|
+
}
|
|
28626
28723
|
const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR2;
|
|
28627
28724
|
const todayFile = getLogFileForDate2(logsDir);
|
|
28628
28725
|
const yesterday = new Date;
|
|
@@ -28633,10 +28730,32 @@ program.command("logs").description("View daemon logs").option("-f, --follow", "
|
|
|
28633
28730
|
console.log(`Logs directory: ${logsDir}`);
|
|
28634
28731
|
process.exit(1);
|
|
28635
28732
|
}
|
|
28636
|
-
const lines =
|
|
28637
|
-
const logFiles = [yesterdayFile, todayFile].filter((file, index, files) =>
|
|
28638
|
-
|
|
28639
|
-
|
|
28733
|
+
const lines = parseLinesOption(options.lines);
|
|
28734
|
+
const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => existsSync14(file) && files.indexOf(file) === index);
|
|
28735
|
+
if (options.recent) {
|
|
28736
|
+
const requests = collectRecentRequests(logFiles, lines);
|
|
28737
|
+
if (requests.length === 0) {
|
|
28738
|
+
console.log("No requests found in recent logs.");
|
|
28739
|
+
return;
|
|
28740
|
+
}
|
|
28741
|
+
console.log("Recent requests (newest first):");
|
|
28742
|
+
for (const request of requests) {
|
|
28743
|
+
const when = request.timestamp ? ` ${request.timestamp}` : "";
|
|
28744
|
+
console.log(` ${request.requestId} ${request.modelId}${when}`);
|
|
28745
|
+
}
|
|
28746
|
+
return;
|
|
28747
|
+
}
|
|
28748
|
+
if (options.requestId) {
|
|
28749
|
+
const matches = filterLinesByRequestId(logFiles, options.requestId);
|
|
28750
|
+
if (matches.length === 0) {
|
|
28751
|
+
console.log(`No log lines found for request '${options.requestId}'.`);
|
|
28752
|
+
return;
|
|
28753
|
+
}
|
|
28754
|
+
for (const line of matches) {
|
|
28755
|
+
console.log(line);
|
|
28756
|
+
}
|
|
28757
|
+
return;
|
|
28758
|
+
}
|
|
28640
28759
|
if (options.follow) {
|
|
28641
28760
|
if (existsSync14(todayFile)) {
|
|
28642
28761
|
await followLogFile(todayFile, lines);
|
package/package.json
CHANGED
package/src/cli.test.ts
CHANGED
|
@@ -2,7 +2,11 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
collectRecentRequestsFromLines,
|
|
7
|
+
initializeWallet,
|
|
8
|
+
parseStructuredLogLine,
|
|
9
|
+
} from "./cli";
|
|
6
10
|
|
|
7
11
|
const tempDirs: string[] = [];
|
|
8
12
|
|
|
@@ -22,6 +26,139 @@ afterEach(() => {
|
|
|
22
26
|
}
|
|
23
27
|
});
|
|
24
28
|
|
|
29
|
+
describe("parseStructuredLogLine", () => {
|
|
30
|
+
test("extracts timestamp, level, request id, model, and routing flag", () => {
|
|
31
|
+
const line =
|
|
32
|
+
"[2026-08-27T13:00:00.000Z] [INFO] [req:a1b2c3d4] [model:gpt-5] Routing request with path: /v1/chat/completions";
|
|
33
|
+
|
|
34
|
+
expect(parseStructuredLogLine(line)).toEqual({
|
|
35
|
+
timestamp: "2026-08-27T13:00:00.000Z",
|
|
36
|
+
level: "INFO",
|
|
37
|
+
requestId: "a1b2c3d4",
|
|
38
|
+
modelId: "gpt-5",
|
|
39
|
+
isRouting: true,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("parses SDK child lines without marking them as routing lines", () => {
|
|
44
|
+
const line =
|
|
45
|
+
"[2026-08-27T13:00:01.000Z] [DEBUG] [req:a1b2c3d4] [model:gpt-5] [BalanceManager] token stuff";
|
|
46
|
+
|
|
47
|
+
expect(parseStructuredLogLine(line)).toEqual({
|
|
48
|
+
timestamp: "2026-08-27T13:00:01.000Z",
|
|
49
|
+
level: "DEBUG",
|
|
50
|
+
requestId: "a1b2c3d4",
|
|
51
|
+
modelId: "gpt-5",
|
|
52
|
+
isRouting: false,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("handles legacy lines that have a request id but no model tag", () => {
|
|
57
|
+
const line =
|
|
58
|
+
"[2026-08-27T13:00:02.000Z] [INFO] [req:deadbeef] Routing request with path: /v1/responses";
|
|
59
|
+
|
|
60
|
+
expect(parseStructuredLogLine(line)).toEqual({
|
|
61
|
+
timestamp: "2026-08-27T13:00:02.000Z",
|
|
62
|
+
level: "INFO",
|
|
63
|
+
requestId: "deadbeef",
|
|
64
|
+
modelId: undefined,
|
|
65
|
+
isRouting: true,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("extracts the bare request id from colon-joined legacy child tags", () => {
|
|
70
|
+
const line =
|
|
71
|
+
"[2026-08-27T13:00:04.000Z] [INFO] [req:deadbeef:RoutstrClient] [RoutstrClient] generic request pricing input {\"modelId\":\"deepseek-v4-flash-0731\"}";
|
|
72
|
+
|
|
73
|
+
const parsed = parseStructuredLogLine(line);
|
|
74
|
+
expect(parsed.requestId).toBe("deadbeef");
|
|
75
|
+
expect(parsed.modelId).toBe("deepseek-v4-flash-0731");
|
|
76
|
+
expect(parsed.isRouting).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("falls back to the JSON modelId when no model tag is present", () => {
|
|
80
|
+
const line =
|
|
81
|
+
"[2026-08-27T13:00:05.000Z] [INFO] [req:deadbeef:RoutstrClient] [RoutstrClient] generic request pricing input {\"modelId\":\"deepseek-v4-pro-0813\",\"messageCount\":121}";
|
|
82
|
+
|
|
83
|
+
expect(parseStructuredLogLine(line).modelId).toBe("deepseek-v4-pro-0813");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("returns an empty record for plain non-request lines", () => {
|
|
87
|
+
const line = "[2026-08-27T13:00:03.000Z] [WARN] Some startup message";
|
|
88
|
+
|
|
89
|
+
expect(parseStructuredLogLine(line)).toEqual({
|
|
90
|
+
timestamp: "2026-08-27T13:00:03.000Z",
|
|
91
|
+
level: "WARN",
|
|
92
|
+
requestId: undefined,
|
|
93
|
+
modelId: undefined,
|
|
94
|
+
isRouting: false,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("collectRecentRequestsFromLines", () => {
|
|
100
|
+
const lines = [
|
|
101
|
+
"[2026-08-27T13:00:00.000Z] [INFO] [req:11111111] [model:gpt-5] Routing request with path: /a",
|
|
102
|
+
"[2026-08-27T13:00:01.000Z] [DEBUG] [req:11111111] [model:gpt-5] [BalanceManager] token",
|
|
103
|
+
"[2026-08-27T13:00:02.000Z] [INFO] [req:22222222] [model:claude-sonnet-4.5] Routing request with path: /b",
|
|
104
|
+
"[2026-08-27T13:00:03.000Z] [DEBUG] [req:22222222] [model:claude-sonnet-4.5] [ModelManager] refresh",
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
test("returns one record per request, newest first", () => {
|
|
108
|
+
expect(collectRecentRequestsFromLines(lines, 10)).toEqual([
|
|
109
|
+
{
|
|
110
|
+
requestId: "22222222",
|
|
111
|
+
modelId: "claude-sonnet-4.5",
|
|
112
|
+
timestamp: "2026-08-27T13:00:02.000Z",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
requestId: "11111111",
|
|
116
|
+
modelId: "gpt-5",
|
|
117
|
+
timestamp: "2026-08-27T13:00:00.000Z",
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("respects the limit", () => {
|
|
123
|
+
expect(collectRecentRequestsFromLines(lines, 1)).toEqual([
|
|
124
|
+
{
|
|
125
|
+
requestId: "22222222",
|
|
126
|
+
modelId: "claude-sonnet-4.5",
|
|
127
|
+
timestamp: "2026-08-27T13:00:02.000Z",
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("defaults the model to unknown for legacy routing lines", () => {
|
|
133
|
+
const legacy = [
|
|
134
|
+
"[2026-08-27T13:00:00.000Z] [INFO] [req:deadbeef] Routing request with path: /a",
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
expect(collectRecentRequestsFromLines(legacy, 10)).toEqual([
|
|
138
|
+
{
|
|
139
|
+
requestId: "deadbeef",
|
|
140
|
+
modelId: "unknown",
|
|
141
|
+
timestamp: "2026-08-27T13:00:00.000Z",
|
|
142
|
+
},
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("enriches the model from a sibling pricing line in legacy logs", () => {
|
|
147
|
+
const legacy = [
|
|
148
|
+
"[2026-08-27T13:00:00.000Z] [INFO] [req:deadbeef] Routing request with path: /a",
|
|
149
|
+
"[2026-08-27T13:00:01.000Z] [INFO] [req:deadbeef:RoutstrClient] [RoutstrClient] generic request pricing input {\"modelId\":\"deepseek-v4-flash-0731\"}",
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
expect(collectRecentRequestsFromLines(legacy, 10)).toEqual([
|
|
153
|
+
{
|
|
154
|
+
requestId: "deadbeef",
|
|
155
|
+
modelId: "deepseek-v4-flash-0731",
|
|
156
|
+
timestamp: "2026-08-27T13:00:00.000Z",
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
25
162
|
describe("initializeWallet", () => {
|
|
26
163
|
test("creates the wallet directory and config with restrictive permissions", () => {
|
|
27
164
|
const walletDir = join(makeTempDir(), "wallet");
|
package/src/cli.ts
CHANGED
|
@@ -2268,11 +2268,15 @@ function getLogFileForDate(logsDir: string, date: Date = new Date()): string {
|
|
|
2268
2268
|
return `${logsDir}/${year}-${month}-${day}.log`;
|
|
2269
2269
|
}
|
|
2270
2270
|
|
|
2271
|
-
function
|
|
2271
|
+
function readAllLines(file: string): string[] {
|
|
2272
2272
|
const content = readFileSync(file, "utf8");
|
|
2273
2273
|
const allLines = content.replace(/\r\n/g, "\n").split("\n");
|
|
2274
2274
|
if (allLines.at(-1) === "") allLines.pop();
|
|
2275
|
-
return allLines
|
|
2275
|
+
return allLines;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
function readLastLines(file: string, lines: number): string {
|
|
2279
|
+
return readAllLines(file).slice(-lines).join("\n");
|
|
2276
2280
|
}
|
|
2277
2281
|
|
|
2278
2282
|
async function followLogFile(file: string, lines: number): Promise<void> {
|
|
@@ -2302,51 +2306,239 @@ async function followLogFile(file: string, lines: number): Promise<void> {
|
|
|
2302
2306
|
}
|
|
2303
2307
|
}
|
|
2304
2308
|
|
|
2309
|
+
type StructuredLogLine = {
|
|
2310
|
+
timestamp?: string;
|
|
2311
|
+
level?: string;
|
|
2312
|
+
requestId?: string;
|
|
2313
|
+
modelId?: string;
|
|
2314
|
+
isRouting: boolean;
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2317
|
+
export function parseStructuredLogLine(line: string): StructuredLogLine {
|
|
2318
|
+
const out: StructuredLogLine = { isRouting: false };
|
|
2319
|
+
|
|
2320
|
+
const head = line.match(/^\[([^\]]+)\]\s+\[([^\]]+)\]/);
|
|
2321
|
+
if (head) {
|
|
2322
|
+
out.timestamp = head[1];
|
|
2323
|
+
out.level = head[2];
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// Request ids are hex (no colons). Stop at the first ':' so legacy
|
|
2327
|
+
// colon-joined child tags like `[req:2c30d5ed:RoutstrClient]` still yield
|
|
2328
|
+
// the bare request id.
|
|
2329
|
+
const req = line.match(/\[req:([^\]\:]+)/);
|
|
2330
|
+
if (req) {
|
|
2331
|
+
out.requestId = req[1]!.trim();
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// Prefer an explicit `[model:...]` tag; fall back to the modelId embedded
|
|
2335
|
+
// in the SDK's `generic request pricing input {"modelId":"..."}` line.
|
|
2336
|
+
const model = line.match(/\[model:([^\]\:]+)\]/);
|
|
2337
|
+
if (model) {
|
|
2338
|
+
out.modelId = model[1]!.trim();
|
|
2339
|
+
} else {
|
|
2340
|
+
const jsonModel = line.match(/"modelId"\s*:\s*"([^"]+)"/);
|
|
2341
|
+
if (jsonModel) {
|
|
2342
|
+
out.modelId = jsonModel[1];
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
out.isRouting = !!out.requestId && line.includes("Routing request with path");
|
|
2347
|
+
return out;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
type RequestSummary = {
|
|
2351
|
+
requestId: string;
|
|
2352
|
+
modelId: string;
|
|
2353
|
+
timestamp?: string;
|
|
2354
|
+
};
|
|
2355
|
+
|
|
2356
|
+
/**
|
|
2357
|
+
* Scan log lines newest-first and collect one record per request, keyed off
|
|
2358
|
+
* the "Routing request with path" line (the canonical per-request entry).
|
|
2359
|
+
* The model is enriched in a second pass from any line belonging to the
|
|
2360
|
+
* request (the SDK pricing line embeds modelId even in legacy logs).
|
|
2361
|
+
*/
|
|
2362
|
+
export function collectRecentRequestsFromLines(
|
|
2363
|
+
lines: string[],
|
|
2364
|
+
limit: number,
|
|
2365
|
+
): RequestSummary[] {
|
|
2366
|
+
const result: RequestSummary[] = [];
|
|
2367
|
+
const seen = new Set<string>();
|
|
2368
|
+
|
|
2369
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
2370
|
+
const parsed = parseStructuredLogLine(lines[i]!);
|
|
2371
|
+
if (!parsed.isRouting || !parsed.requestId) continue;
|
|
2372
|
+
if (seen.has(parsed.requestId)) continue;
|
|
2373
|
+
seen.add(parsed.requestId);
|
|
2374
|
+
|
|
2375
|
+
result.push({
|
|
2376
|
+
requestId: parsed.requestId,
|
|
2377
|
+
modelId: parsed.modelId ?? "unknown",
|
|
2378
|
+
timestamp: parsed.timestamp,
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
if (result.length >= limit) break;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// Second pass: fill in the model for requests whose routing line predates
|
|
2385
|
+
// the `[model:...]` tag but which logged `{"modelId":"..."}` later on.
|
|
2386
|
+
const byRequestId = new Map(result.map((r) => [r.requestId, r]));
|
|
2387
|
+
for (const line of lines) {
|
|
2388
|
+
const parsed = parseStructuredLogLine(line);
|
|
2389
|
+
if (!parsed.requestId || !parsed.modelId) continue;
|
|
2390
|
+
const record = byRequestId.get(parsed.requestId);
|
|
2391
|
+
if (record && record.modelId === "unknown") {
|
|
2392
|
+
record.modelId = parsed.modelId;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
return result;
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
function collectRecentRequests(
|
|
2400
|
+
files: string[],
|
|
2401
|
+
limit: number,
|
|
2402
|
+
): RequestSummary[] {
|
|
2403
|
+
const result: RequestSummary[] = [];
|
|
2404
|
+
const seen = new Set<string>();
|
|
2405
|
+
|
|
2406
|
+
// Files are passed oldest-first (yesterday, today); scan newest-first.
|
|
2407
|
+
for (const file of [...files].reverse()) {
|
|
2408
|
+
if (!existsSync(file)) continue;
|
|
2409
|
+
const remaining = limit - result.length;
|
|
2410
|
+
if (remaining <= 0) break;
|
|
2411
|
+
|
|
2412
|
+
for (const request of collectRecentRequestsFromLines(
|
|
2413
|
+
readAllLines(file),
|
|
2414
|
+
remaining,
|
|
2415
|
+
)) {
|
|
2416
|
+
if (seen.has(request.requestId)) continue;
|
|
2417
|
+
seen.add(request.requestId);
|
|
2418
|
+
result.push(request);
|
|
2419
|
+
if (result.length >= limit) return result;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
return result;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
function filterLinesByRequestId(
|
|
2427
|
+
files: string[],
|
|
2428
|
+
requestId: string,
|
|
2429
|
+
): string[] {
|
|
2430
|
+
const matches: string[] = [];
|
|
2431
|
+
for (const file of files) {
|
|
2432
|
+
if (!existsSync(file)) continue;
|
|
2433
|
+
for (const line of readAllLines(file)) {
|
|
2434
|
+
if (parseStructuredLogLine(line).requestId === requestId) {
|
|
2435
|
+
matches.push(line);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
return matches;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
function parseLinesOption(value: string): number {
|
|
2443
|
+
const parsed = Number.parseInt(value, 10);
|
|
2444
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 50;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
|
|
2305
2448
|
program
|
|
2306
2449
|
.command("logs")
|
|
2307
2450
|
.description("View daemon logs")
|
|
2308
2451
|
.option("-f, --follow", "Follow log output", false)
|
|
2309
2452
|
.option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false)
|
|
2310
2453
|
.option("-n, --lines <number>", "Number of lines to show", "50")
|
|
2311
|
-
.
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
process.exit(1);
|
|
2323
|
-
}
|
|
2454
|
+
.option("-r, --recent", "List recent request ids with their model", false)
|
|
2455
|
+
.option("-i, --request-id <id>", "Only show log lines for a specific request id")
|
|
2456
|
+
.action(
|
|
2457
|
+
async (options: {
|
|
2458
|
+
follow: boolean;
|
|
2459
|
+
lines: string;
|
|
2460
|
+
coco: boolean;
|
|
2461
|
+
recent: boolean;
|
|
2462
|
+
requestId?: string;
|
|
2463
|
+
}) => {
|
|
2464
|
+
await requireLocalDaemon();
|
|
2324
2465
|
|
|
2325
|
-
|
|
2466
|
+
if (options.recent && options.requestId) {
|
|
2467
|
+
console.error(
|
|
2468
|
+
"Cannot combine --recent (-r) with --request-id (-i).",
|
|
2469
|
+
);
|
|
2470
|
+
process.exit(1);
|
|
2471
|
+
}
|
|
2326
2472
|
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2473
|
+
const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR;
|
|
2474
|
+
const todayFile = getLogFileForDate(logsDir);
|
|
2475
|
+
const yesterday = new Date();
|
|
2476
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
2477
|
+
const yesterdayFile = getLogFileForDate(logsDir, yesterday);
|
|
2330
2478
|
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
console.log("No log file for today to follow.");
|
|
2479
|
+
if (!existsSync(todayFile) && !existsSync(yesterdayFile)) {
|
|
2480
|
+
console.log("No log files found. Daemon may not have started yet.");
|
|
2481
|
+
console.log(`Logs directory: ${logsDir}`);
|
|
2482
|
+
process.exit(1);
|
|
2336
2483
|
}
|
|
2337
|
-
return;
|
|
2338
|
-
}
|
|
2339
2484
|
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2485
|
+
const lines = parseLinesOption(options.lines);
|
|
2486
|
+
|
|
2487
|
+
const logFiles = [yesterdayFile, todayFile].filter(
|
|
2488
|
+
(file, index, files) =>
|
|
2489
|
+
existsSync(file) && files.indexOf(file) === index,
|
|
2490
|
+
);
|
|
2491
|
+
|
|
2492
|
+
if (options.recent) {
|
|
2493
|
+
const requests = collectRecentRequests(logFiles, lines);
|
|
2494
|
+
if (requests.length === 0) {
|
|
2495
|
+
console.log("No requests found in recent logs.");
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
console.log("Recent requests (newest first):");
|
|
2500
|
+
for (const request of requests) {
|
|
2501
|
+
const when = request.timestamp ? ` ${request.timestamp}` : "";
|
|
2502
|
+
console.log(` ${request.requestId} ${request.modelId}${when}`);
|
|
2503
|
+
}
|
|
2504
|
+
return;
|
|
2343
2505
|
}
|
|
2344
|
-
|
|
2345
|
-
if (
|
|
2346
|
-
|
|
2506
|
+
|
|
2507
|
+
if (options.requestId) {
|
|
2508
|
+
const matches = filterLinesByRequestId(logFiles, options.requestId);
|
|
2509
|
+
if (matches.length === 0) {
|
|
2510
|
+
console.log(
|
|
2511
|
+
`No log lines found for request '${options.requestId}'.`,
|
|
2512
|
+
);
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
for (const line of matches) {
|
|
2517
|
+
console.log(line);
|
|
2518
|
+
}
|
|
2519
|
+
return;
|
|
2347
2520
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2521
|
+
|
|
2522
|
+
if (options.follow) {
|
|
2523
|
+
if (existsSync(todayFile)) {
|
|
2524
|
+
await followLogFile(todayFile, lines);
|
|
2525
|
+
} else {
|
|
2526
|
+
console.log("No log file for today to follow.");
|
|
2527
|
+
}
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
for (const file of logFiles) {
|
|
2532
|
+
if (logFiles.length > 1) {
|
|
2533
|
+
console.log(`==> ${file} <==`);
|
|
2534
|
+
}
|
|
2535
|
+
const output = readLastLines(file, lines);
|
|
2536
|
+
if (output) {
|
|
2537
|
+
console.log(output);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
},
|
|
2541
|
+
);
|
|
2350
2542
|
|
|
2351
2543
|
export function cli(args: string[]) {
|
|
2352
2544
|
program.parse(args);
|