onepass-proxy 0.3.0
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/LICENSE +21 -0
- package/README.md +408 -0
- package/dist/claudep.js +207 -0
- package/dist/evict.js +416 -0
- package/dist/launch.js +176 -0
- package/dist/log.js +53 -0
- package/dist/main.js +62 -0
- package/dist/recall.js +190 -0
- package/dist/report.js +137 -0
- package/dist/server.js +340 -0
- package/dist/session.js +100 -0
- package/dist/speed.js +77 -0
- package/dist/transcript.js +79 -0
- package/package.json +48 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { createProxyServer } from "./server.js";
|
|
4
|
+
import { newProxyLogPath } from "./log.js";
|
|
5
|
+
if (process.argv.includes("--version")) {
|
|
6
|
+
const packageJson = createRequire(import.meta.url)("../package.json");
|
|
7
|
+
console.log(packageJson.version);
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
function envInt(name, fallback) {
|
|
11
|
+
const raw = process.env[name];
|
|
12
|
+
if (raw === undefined || raw === "")
|
|
13
|
+
return fallback;
|
|
14
|
+
const value = Number(raw);
|
|
15
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
16
|
+
console.error(`[onepass] ${name} must be a non-negative integer, got: ${raw}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
// 0 asks the operating system for a free port. The eval starts a proxy child per case and per
|
|
22
|
+
// tail, several at once, so it uses 0 and reads the port the child reports below.
|
|
23
|
+
const port = envInt("ONEPASS_PORT", 3777);
|
|
24
|
+
// Loopback only. Every request through here carries the user's Claude Code credentials upstream,
|
|
25
|
+
// so a proxy bound to every interface is an open relay for anyone on the same café wifi.
|
|
26
|
+
const host = process.env.ONEPASS_HOST ?? "127.0.0.1";
|
|
27
|
+
const config = {
|
|
28
|
+
upstreamUrl: process.env.ONEPASS_UPSTREAM ?? "https://api.anthropic.com",
|
|
29
|
+
evictAfterAssistantTurns: envInt("ONEPASS_EVICT_AFTER_TURNS", 8),
|
|
30
|
+
protectLastAssistantTurns: envInt("ONEPASS_PROTECT_LAST_TURNS", 4),
|
|
31
|
+
// 80k, not the 110k this shipped with: with a batch minimum in front of it, a lower threshold
|
|
32
|
+
// buys a flatter curve for a handful of larger trips rather than a swarm of small ones
|
|
33
|
+
// (docs/findings.md §21). Below it the proxy is inert, so T is what decides when it starts.
|
|
34
|
+
tripThresholdTokens: envInt("ONEPASS_TRIP_TOKENS", 80_000),
|
|
35
|
+
batchMinTokens: envInt("ONEPASS_BATCH_MIN_TOKENS", 20_000),
|
|
36
|
+
minSavedChars: envInt("ONEPASS_MIN_SAVED_CHARS", 50),
|
|
37
|
+
logFilePath: newProxyLogPath(),
|
|
38
|
+
...(process.env.ONEPASS_DUMP_DIR !== undefined && process.env.ONEPASS_DUMP_DIR !== ""
|
|
39
|
+
? { dumpDir: process.env.ONEPASS_DUMP_DIR }
|
|
40
|
+
: {}),
|
|
41
|
+
};
|
|
42
|
+
const server = createProxyServer(config);
|
|
43
|
+
server.on("error", (err) => {
|
|
44
|
+
if (err.code === "EADDRINUSE") {
|
|
45
|
+
console.error(`[onepass] port ${port} is already in use — is another onepass-proxy running?`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
49
|
+
});
|
|
50
|
+
server.listen(port, host, () => {
|
|
51
|
+
// The requested port may be 0; what a caller has to connect to is the one that got bound.
|
|
52
|
+
const boundPort = server.address().port;
|
|
53
|
+
console.log(`[onepass] eviction proxy listening on http://${host}:${boundPort}`);
|
|
54
|
+
console.log(`[onepass] upstream: ${config.upstreamUrl}`);
|
|
55
|
+
console.log(`[onepass] evict after N=${config.evictAfterAssistantTurns} assistant turns, ` +
|
|
56
|
+
`protect last K=${config.protectLastAssistantTurns}, trip over T=${config.tripThresholdTokens} real tokens (live-calibrated), ` +
|
|
57
|
+
`min chars saved per stub ${config.minSavedChars}, batch min ${config.batchMinTokens} tokens`);
|
|
58
|
+
console.log(`[onepass] log: ${config.logFilePath}`);
|
|
59
|
+
// The flag keeps native-1M models at 1M: Claude Code caps them at 200k behind a non-api.anthropic.com host.
|
|
60
|
+
console.log(`[onepass] point Claude Code at it: ` +
|
|
61
|
+
`ANTHROPIC_BASE_URL=http://${host}:${boundPort} _CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL=1 claude`);
|
|
62
|
+
});
|
package/dist/recall.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `onepass-recall` — the MCP server that reads this session's original history back off disk.
|
|
3
|
+
//
|
|
4
|
+
// Half of Onepass by itself: the proxy may only evict what can be recovered verbatim, and this
|
|
5
|
+
// is what recovers it. Its `recall_search` description is also where the legend for the proxy's
|
|
6
|
+
// stubs lives, so a stub does not have to repeat the recovery hint in every block.
|
|
7
|
+
import { readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { transcriptForSession } from "./transcript.js";
|
|
15
|
+
const CALL_LOG = join(homedir(), ".onepass", "recall-calls.log");
|
|
16
|
+
const MAX_RESULT_CHARS = 8000;
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return typeof value === "object" && value !== null;
|
|
19
|
+
}
|
|
20
|
+
function blockToText(block) {
|
|
21
|
+
const type = block.type;
|
|
22
|
+
if (type === "text" && typeof block.text === "string")
|
|
23
|
+
return block.text;
|
|
24
|
+
if (type === "thinking" && typeof block.thinking === "string")
|
|
25
|
+
return block.thinking;
|
|
26
|
+
if (type === "tool_use")
|
|
27
|
+
return JSON.stringify(block.input ?? {});
|
|
28
|
+
if (type === "tool_result")
|
|
29
|
+
return typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
function parseTranscript(path) {
|
|
33
|
+
const entries = [];
|
|
34
|
+
// tool_use carries the name and file path; the matching tool_result only carries an id.
|
|
35
|
+
const toolById = new Map();
|
|
36
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
37
|
+
if (!line.trim())
|
|
38
|
+
continue;
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(line);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(parsed))
|
|
47
|
+
continue;
|
|
48
|
+
const message = parsed.message;
|
|
49
|
+
if (!isRecord(message))
|
|
50
|
+
continue;
|
|
51
|
+
const content = message.content;
|
|
52
|
+
if (!Array.isArray(content))
|
|
53
|
+
continue;
|
|
54
|
+
const kind = typeof parsed.type === "string" ? parsed.type : "unknown";
|
|
55
|
+
const timestamp = typeof parsed.timestamp === "string" ? parsed.timestamp : "";
|
|
56
|
+
for (const block of content) {
|
|
57
|
+
if (!isRecord(block))
|
|
58
|
+
continue;
|
|
59
|
+
const body = blockToText(block);
|
|
60
|
+
if (!body.trim())
|
|
61
|
+
continue;
|
|
62
|
+
let toolName;
|
|
63
|
+
let filePath;
|
|
64
|
+
if (block.type === "tool_use") {
|
|
65
|
+
toolName = typeof block.name === "string" ? block.name : undefined;
|
|
66
|
+
const input = block.input;
|
|
67
|
+
if (isRecord(input) && typeof input.file_path === "string")
|
|
68
|
+
filePath = input.file_path;
|
|
69
|
+
if (typeof block.id === "string")
|
|
70
|
+
toolById.set(block.id, { name: toolName ?? "?", filePath });
|
|
71
|
+
}
|
|
72
|
+
else if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
73
|
+
const origin = toolById.get(block.tool_use_id);
|
|
74
|
+
toolName = origin?.name;
|
|
75
|
+
filePath = origin?.filePath;
|
|
76
|
+
}
|
|
77
|
+
entries.push({
|
|
78
|
+
ref: entries.length,
|
|
79
|
+
kind: block.type === "tool_result" ? "tool_result" : `${kind}:${String(block.type)}`,
|
|
80
|
+
timestamp,
|
|
81
|
+
toolName,
|
|
82
|
+
filePath,
|
|
83
|
+
body,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return entries;
|
|
88
|
+
}
|
|
89
|
+
function truncate(text) {
|
|
90
|
+
if (text.length <= MAX_RESULT_CHARS)
|
|
91
|
+
return text;
|
|
92
|
+
return `${text.slice(0, MAX_RESULT_CHARS)}\n\n…[truncated ${text.length - MAX_RESULT_CHARS} chars — narrow your query for the rest]`;
|
|
93
|
+
}
|
|
94
|
+
function logCall(tool, args, outcome) {
|
|
95
|
+
const line = JSON.stringify({ at: new Date().toISOString(), tool, args, outcome });
|
|
96
|
+
try {
|
|
97
|
+
mkdirSync(dirname(CALL_LOG), { recursive: true });
|
|
98
|
+
appendFileSync(CALL_LOG, `${line}\n`);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// the log is spike instrumentation; never fail a recall because it could not be written
|
|
102
|
+
}
|
|
103
|
+
process.stderr.write(`[onepass-recall] ${line}\n`);
|
|
104
|
+
}
|
|
105
|
+
function loadEntries() {
|
|
106
|
+
const chosen = transcriptForSession(process.env, process.cwd());
|
|
107
|
+
if (chosen.path === null)
|
|
108
|
+
return { entries: [], error: chosen.reason };
|
|
109
|
+
return { entries: parseTranscript(chosen.path) };
|
|
110
|
+
}
|
|
111
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
112
|
+
const server = new McpServer({ name: "onepass-recall", version });
|
|
113
|
+
server.registerTool("recall_search", {
|
|
114
|
+
title: "Search the original conversation",
|
|
115
|
+
description: "Search the ORIGINAL, unmodified session history on disk — including turns that were removed from your context by compaction or tool-result clearing. " +
|
|
116
|
+
"Use this whenever you are about to state something about earlier work that you cannot actually see anymore: a file's contents, a command's output, a decision, or something that was tried and failed. " +
|
|
117
|
+
"The query is split on whitespace and each term matched separately — an entry matching only some terms is still returned, ranked below entries matching more. " +
|
|
118
|
+
"So throw several words at it rather than guessing one exact phrase. " +
|
|
119
|
+
"Returns matching entries with a `ref` — pass that ref to recall_get for the full content. " +
|
|
120
|
+
"Blocks marked `[onepass: evicted N chars]` were removed from your context by the Onepass proxy; the original is on disk and this tool finds it. " +
|
|
121
|
+
"A tool call whose `input` is an empty object was evicted the same way — its arguments are gone. " +
|
|
122
|
+
"To find one, search the path in the `call evicted, <path>` note on its result's stub, or the file names and error text around it. " +
|
|
123
|
+
"For an attached file, search the path from the `Called the Read tool` line beside it. " +
|
|
124
|
+
"For a task notification, search the task id. " +
|
|
125
|
+
"When you need the current state rather than what it was, read the file or re-run the command instead.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
query: z.string().describe("Words to look for, e.g. a filename, error message, or function name. Multiple words are matched independently, not as a phrase."),
|
|
128
|
+
limit: z.number().int().min(1).max(50).default(10).describe("Maximum matches to return"),
|
|
129
|
+
},
|
|
130
|
+
}, async ({ query, limit }) => {
|
|
131
|
+
const { entries, error } = loadEntries();
|
|
132
|
+
if (error) {
|
|
133
|
+
logCall("recall_search", { query, limit }, "error");
|
|
134
|
+
return { content: [{ type: "text", text: error }] };
|
|
135
|
+
}
|
|
136
|
+
const terms = [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
|
|
137
|
+
if (!terms.length) {
|
|
138
|
+
logCall("recall_search", { query, limit }, "empty query");
|
|
139
|
+
return { content: [{ type: "text", text: "Empty query — pass at least one word." }] };
|
|
140
|
+
}
|
|
141
|
+
const scored = entries
|
|
142
|
+
.map((entry) => {
|
|
143
|
+
const haystack = `${entry.body}\n${entry.filePath ?? ""}`.toLowerCase();
|
|
144
|
+
return { entry, hits: terms.filter((term) => haystack.includes(term)) };
|
|
145
|
+
})
|
|
146
|
+
.filter(({ hits }) => hits.length > 0)
|
|
147
|
+
.sort((a, b) => b.hits.length - a.hits.length || b.entry.ref - a.entry.ref)
|
|
148
|
+
.slice(0, limit);
|
|
149
|
+
const matches = scored.map(({ entry, hits }) => {
|
|
150
|
+
// Anchor the snippet on the longest matched term: short terms like "ok" match everywhere
|
|
151
|
+
// and would centre the snippet on noise.
|
|
152
|
+
const anchor = hits.reduce((longest, term) => (term.length > longest.length ? term : longest));
|
|
153
|
+
const at = entry.body.toLowerCase().indexOf(anchor);
|
|
154
|
+
const from = Math.max(0, at - 100);
|
|
155
|
+
const snippet = entry.body.slice(from, from + 300).replace(/\s+/g, " ");
|
|
156
|
+
const label = [entry.kind, entry.toolName, entry.filePath].filter(Boolean).join(" ");
|
|
157
|
+
return `ref=${entry.ref} ${label} ${entry.timestamp}\n matched ${hits.length}/${terms.length}: ${hits.join(", ")}\n …${snippet}…`;
|
|
158
|
+
});
|
|
159
|
+
logCall("recall_search", { query, limit }, `${matches.length} matches of ${entries.length} entries`);
|
|
160
|
+
return {
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: matches.length
|
|
165
|
+
? `${matches.length} match(es) in the original history, best first:\n\n${matches.join("\n\n")}`
|
|
166
|
+
: `No entry contains any of: ${terms.join(", ")} (searched ${entries.length} original entries).`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
server.registerTool("recall_get", {
|
|
172
|
+
title: "Fetch one original entry",
|
|
173
|
+
description: "Fetch the full, original content of one entry by its `ref` (from recall_search). This is the unmodified text as it was at the time — not a summary.",
|
|
174
|
+
inputSchema: { ref: z.number().int().min(0).describe("The ref returned by recall_search") },
|
|
175
|
+
}, async ({ ref }) => {
|
|
176
|
+
const { entries, error } = loadEntries();
|
|
177
|
+
if (error) {
|
|
178
|
+
logCall("recall_get", { ref }, "error");
|
|
179
|
+
return { content: [{ type: "text", text: error }] };
|
|
180
|
+
}
|
|
181
|
+
const entry = entries[ref];
|
|
182
|
+
if (!entry) {
|
|
183
|
+
logCall("recall_get", { ref }, "not found");
|
|
184
|
+
return { content: [{ type: "text", text: `No entry at ref=${ref}. Valid range 0..${entries.length - 1}.` }] };
|
|
185
|
+
}
|
|
186
|
+
logCall("recall_get", { ref }, `${entry.kind} ${entry.body.length} chars`);
|
|
187
|
+
const label = [entry.kind, entry.toolName, entry.filePath].filter(Boolean).join(" ");
|
|
188
|
+
return { content: [{ type: "text", text: `ref=${ref} ${label} ${entry.timestamp}\n\n${truncate(entry.body)}` }] };
|
|
189
|
+
});
|
|
190
|
+
await server.connect(new StdioServerTransport());
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Reads a Claude Code session transcript (read-only) plus the proxy's JSONL log and prints:
|
|
3
|
+
// compaction count, tokens evicted, tokens recalled, and per-request size over time.
|
|
4
|
+
//
|
|
5
|
+
// npm run report -- <session-jsonl-path> [proxy-log-path]
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { basename } from "node:path";
|
|
8
|
+
import { formatThousands } from "./evict.js";
|
|
9
|
+
import { latestProxyLogPath, proxyLogDir } from "./log.js";
|
|
10
|
+
import { parseProxyLog, scanTranscript } from "./session.js";
|
|
11
|
+
import { describeRebuild, formatDuration, GAUGE_MIN_ESTIMATED_TOKENS } from "./speed.js";
|
|
12
|
+
const REBUILD_KINDS = ["first", "after-trip", "after-idle", "unexpected"];
|
|
13
|
+
const BAR_WIDTH = 24;
|
|
14
|
+
function timeOfDay(isoTimestamp) {
|
|
15
|
+
const timePart = isoTimestamp.split("T")[1];
|
|
16
|
+
return timePart === undefined ? isoTimestamp : timePart.slice(0, 8);
|
|
17
|
+
}
|
|
18
|
+
function collectDefined(requests, pick) {
|
|
19
|
+
return requests.map(pick).filter((value) => value !== undefined);
|
|
20
|
+
}
|
|
21
|
+
function median(values) {
|
|
22
|
+
if (values.length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
25
|
+
const middle = Math.floor(sorted.length / 2);
|
|
26
|
+
return sorted.length % 2 === 1 ? sorted[middle] : Math.round((sorted[middle - 1] + sorted[middle]) / 2);
|
|
27
|
+
}
|
|
28
|
+
function formatMedian(values) {
|
|
29
|
+
const middle = median(values);
|
|
30
|
+
return middle === null ? "n/a" : formatDuration(middle);
|
|
31
|
+
}
|
|
32
|
+
function formatMedianAndMax(values) {
|
|
33
|
+
const middle = median(values);
|
|
34
|
+
return middle === null ? "n/a" : `median ${formatDuration(middle)}, max ${formatDuration(Math.max(...values))}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A rebuild means Anthropic re-read the conversation instead of serving it from cache — a few
|
|
38
|
+
* extra seconds on that turn. It is normal on the session's first request, on the request
|
|
39
|
+
* where the proxy tripped, and after the cache expires. Anything else is a bug.
|
|
40
|
+
*/
|
|
41
|
+
function printSpeedSummary(requests) {
|
|
42
|
+
// Same floor the proxy applies: small side calls have their own cache prefix, so mixing them
|
|
43
|
+
// into the cached-versus-rebuilt comparison compares two different conversations.
|
|
44
|
+
const conversation = requests.filter((request) => (request.estimatedTokensSent ?? 0) >= GAUGE_MIN_ESTIMATED_TOKENS);
|
|
45
|
+
const rebuilt = conversation.filter((request) => request.rebuild !== undefined);
|
|
46
|
+
const cached = conversation.filter((request) => request.rebuild === undefined);
|
|
47
|
+
const byKind = REBUILD_KINDS.map((kind) => `${conversation.filter((request) => request.rebuild === kind).length} ${describeRebuild(kind)}`);
|
|
48
|
+
console.log("Speed:");
|
|
49
|
+
console.log(` proxy's own time per request: ${formatMedianAndMax(collectDefined(requests, (r) => r.proxyMs))} ` +
|
|
50
|
+
`(over all ${requests.length} requests)`);
|
|
51
|
+
console.log(` conversation requests, over ${formatThousands(GAUGE_MIN_ESTIMATED_TOKENS)} tokens ` +
|
|
52
|
+
`(smaller side calls are not gauged): ${conversation.length}`);
|
|
53
|
+
console.log(` rebuilds: ${rebuilt.length} — ${byKind.join(", ")} (goal: 0 unexpected)`);
|
|
54
|
+
console.log(` wait for the first byte: ${formatMedian(collectDefined(cached, (r) => r.upstreamFirstByteMs))} cached, ` +
|
|
55
|
+
`${formatMedian(collectDefined(rebuilt, (r) => r.upstreamFirstByteMs))} rebuilt`);
|
|
56
|
+
console.log("");
|
|
57
|
+
}
|
|
58
|
+
function ratioLine(tokensEvicted, tokensRecalled) {
|
|
59
|
+
if (tokensEvicted === 0)
|
|
60
|
+
return "n/a — nothing evicted";
|
|
61
|
+
if (tokensRecalled === 0)
|
|
62
|
+
return `${formatThousands(tokensEvicted)} : 0 — nothing recalled yet`;
|
|
63
|
+
return `${Math.round(tokensEvicted / tokensRecalled)} : 1`;
|
|
64
|
+
}
|
|
65
|
+
async function main() {
|
|
66
|
+
const [sessionPath, proxyLogArg] = process.argv.slice(2);
|
|
67
|
+
if (sessionPath === undefined) {
|
|
68
|
+
console.error("usage: onepass-report <session-jsonl-path> [proxy-log-path]");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
if (!existsSync(sessionPath)) {
|
|
72
|
+
console.error(`no transcript at ${sessionPath}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const proxyLogPath = proxyLogArg ?? latestProxyLogPath();
|
|
76
|
+
const transcript = await scanTranscript(sessionPath);
|
|
77
|
+
const recalledTokens = Math.round(transcript.recallChars / 4);
|
|
78
|
+
console.log(`Onepass report — ${basename(sessionPath)}`);
|
|
79
|
+
console.log("");
|
|
80
|
+
console.log(`Transcript: ${sessionPath}`);
|
|
81
|
+
console.log(` entries: ${transcript.entryCount}` +
|
|
82
|
+
(transcript.firstTimestamp !== null ? ` (${transcript.firstTimestamp} -> ${transcript.lastTimestamp})` : ""));
|
|
83
|
+
console.log(` compactions: ${transcript.compactionCount} (target: 0)`);
|
|
84
|
+
if (transcript.realUsageSamples > 0) {
|
|
85
|
+
console.log(` peak real context (API-reported usage): ${formatThousands(transcript.realUsagePeak)} tokens ` +
|
|
86
|
+
`over ${transcript.realUsageSamples} assistant turns — ` +
|
|
87
|
+
`${transcript.realUsageTurnsAbove150k} above 150,000 (goal: 0)`);
|
|
88
|
+
}
|
|
89
|
+
console.log(` recall results: ${transcript.recallResultCount} — ~${formatThousands(recalledTokens)} tokens recalled`);
|
|
90
|
+
console.log("");
|
|
91
|
+
if (proxyLogPath === null || !existsSync(proxyLogPath)) {
|
|
92
|
+
console.log(`Proxy log: none found at ${proxyLogPath ?? proxyLogDir} — start the proxy and run the session through it.`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const { requests, trips } = parseProxyLog(proxyLogPath);
|
|
96
|
+
const evictedIdCount = trips.reduce((sum, trip) => sum + trip.addedToolUseIds.length, 0);
|
|
97
|
+
const tripCharsRemoved = trips.reduce((sum, trip) => sum + trip.charsRemoved, 0);
|
|
98
|
+
const tokensEvictedOnce = Math.round(tripCharsRemoved / 4);
|
|
99
|
+
const cumulativeTokensKeptOut = requests.reduce((sum, request) => request.estimatedTokensBefore !== undefined && request.estimatedTokensSent !== undefined
|
|
100
|
+
? sum + (request.estimatedTokensBefore - request.estimatedTokensSent)
|
|
101
|
+
: sum, 0);
|
|
102
|
+
console.log(`Proxy log: ${proxyLogPath}`);
|
|
103
|
+
console.log(` /v1/messages requests: ${requests.length}`);
|
|
104
|
+
console.log(` eviction trips: ${trips.length} — ${evictedIdCount} segments evicted, ` +
|
|
105
|
+
`${formatThousands(tripCharsRemoved)} chars removed`);
|
|
106
|
+
console.log(` tokens evicted (one-time, chars/4): ${formatThousands(tokensEvictedOnce)}`);
|
|
107
|
+
console.log(` tokens kept out of requests (cumulative over turns): ${formatThousands(cumulativeTokensKeptOut)}`);
|
|
108
|
+
console.log("");
|
|
109
|
+
console.log(`Product metric — tokens evicted : tokens recalled = ${ratioLine(tokensEvictedOnce, recalledTokens)}`);
|
|
110
|
+
console.log("");
|
|
111
|
+
if (requests.length === 0)
|
|
112
|
+
return;
|
|
113
|
+
printSpeedSummary(requests);
|
|
114
|
+
console.log("Per /v1/messages request (chart = estimated tokens sent; flat is good, unproxied it climbs):");
|
|
115
|
+
console.log(` ${"#".padStart(4)} ${"time".padEnd(8)} ${"sent tok".padStart(9)} ${"proxy".padStart(7)} ` +
|
|
116
|
+
`${"1st byte".padStart(8)} ${"cached".padStart(9)} ${"new".padStart(7)} chart`);
|
|
117
|
+
const maxSent = Math.max(...requests.map((request) => request.estimatedTokensSent ?? 0), 1);
|
|
118
|
+
const column = (value, format, width) => (value === undefined ? "-" : format(value)).padStart(width);
|
|
119
|
+
requests.forEach((request, index) => {
|
|
120
|
+
const sent = request.estimatedTokensSent ?? 0;
|
|
121
|
+
const before = request.estimatedTokensBefore ?? sent;
|
|
122
|
+
const bar = "#".repeat(Math.max(sent > 0 ? 1 : 0, Math.round((sent / maxSent) * BAR_WIDTH)));
|
|
123
|
+
const marks = [];
|
|
124
|
+
if ((request.newlyEvictedCount ?? 0) > 0)
|
|
125
|
+
marks.push("trip");
|
|
126
|
+
if (request.rebuild !== undefined) {
|
|
127
|
+
marks.push(request.rebuild === "unexpected" ? "REBUILD (unexpected)" : `rebuild (${describeRebuild(request.rebuild)})`);
|
|
128
|
+
}
|
|
129
|
+
console.log(` ${String(index + 1).padStart(4)} ${timeOfDay(request.timestamp)} ${formatThousands(sent).padStart(9)} ` +
|
|
130
|
+
`${column(request.proxyMs, formatDuration, 7)} ${column(request.upstreamFirstByteMs, formatDuration, 8)} ` +
|
|
131
|
+
`${column(request.cacheReadInputTokens, formatThousands, 9)} ` +
|
|
132
|
+
`${column(request.cacheCreationInputTokens, formatThousands, 7)} ` +
|
|
133
|
+
`${bar}${before !== sent ? ` (raw ${formatThousands(before)})` : ""}` +
|
|
134
|
+
`${marks.length === 0 ? "" : ` <- ${marks.join(", ")}`}`);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
await main();
|