ai-cc-router 0.1.6 → 0.1.7
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/dist/proxy/server.js +43 -31
- package/dist/proxy/stats.js +1 -0
- package/dist/ui/Dashboard.js +21 -11
- package/package.json +1 -1
package/dist/proxy/server.js
CHANGED
|
@@ -9,10 +9,10 @@ import { logRoute, logError, logStartup } from "./logger.js";
|
|
|
9
9
|
import { stats } from "./stats.js";
|
|
10
10
|
import { PROXY_PORT, LITELLM_URL } from "../config/paths.js";
|
|
11
11
|
import chalk from "chalk";
|
|
12
|
-
// Mutates entry and updates aggregate counters with
|
|
13
|
-
//
|
|
12
|
+
// Mutates entry and updates aggregate counters with token usage from Anthropic's
|
|
13
|
+
// response. Called asynchronously after the log entry is already stored,
|
|
14
14
|
// so the dashboard picks up the values on the next poll.
|
|
15
|
-
function
|
|
15
|
+
function applyInputUsage(entry, usage) {
|
|
16
16
|
entry.cacheReadTokens = usage["cache_read_input_tokens"] ?? 0;
|
|
17
17
|
entry.cacheCreationTokens = usage["cache_creation_input_tokens"] ?? 0;
|
|
18
18
|
entry.inputTokens = usage["input_tokens"] ?? 0;
|
|
@@ -20,6 +20,10 @@ function applyCacheUsage(entry, usage) {
|
|
|
20
20
|
stats.totalCacheCreationTokens += entry.cacheCreationTokens;
|
|
21
21
|
stats.totalInputTokens += entry.inputTokens;
|
|
22
22
|
}
|
|
23
|
+
function applyOutputUsage(entry, usage) {
|
|
24
|
+
entry.outputTokens = usage["output_tokens"] ?? 0;
|
|
25
|
+
stats.totalOutputTokens += entry.outputTokens;
|
|
26
|
+
}
|
|
23
27
|
export async function startServer(opts = {}) {
|
|
24
28
|
const port = opts.port ?? PROXY_PORT;
|
|
25
29
|
// Direct-to-Anthropic (standalone) or via LiteLLM (full mode).
|
|
@@ -79,6 +83,7 @@ export async function startServer(opts = {}) {
|
|
|
79
83
|
totalCacheReadTokens: stats.totalCacheReadTokens,
|
|
80
84
|
totalCacheCreationTokens: stats.totalCacheCreationTokens,
|
|
81
85
|
totalInputTokens: stats.totalInputTokens,
|
|
86
|
+
totalOutputTokens: stats.totalOutputTokens,
|
|
82
87
|
accounts: pool.getStats(),
|
|
83
88
|
recentLogs: stats.getRecentLogs(50),
|
|
84
89
|
});
|
|
@@ -179,50 +184,57 @@ export async function startServer(opts = {}) {
|
|
|
179
184
|
}
|
|
180
185
|
const entry = pendingLog;
|
|
181
186
|
stats.addLog(entry);
|
|
182
|
-
// ── Capture
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// streaming
|
|
187
|
+
// ── Capture token usage from Anthropic response body ─────────────────
|
|
188
|
+
// SSE streams carry usage across two events:
|
|
189
|
+
// message_start → input_tokens, cache_read/creation_input_tokens
|
|
190
|
+
// message_delta → output_tokens
|
|
191
|
+
// Non-streaming JSON carries all fields in a single usage object.
|
|
192
|
+
// We use incremental line parsing (not buffering) so we can capture
|
|
193
|
+
// both events without holding the full stream in memory.
|
|
187
194
|
const contentType = String(proxyRes.headers["content-type"] ?? "");
|
|
188
195
|
const encoding = String(proxyRes.headers["content-encoding"] ?? "");
|
|
189
196
|
const isCompressed = /gzip|br|deflate/.test(encoding);
|
|
190
197
|
if (!isCompressed && (contentType.includes("text/event-stream") || contentType.includes("application/json"))) {
|
|
191
198
|
const isSSE = contentType.includes("text/event-stream");
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
if (isSSE) {
|
|
200
|
+
let lineBuf = "";
|
|
201
|
+
let gotInput = false;
|
|
202
|
+
let gotOutput = false;
|
|
203
|
+
proxyRes.on("data", (chunk) => {
|
|
204
|
+
if (gotInput && gotOutput)
|
|
205
|
+
return;
|
|
206
|
+
lineBuf += chunk.toString("utf8");
|
|
207
|
+
const lines = lineBuf.split("\n");
|
|
208
|
+
lineBuf = lines.pop() ?? ""; // keep incomplete last line
|
|
202
209
|
for (const line of lines) {
|
|
203
210
|
if (!line.startsWith("data: "))
|
|
204
211
|
continue;
|
|
205
212
|
try {
|
|
206
213
|
const evt = JSON.parse(line.slice(6));
|
|
207
|
-
if (evt.type === "message_start" && evt.message?.usage) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
214
|
+
if (!gotInput && evt.type === "message_start" && evt.message?.usage) {
|
|
215
|
+
applyInputUsage(entry, evt.message.usage);
|
|
216
|
+
gotInput = true;
|
|
217
|
+
}
|
|
218
|
+
if (!gotOutput && evt.type === "message_delta" && evt.usage) {
|
|
219
|
+
applyOutputUsage(entry, evt.usage);
|
|
220
|
+
gotOutput = true;
|
|
211
221
|
}
|
|
212
222
|
}
|
|
213
|
-
catch { /* partial JSON
|
|
223
|
+
catch { /* partial JSON across chunk boundary — next chunk will complete it */ }
|
|
214
224
|
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Non-streaming:
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Non-streaming JSON: buffer full body then parse once
|
|
229
|
+
let buf = "";
|
|
230
|
+
proxyRes.on("data", (chunk) => { buf += chunk.toString("utf8"); });
|
|
219
231
|
proxyRes.on("end", () => {
|
|
220
|
-
if (captured)
|
|
221
|
-
return;
|
|
222
232
|
try {
|
|
223
233
|
const body = JSON.parse(buf);
|
|
224
|
-
if (body.usage)
|
|
225
|
-
|
|
234
|
+
if (body.usage) {
|
|
235
|
+
applyInputUsage(entry, body.usage);
|
|
236
|
+
applyOutputUsage(entry, body.usage);
|
|
237
|
+
}
|
|
226
238
|
}
|
|
227
239
|
catch { /* ignore */ }
|
|
228
240
|
});
|
package/dist/proxy/stats.js
CHANGED
package/dist/ui/Dashboard.js
CHANGED
|
@@ -79,7 +79,7 @@ function LiveDashboard({ data, port, lastUpdate }) {
|
|
|
79
79
|
});
|
|
80
80
|
const selectedLog = logs[selectedIndex] ?? null;
|
|
81
81
|
const visibleLogs = logs.slice(0, LOG_VISIBLE);
|
|
82
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: " CC-Router " }), _jsx(Text, { color: "gray", children: "\u00B7 " }), _jsx(Text, { color: "green", children: data.mode }), _jsxs(Text, { color: "gray", children: [" \u2192 ", data.target, " \u00B7 "] }), _jsxs(Text, { children: ["up ", formatUptime(data.uptime)] }), _jsxs(Text, { color: "gray", children: [" \u00B7 updated ", updatedAgo, "s ago \u00B7 [\u2191\u2193] navigate \u00B7 [q] quit"] })] }), _jsx(Box, { marginTop: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [" ACCOUNTS ", _jsxs(Text, { color: healthyCount === data.accounts.length ? "green" : "yellow", children: [healthyCount, "/", data.accounts.length, " healthy"] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: data.accounts.map(a => (_jsx(AccountRow, { account: a }, a.id))) })] }), _jsx(Box, { marginTop: 1 }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: " TOTALS " }), _jsx(Text, { children: "requests " }), _jsx(Text, { color: "cyan", children: data.totalRequests }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "errors " }), _jsx(Text, { color: data.totalErrors > 0 ? "red" : "green", children: data.totalErrors }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "refreshes " }), _jsx(Text, { color: "yellow", children: data.totalRefreshes }), _jsx(CacheHealthBadge, { read: data.totalCacheReadTokens, created: data.totalCacheCreationTokens, input: data.totalInputTokens })] }), _jsx(Box, { marginTop: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: " RECENT ACTIVITY" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: visibleLogs.length === 0
|
|
82
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: " CC-Router " }), _jsx(Text, { color: "gray", children: "\u00B7 " }), _jsx(Text, { color: "green", children: data.mode }), _jsxs(Text, { color: "gray", children: [" \u2192 ", data.target, " \u00B7 "] }), _jsxs(Text, { children: ["up ", formatUptime(data.uptime)] }), _jsxs(Text, { color: "gray", children: [" \u00B7 updated ", updatedAgo, "s ago \u00B7 [\u2191\u2193] navigate \u00B7 [q] quit"] })] }), _jsx(Box, { marginTop: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [" ACCOUNTS ", _jsxs(Text, { color: healthyCount === data.accounts.length ? "green" : "yellow", children: [healthyCount, "/", data.accounts.length, " healthy"] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: data.accounts.map(a => (_jsx(AccountRow, { account: a }, a.id))) })] }), _jsx(Box, { marginTop: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " TOTALS " }), _jsx(Text, { children: "requests " }), _jsx(Text, { color: "cyan", children: data.totalRequests }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "errors " }), _jsx(Text, { color: data.totalErrors > 0 ? "red" : "green", children: data.totalErrors }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "refreshes " }), _jsx(Text, { color: "yellow", children: data.totalRefreshes }), _jsx(CacheHealthBadge, { read: data.totalCacheReadTokens, created: data.totalCacheCreationTokens, input: data.totalInputTokens })] }), _jsx(TokenSummary, { cacheRead: data.totalCacheReadTokens, cacheCreated: data.totalCacheCreationTokens, uncached: data.totalInputTokens, output: data.totalOutputTokens })] }), _jsx(Box, { marginTop: 1 }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: " RECENT ACTIVITY" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: visibleLogs.length === 0
|
|
83
83
|
? _jsx(Text, { color: "gray", children: " No activity yet" })
|
|
84
84
|
: visibleLogs.map((log, i) => (_jsx(LogRow, { log: log, selected: i === selectedIndex }, log.ts))) })] }), selectedLog && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1 }), _jsx(DetailPanel, { log: selectedLog })] }))] }));
|
|
85
85
|
}
|
|
@@ -109,16 +109,17 @@ function LogRow({ log, selected }) {
|
|
|
109
109
|
: "gray";
|
|
110
110
|
const bg = selected ? "white" : undefined;
|
|
111
111
|
const fg = (c) => selected ? "black" : c;
|
|
112
|
-
// Per-request
|
|
113
|
-
const
|
|
114
|
-
const
|
|
112
|
+
// Per-request token stats
|
|
113
|
+
const inputTok = (log.cacheReadTokens ?? 0) + (log.cacheCreationTokens ?? 0) + (log.inputTokens ?? 0);
|
|
114
|
+
const outputTok = log.outputTokens ?? 0;
|
|
115
|
+
const cacheHitPct = inputTok > 0 ? Math.round(((log.cacheReadTokens ?? 0) / inputTok) * 100) : null;
|
|
115
116
|
const cacheColor = cacheHitPct === null ? undefined
|
|
116
117
|
: cacheHitPct >= 70 ? "green"
|
|
117
118
|
: cacheHitPct >= 30 ? "yellow"
|
|
118
119
|
: "red";
|
|
119
120
|
return (_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: bg, color: fg(undefined), children: [selected ? "▶" : " ", " ", time, " "] }), _jsxs(Text, { backgroundColor: bg, color: fg(typeColor), children: [typeIcon, " "] }), _jsx(Text, { backgroundColor: bg, color: fg("cyan"), children: log.accountId.slice(0, 22).padEnd(22) }), log.method && log.path
|
|
120
121
|
? _jsxs(Text, { backgroundColor: bg, color: fg("white"), children: [" ", log.method, " ", log.path.padEnd(14)] })
|
|
121
|
-
: _jsxs(Text, { backgroundColor: bg, color: fg(typeColor), children: [" ", log.type.padEnd(9)] }), log.statusCode !== undefined && (_jsxs(Text, { backgroundColor: bg, color: fg(statusColor), children: [" ", log.statusCode] })), log.durationMs !== undefined && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", log.durationMs, "ms"] })), cacheHitPct !== null && (_jsxs(Text, { backgroundColor: bg, color: fg(cacheColor), children: [" \u2191", cacheHitPct, "%"] })), log.details && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", log.details] }))] }));
|
|
122
|
+
: _jsxs(Text, { backgroundColor: bg, color: fg(typeColor), children: [" ", log.type.padEnd(9)] }), log.statusCode !== undefined && (_jsxs(Text, { backgroundColor: bg, color: fg(statusColor), children: [" ", log.statusCode] })), log.durationMs !== undefined && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", log.durationMs, "ms"] })), cacheHitPct !== null && (_jsxs(Text, { backgroundColor: bg, color: fg(cacheColor), children: [" \u2191", cacheHitPct, "%"] })), (inputTok > 0 || outputTok > 0) && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", fmtTok(inputTok), "\u2191 ", fmtTok(outputTok), "\u2193"] })), log.details && (_jsxs(Text, { backgroundColor: bg, color: fg("gray"), children: [" ", log.details] }))] }));
|
|
122
123
|
}
|
|
123
124
|
// ─── Detail panel ─────────────────────────────────────────────────────────────
|
|
124
125
|
function DetailPanel({ log }) {
|
|
@@ -136,7 +137,7 @@ function DetailPanel({ log }) {
|
|
|
136
137
|
: log.statusCode >= 500 ? "red"
|
|
137
138
|
: log.statusCode >= 400 ? "yellow"
|
|
138
139
|
: "green";
|
|
139
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : "cyan", children: " DETAILS " }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Time", value: time }), _jsx(Field, { label: "Account", value: log.accountId })] }), _jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Method", value: log.method ?? "—" }), _jsx(Field, { label: "Path", value: log.path ?? "—" })] }), _jsxs(Box, { gap: 2, children: [_jsx(FieldColored, { label: "Status", value: statusLabel, color: statusColor }), _jsx(Field, { label: "Duration", value: log.durationMs !== undefined ? `${log.durationMs}ms` : "—" }), _jsx(Field, { label: "Type", value: log.type })] }), log.details && (_jsx(Box, { children: _jsx(Field, { label: "Details", value: log.details }) })), log.cacheReadTokens !== undefined && (_jsx(Box, { gap: 2, children: _jsx(CacheBreakdown, { read: log.cacheReadTokens, created: log.cacheCreationTokens ?? 0, input: log.inputTokens ?? 0 }) }))] })] }));
|
|
140
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : "cyan", children: " DETAILS " }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Time", value: time }), _jsx(Field, { label: "Account", value: log.accountId })] }), _jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Method", value: log.method ?? "—" }), _jsx(Field, { label: "Path", value: log.path ?? "—" })] }), _jsxs(Box, { gap: 2, children: [_jsx(FieldColored, { label: "Status", value: statusLabel, color: statusColor }), _jsx(Field, { label: "Duration", value: log.durationMs !== undefined ? `${log.durationMs}ms` : "—" }), _jsx(Field, { label: "Type", value: log.type })] }), log.details && (_jsx(Box, { children: _jsx(Field, { label: "Details", value: log.details }) })), log.cacheReadTokens !== undefined && (_jsx(Box, { gap: 2, children: _jsx(CacheBreakdown, { read: log.cacheReadTokens, created: log.cacheCreationTokens ?? 0, input: log.inputTokens ?? 0, output: log.outputTokens ?? 0 }) }))] })] }));
|
|
140
141
|
}
|
|
141
142
|
function Field({ label, value }) {
|
|
142
143
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: "white", children: value })] }));
|
|
@@ -155,11 +156,20 @@ function CacheHealthBadge({ read, created, input }) {
|
|
|
155
156
|
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { children: "cache " }), _jsxs(Text, { color: color, children: [hitPct, "% hit "] }), _jsxs(Text, { color: "gray", children: ["(", label, ")"] })] }));
|
|
156
157
|
}
|
|
157
158
|
// ─── Cache breakdown (per-request detail) ────────────────────────────────────
|
|
158
|
-
function CacheBreakdown({ read, created, input }) {
|
|
159
|
-
const
|
|
160
|
-
const hitPct =
|
|
161
|
-
const color =
|
|
162
|
-
return (_jsxs(_Fragment, { children: [_jsx(FieldColored, { label: "Cache hit", value:
|
|
159
|
+
function CacheBreakdown({ read, created, input, output }) {
|
|
160
|
+
const totalInput = read + created + input;
|
|
161
|
+
const hitPct = totalInput > 0 ? (read / totalInput) * 100 : 0;
|
|
162
|
+
const color = totalInput === 0 ? "gray" : hitPct >= 70 ? "green" : hitPct >= 30 ? "yellow" : "red";
|
|
163
|
+
return (_jsxs(_Fragment, { children: [_jsx(FieldColored, { label: "Cache hit", value: totalInput > 0 ? `${fmtTok(read)} tok (${hitPct.toFixed(1)}%)` : "—", color: color }), _jsx(Field, { label: "Cache created", value: fmtTok(created) + " tok" }), _jsx(Field, { label: "Uncached", value: fmtTok(input) + " tok" }), _jsx(Field, { label: "Total input", value: fmtTok(totalInput) + " tok" }), _jsx(Field, { label: "Output", value: fmtTok(output) + " tok" }), _jsx(Field, { label: "Total", value: fmtTok(totalInput + output) + " tok" })] }));
|
|
164
|
+
}
|
|
165
|
+
// ─── Token summary (aggregated totals) ──────────────────────────────────────
|
|
166
|
+
function TokenSummary({ cacheRead, cacheCreated, uncached, output }) {
|
|
167
|
+
const totalInput = cacheRead + cacheCreated + uncached;
|
|
168
|
+
const totalAll = totalInput + output;
|
|
169
|
+
if (totalAll === 0)
|
|
170
|
+
return null;
|
|
171
|
+
const hitPct = totalInput > 0 ? (cacheRead / totalInput) * 100 : 0;
|
|
172
|
+
return (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "input " }), _jsx(Text, { color: "white", children: fmtTok(totalInput) }), _jsx(Text, { color: "gray", children: " (cached " }), _jsx(Text, { color: "green", children: fmtTok(cacheRead) }), _jsx(Text, { color: "gray", children: " + new " }), _jsx(Text, { color: "yellow", children: fmtTok(cacheCreated) }), _jsx(Text, { color: "gray", children: " + uncached " }), _jsx(Text, { color: "white", children: fmtTok(uncached) }), _jsx(Text, { color: "gray", children: ") \u00B7 output " }), _jsx(Text, { color: "white", children: fmtTok(output) }), _jsx(Text, { color: "gray", children: " \u00B7 total " }), _jsx(Text, { color: "cyan", bold: true, children: fmtTok(totalAll) })] }));
|
|
163
173
|
}
|
|
164
174
|
function fmtTok(n) {
|
|
165
175
|
if (n >= 1_000_000)
|