routstrd 0.4.4 → 0.4.6
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/README.md +14 -0
- package/dist/daemon/index.js +74735 -91633
- package/dist/index.js +203 -41
- package/package.json +9 -2
- package/src/cli.test.ts +138 -1
- package/src/cli.ts +289 -49
- package/src/daemon/http/index.ts +1862 -0
- package/src/daemon/index.ts +408 -0
- package/src/daemon/wallet/index.ts +360 -0
- package/src/index.ts +4 -0
- package/src/integrations/index.ts +159 -0
- package/src/tui/usage/index.ts +1 -0
- package/.claude/settings.local.json +0 -10
- package/.dockerignore +0 -12
- package/COCO-2.0.0-MIGRATION-PLAN.md +0 -1191
- package/Dockerfile +0 -11
- package/IMPLEMENTATION.md +0 -253
- package/SKILL.md +0 -324
- package/bun.lock +0 -494
- package/docker-compose.yml +0 -15
- package/refund.js +0 -33
- package/refund_new.js +0 -20
- package/routstr-cost-logging.md +0 -71
- package/test_box.ts +0 -15
- package/test_chat.sh +0 -29
- package/test_curl.sh +0 -11
- package/test_split_box.ts +0 -17
- package/test_split_box2.ts +0 -23
- package/tests/daemon/config-store.perms.test.ts +0 -55
- package/tests/daemon/config-store.scenario.ts +0 -113
- package/tests/integrations/hermes.test.ts +0 -134
- package/tests/utils/daemon-client.test.ts +0 -61
- package/tests/wallet/short-keyset-token.test.ts +0 -106
- package/tsconfig.json +0 -20
- package/v1-messages-format-report.md +0 -223
package/dist/index.js
CHANGED
|
@@ -23017,7 +23017,47 @@ function parseChoice(input) {
|
|
|
23017
23017
|
}
|
|
23018
23018
|
return 1;
|
|
23019
23019
|
}
|
|
23020
|
-
async function
|
|
23020
|
+
async function installIntegrationByKey(config, key) {
|
|
23021
|
+
const integrationConfig = CLIENT_CONFIGS[key];
|
|
23022
|
+
if (!integrationConfig) {
|
|
23023
|
+
console.log(`Unknown integration: ${key}`);
|
|
23024
|
+
return;
|
|
23025
|
+
}
|
|
23026
|
+
const { client, created } = await addDaemonClient(integrationConfig.name);
|
|
23027
|
+
if (created) {
|
|
23028
|
+
console.log(`Created new API key for ${integrationConfig.name}`);
|
|
23029
|
+
} else {
|
|
23030
|
+
console.log(`Using existing API key for ${integrationConfig.name}`);
|
|
23031
|
+
}
|
|
23032
|
+
switch (key) {
|
|
23033
|
+
case "opencode":
|
|
23034
|
+
await installOpencodeIntegration(config, client.apiKey, integrationConfig);
|
|
23035
|
+
return;
|
|
23036
|
+
case "openclaw":
|
|
23037
|
+
await installOpenClawIntegration(config, client.apiKey, integrationConfig);
|
|
23038
|
+
return;
|
|
23039
|
+
case "pi-agent":
|
|
23040
|
+
await installPiIntegration(config, client.apiKey, integrationConfig);
|
|
23041
|
+
return;
|
|
23042
|
+
case "claude-code":
|
|
23043
|
+
await installClaudeCodeIntegration(config, client.apiKey, integrationConfig);
|
|
23044
|
+
return;
|
|
23045
|
+
case "hermes":
|
|
23046
|
+
await installHermesIntegration(config, client.apiKey, integrationConfig);
|
|
23047
|
+
return;
|
|
23048
|
+
default:
|
|
23049
|
+
console.log(`Unknown integration: ${key}`);
|
|
23050
|
+
}
|
|
23051
|
+
}
|
|
23052
|
+
async function setupIntegration(config, integrationKey) {
|
|
23053
|
+
if (integrationKey === "skip") {
|
|
23054
|
+
console.log("Skipping integration setup.");
|
|
23055
|
+
return;
|
|
23056
|
+
}
|
|
23057
|
+
if (integrationKey !== undefined) {
|
|
23058
|
+
await installIntegrationByKey(config, integrationKey);
|
|
23059
|
+
return;
|
|
23060
|
+
}
|
|
23021
23061
|
console.log(`
|
|
23022
23062
|
Choose an integration to set up:`);
|
|
23023
23063
|
console.log("1. OpenCode (default)");
|
|
@@ -23040,33 +23080,7 @@ Choose an integration to set up:`);
|
|
|
23040
23080
|
console.log("Skipping integration setup.");
|
|
23041
23081
|
return;
|
|
23042
23082
|
}
|
|
23043
|
-
|
|
23044
|
-
const { client, created } = await addDaemonClient(integrationConfig.name);
|
|
23045
|
-
if (created) {
|
|
23046
|
-
console.log(`Created new API key for ${integrationConfig.name}`);
|
|
23047
|
-
} else {
|
|
23048
|
-
console.log(`Using existing API key for ${integrationConfig.name}`);
|
|
23049
|
-
}
|
|
23050
|
-
if (key === "opencode") {
|
|
23051
|
-
await installOpencodeIntegration(config, client.apiKey, integrationConfig);
|
|
23052
|
-
return;
|
|
23053
|
-
}
|
|
23054
|
-
if (key === "openclaw") {
|
|
23055
|
-
await installOpenClawIntegration(config, client.apiKey, integrationConfig);
|
|
23056
|
-
return;
|
|
23057
|
-
}
|
|
23058
|
-
if (key === "pi-agent") {
|
|
23059
|
-
await installPiIntegration(config, client.apiKey, integrationConfig);
|
|
23060
|
-
return;
|
|
23061
|
-
}
|
|
23062
|
-
if (key === "claude-code") {
|
|
23063
|
-
await installClaudeCodeIntegration(config, client.apiKey, integrationConfig);
|
|
23064
|
-
return;
|
|
23065
|
-
}
|
|
23066
|
-
if (key === "hermes") {
|
|
23067
|
-
await installHermesIntegration(config, client.apiKey, integrationConfig);
|
|
23068
|
-
return;
|
|
23069
|
-
}
|
|
23083
|
+
await installIntegrationByKey(config, key);
|
|
23070
23084
|
}
|
|
23071
23085
|
|
|
23072
23086
|
// node_modules/@noble/hashes/utils.js
|
|
@@ -27170,15 +27184,22 @@ zoo`.split(`
|
|
|
27170
27184
|
// package.json
|
|
27171
27185
|
var package_default = {
|
|
27172
27186
|
name: "routstrd",
|
|
27173
|
-
version: "0.4.
|
|
27187
|
+
version: "0.4.6",
|
|
27174
27188
|
module: "src/index.ts",
|
|
27175
27189
|
type: "module",
|
|
27176
27190
|
private: false,
|
|
27177
27191
|
bin: {
|
|
27178
27192
|
routstrd: "./dist/index.js"
|
|
27179
27193
|
},
|
|
27194
|
+
files: [
|
|
27195
|
+
"dist",
|
|
27196
|
+
"src",
|
|
27197
|
+
"README.md",
|
|
27198
|
+
"SECURITY.md"
|
|
27199
|
+
],
|
|
27180
27200
|
scripts: {
|
|
27181
27201
|
start: "bun src/index.ts start",
|
|
27202
|
+
stop: "bun src/index.ts stop",
|
|
27182
27203
|
monitor: "bun src/index.ts monitor",
|
|
27183
27204
|
lint: "tsc --noEmit",
|
|
27184
27205
|
test: "bun test",
|
|
@@ -27196,7 +27217,7 @@ var package_default = {
|
|
|
27196
27217
|
"@cashu/cashu-ts": "^4.3.0",
|
|
27197
27218
|
"@cashu/coco-core": "^1.0.1",
|
|
27198
27219
|
"@cashu/coco-sqlite-bun": "^1.0.1",
|
|
27199
|
-
"@routstr/sdk": "^0.4.
|
|
27220
|
+
"@routstr/sdk": "^0.4.2",
|
|
27200
27221
|
"@scure/bip39": "^2.2.0",
|
|
27201
27222
|
"applesauce-core": "^5.1.0",
|
|
27202
27223
|
"applesauce-relay": "^5.1.0",
|
|
@@ -27320,7 +27341,7 @@ async function requireLocalDaemon() {
|
|
|
27320
27341
|
process.exit(1);
|
|
27321
27342
|
}
|
|
27322
27343
|
}
|
|
27323
|
-
async function initDaemon() {
|
|
27344
|
+
async function initDaemon(integrationKey) {
|
|
27324
27345
|
console.log("Initializing routstrd...");
|
|
27325
27346
|
if (!existsSync14(CONFIG_DIR)) {
|
|
27326
27347
|
console.log(`Created config directory: ${CONFIG_DIR}`);
|
|
@@ -27370,7 +27391,7 @@ A new Nostr identity has been generated for authentication.`);
|
|
|
27370
27391
|
}
|
|
27371
27392
|
initializeWallet();
|
|
27372
27393
|
await startDaemon({ port: String(config.port || 8008), host: config.host || undefined });
|
|
27373
|
-
await setupIntegration(config);
|
|
27394
|
+
await setupIntegration(config, integrationKey);
|
|
27374
27395
|
console.log(`
|
|
27375
27396
|
Initialization complete!`);
|
|
27376
27397
|
console.log(`
|
|
@@ -27569,10 +27590,32 @@ program.command("local").description("Switch back to local daemon mode (removes
|
|
|
27569
27590
|
Use 'routstrd onboard' for first time using local mode or 'routstrd start' to start the local daemon if you already are using it.`);
|
|
27570
27591
|
console.log(`You can view the config file at: ${CONFIG_FILE}`);
|
|
27571
27592
|
});
|
|
27572
|
-
program.command("onboard").description("Initialize routstrd (creates config directory and initializes wallet)").action(async () => {
|
|
27593
|
+
program.command("onboard").description("Initialize routstrd (creates config directory and initializes wallet)").option("--opencode", "Set up OpenCode integration (non-interactive)").option("--openclaw", "Set up OpenClaw integration (non-interactive)").option("--pi-agent", "Set up Pi Agent integration (non-interactive)").option("--claude-code", "Set up Claude Code integration (non-interactive)").option("--hermes", "Set up Hermes integration (non-interactive)").option("--skip-integration", "Skip integration setup").action(async (options) => {
|
|
27573
27594
|
await requireLocalDaemon();
|
|
27595
|
+
const integrationFlags = {
|
|
27596
|
+
opencode: options.opencode,
|
|
27597
|
+
openclaw: options.openclaw,
|
|
27598
|
+
"pi-agent": options.piAgent,
|
|
27599
|
+
"claude-code": options.claudeCode,
|
|
27600
|
+
hermes: options.hermes
|
|
27601
|
+
};
|
|
27602
|
+
const selectedIntegrations = Object.keys(integrationFlags).filter((key) => integrationFlags[key]);
|
|
27603
|
+
if (selectedIntegrations.length > 1) {
|
|
27604
|
+
console.error("Error: use only one integration flag, or use 'routstrd clients add' for multiple clients.");
|
|
27605
|
+
process.exit(1);
|
|
27606
|
+
}
|
|
27607
|
+
if (options.skipIntegration && selectedIntegrations.length > 0) {
|
|
27608
|
+
console.error("Error: --skip-integration cannot be combined with an integration flag.");
|
|
27609
|
+
process.exit(1);
|
|
27610
|
+
}
|
|
27611
|
+
let integrationKey;
|
|
27612
|
+
if (options.skipIntegration) {
|
|
27613
|
+
integrationKey = "skip";
|
|
27614
|
+
} else if (selectedIntegrations.length === 1) {
|
|
27615
|
+
integrationKey = selectedIntegrations[0];
|
|
27616
|
+
}
|
|
27574
27617
|
try {
|
|
27575
|
-
await initDaemon();
|
|
27618
|
+
await initDaemon(integrationKey);
|
|
27576
27619
|
} catch (error) {
|
|
27577
27620
|
if (error instanceof WalletMigrationConflictError) {
|
|
27578
27621
|
console.error(error.message);
|
|
@@ -28588,14 +28631,17 @@ function getLogFileForDate2(logsDir, date = new Date) {
|
|
|
28588
28631
|
const day = String(date.getDate()).padStart(2, "0");
|
|
28589
28632
|
return `${logsDir}/${year}-${month}-${day}.log`;
|
|
28590
28633
|
}
|
|
28591
|
-
function
|
|
28634
|
+
function readAllLines(file) {
|
|
28592
28635
|
const content = readFileSync5(file, "utf8");
|
|
28593
28636
|
const allLines = content.replace(/\r\n/g, `
|
|
28594
28637
|
`).split(`
|
|
28595
28638
|
`);
|
|
28596
28639
|
if (allLines.at(-1) === "")
|
|
28597
28640
|
allLines.pop();
|
|
28598
|
-
return allLines
|
|
28641
|
+
return allLines;
|
|
28642
|
+
}
|
|
28643
|
+
function readLastLines(file, lines) {
|
|
28644
|
+
return readAllLines(file).slice(-lines).join(`
|
|
28599
28645
|
`);
|
|
28600
28646
|
}
|
|
28601
28647
|
async function followLogFile(file, lines) {
|
|
@@ -28621,8 +28667,102 @@ async function followLogFile(file, lines) {
|
|
|
28621
28667
|
position = size;
|
|
28622
28668
|
}
|
|
28623
28669
|
}
|
|
28624
|
-
|
|
28670
|
+
function parseStructuredLogLine(line) {
|
|
28671
|
+
const out = { isRouting: false };
|
|
28672
|
+
const head = line.match(/^\[([^\]]+)\]\s+\[([^\]]+)\]/);
|
|
28673
|
+
if (head) {
|
|
28674
|
+
out.timestamp = head[1];
|
|
28675
|
+
out.level = head[2];
|
|
28676
|
+
}
|
|
28677
|
+
const req = line.match(/\[req:([^\]\:]+)/);
|
|
28678
|
+
if (req) {
|
|
28679
|
+
out.requestId = req[1].trim();
|
|
28680
|
+
}
|
|
28681
|
+
const model = line.match(/\[model:([^\]\:]+)\]/);
|
|
28682
|
+
if (model) {
|
|
28683
|
+
out.modelId = model[1].trim();
|
|
28684
|
+
} else {
|
|
28685
|
+
const jsonModel = line.match(/"modelId"\s*:\s*"([^"]+)"/);
|
|
28686
|
+
if (jsonModel) {
|
|
28687
|
+
out.modelId = jsonModel[1];
|
|
28688
|
+
}
|
|
28689
|
+
}
|
|
28690
|
+
out.isRouting = !!out.requestId && line.includes("Routing request with path");
|
|
28691
|
+
return out;
|
|
28692
|
+
}
|
|
28693
|
+
function collectRecentRequestsFromLines(lines, limit) {
|
|
28694
|
+
const result = [];
|
|
28695
|
+
const seen = new Set;
|
|
28696
|
+
for (let i2 = lines.length - 1;i2 >= 0; i2--) {
|
|
28697
|
+
const parsed = parseStructuredLogLine(lines[i2]);
|
|
28698
|
+
if (!parsed.isRouting || !parsed.requestId)
|
|
28699
|
+
continue;
|
|
28700
|
+
if (seen.has(parsed.requestId))
|
|
28701
|
+
continue;
|
|
28702
|
+
seen.add(parsed.requestId);
|
|
28703
|
+
result.push({
|
|
28704
|
+
requestId: parsed.requestId,
|
|
28705
|
+
modelId: parsed.modelId ?? "unknown",
|
|
28706
|
+
timestamp: parsed.timestamp
|
|
28707
|
+
});
|
|
28708
|
+
if (result.length >= limit)
|
|
28709
|
+
break;
|
|
28710
|
+
}
|
|
28711
|
+
const byRequestId = new Map(result.map((r) => [r.requestId, r]));
|
|
28712
|
+
for (const line of lines) {
|
|
28713
|
+
const parsed = parseStructuredLogLine(line);
|
|
28714
|
+
if (!parsed.requestId || !parsed.modelId)
|
|
28715
|
+
continue;
|
|
28716
|
+
const record = byRequestId.get(parsed.requestId);
|
|
28717
|
+
if (record && record.modelId === "unknown") {
|
|
28718
|
+
record.modelId = parsed.modelId;
|
|
28719
|
+
}
|
|
28720
|
+
}
|
|
28721
|
+
return result;
|
|
28722
|
+
}
|
|
28723
|
+
function collectRecentRequests(files, limit) {
|
|
28724
|
+
const result = [];
|
|
28725
|
+
const seen = new Set;
|
|
28726
|
+
for (const file of [...files].reverse()) {
|
|
28727
|
+
if (!existsSync14(file))
|
|
28728
|
+
continue;
|
|
28729
|
+
const remaining = limit - result.length;
|
|
28730
|
+
if (remaining <= 0)
|
|
28731
|
+
break;
|
|
28732
|
+
for (const request of collectRecentRequestsFromLines(readAllLines(file), remaining)) {
|
|
28733
|
+
if (seen.has(request.requestId))
|
|
28734
|
+
continue;
|
|
28735
|
+
seen.add(request.requestId);
|
|
28736
|
+
result.push(request);
|
|
28737
|
+
if (result.length >= limit)
|
|
28738
|
+
return result;
|
|
28739
|
+
}
|
|
28740
|
+
}
|
|
28741
|
+
return result;
|
|
28742
|
+
}
|
|
28743
|
+
function filterLinesByRequestId(files, requestId) {
|
|
28744
|
+
const matches = [];
|
|
28745
|
+
for (const file of files) {
|
|
28746
|
+
if (!existsSync14(file))
|
|
28747
|
+
continue;
|
|
28748
|
+
for (const line of readAllLines(file)) {
|
|
28749
|
+
if (parseStructuredLogLine(line).requestId === requestId) {
|
|
28750
|
+
matches.push(line);
|
|
28751
|
+
}
|
|
28752
|
+
}
|
|
28753
|
+
}
|
|
28754
|
+
return matches;
|
|
28755
|
+
}
|
|
28756
|
+
function parseLinesOption(value) {
|
|
28757
|
+
const parsed = Number.parseInt(value, 10);
|
|
28758
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 50;
|
|
28759
|
+
}
|
|
28760
|
+
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
28761
|
await requireLocalDaemon();
|
|
28762
|
+
if (options.recent && options.requestId) {
|
|
28763
|
+
console.error("Cannot combine --recent (-r) with --request-id (-i).");
|
|
28764
|
+
process.exit(1);
|
|
28765
|
+
}
|
|
28626
28766
|
const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR2;
|
|
28627
28767
|
const todayFile = getLogFileForDate2(logsDir);
|
|
28628
28768
|
const yesterday = new Date;
|
|
@@ -28633,10 +28773,32 @@ program.command("logs").description("View daemon logs").option("-f, --follow", "
|
|
|
28633
28773
|
console.log(`Logs directory: ${logsDir}`);
|
|
28634
28774
|
process.exit(1);
|
|
28635
28775
|
}
|
|
28636
|
-
const lines =
|
|
28637
|
-
const logFiles = [yesterdayFile, todayFile].filter((file, index, files) =>
|
|
28638
|
-
|
|
28639
|
-
|
|
28776
|
+
const lines = parseLinesOption(options.lines);
|
|
28777
|
+
const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => existsSync14(file) && files.indexOf(file) === index);
|
|
28778
|
+
if (options.recent) {
|
|
28779
|
+
const requests = collectRecentRequests(logFiles, lines);
|
|
28780
|
+
if (requests.length === 0) {
|
|
28781
|
+
console.log("No requests found in recent logs.");
|
|
28782
|
+
return;
|
|
28783
|
+
}
|
|
28784
|
+
console.log("Recent requests (newest first):");
|
|
28785
|
+
for (const request of requests) {
|
|
28786
|
+
const when = request.timestamp ? ` ${request.timestamp}` : "";
|
|
28787
|
+
console.log(` ${request.requestId} ${request.modelId}${when}`);
|
|
28788
|
+
}
|
|
28789
|
+
return;
|
|
28790
|
+
}
|
|
28791
|
+
if (options.requestId) {
|
|
28792
|
+
const matches = filterLinesByRequestId(logFiles, options.requestId);
|
|
28793
|
+
if (matches.length === 0) {
|
|
28794
|
+
console.log(`No log lines found for request '${options.requestId}'.`);
|
|
28795
|
+
return;
|
|
28796
|
+
}
|
|
28797
|
+
for (const line of matches) {
|
|
28798
|
+
console.log(line);
|
|
28799
|
+
}
|
|
28800
|
+
return;
|
|
28801
|
+
}
|
|
28640
28802
|
if (options.follow) {
|
|
28641
28803
|
if (existsSync14(todayFile)) {
|
|
28642
28804
|
await followLogFile(todayFile, lines);
|
package/package.json
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"bin": {
|
|
8
8
|
"routstrd": "./dist/index.js"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"SECURITY.md"
|
|
15
|
+
],
|
|
10
16
|
"scripts": {
|
|
11
17
|
"start": "bun src/index.ts start",
|
|
18
|
+
"stop": "bun src/index.ts stop",
|
|
12
19
|
"monitor": "bun src/index.ts monitor",
|
|
13
20
|
"lint": "tsc --noEmit",
|
|
14
21
|
"test": "bun test",
|
|
@@ -26,7 +33,7 @@
|
|
|
26
33
|
"@cashu/cashu-ts": "^4.3.0",
|
|
27
34
|
"@cashu/coco-core": "^1.0.1",
|
|
28
35
|
"@cashu/coco-sqlite-bun": "^1.0.1",
|
|
29
|
-
"@routstr/sdk": "^0.4.
|
|
36
|
+
"@routstr/sdk": "^0.4.2",
|
|
30
37
|
"@scure/bip39": "^2.2.0",
|
|
31
38
|
"applesauce-core": "^5.1.0",
|
|
32
39
|
"applesauce-relay": "^5.1.0",
|
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");
|