ai-cc-router 0.1.5 → 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 +77 -1
- package/dist/proxy/stats.js +4 -0
- package/dist/ui/Dashboard.js +44 -3
- package/package.json +1 -1
package/dist/proxy/server.js
CHANGED
|
@@ -9,6 +9,21 @@ 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 token usage from Anthropic's
|
|
13
|
+
// response. Called asynchronously after the log entry is already stored,
|
|
14
|
+
// so the dashboard picks up the values on the next poll.
|
|
15
|
+
function applyInputUsage(entry, usage) {
|
|
16
|
+
entry.cacheReadTokens = usage["cache_read_input_tokens"] ?? 0;
|
|
17
|
+
entry.cacheCreationTokens = usage["cache_creation_input_tokens"] ?? 0;
|
|
18
|
+
entry.inputTokens = usage["input_tokens"] ?? 0;
|
|
19
|
+
stats.totalCacheReadTokens += entry.cacheReadTokens;
|
|
20
|
+
stats.totalCacheCreationTokens += entry.cacheCreationTokens;
|
|
21
|
+
stats.totalInputTokens += entry.inputTokens;
|
|
22
|
+
}
|
|
23
|
+
function applyOutputUsage(entry, usage) {
|
|
24
|
+
entry.outputTokens = usage["output_tokens"] ?? 0;
|
|
25
|
+
stats.totalOutputTokens += entry.outputTokens;
|
|
26
|
+
}
|
|
12
27
|
export async function startServer(opts = {}) {
|
|
13
28
|
const port = opts.port ?? PROXY_PORT;
|
|
14
29
|
// Direct-to-Anthropic (standalone) or via LiteLLM (full mode).
|
|
@@ -65,6 +80,10 @@ export async function startServer(opts = {}) {
|
|
|
65
80
|
totalRequests: stats.totalRequests,
|
|
66
81
|
totalErrors: stats.totalErrors,
|
|
67
82
|
totalRefreshes: stats.totalRefreshes,
|
|
83
|
+
totalCacheReadTokens: stats.totalCacheReadTokens,
|
|
84
|
+
totalCacheCreationTokens: stats.totalCacheCreationTokens,
|
|
85
|
+
totalInputTokens: stats.totalInputTokens,
|
|
86
|
+
totalOutputTokens: stats.totalOutputTokens,
|
|
68
87
|
accounts: pool.getStats(),
|
|
69
88
|
recentLogs: stats.getRecentLogs(50),
|
|
70
89
|
});
|
|
@@ -163,7 +182,64 @@ export async function startServer(opts = {}) {
|
|
|
163
182
|
account.busy = true;
|
|
164
183
|
setTimeout(() => { account.busy = false; }, 30_000);
|
|
165
184
|
}
|
|
166
|
-
|
|
185
|
+
const entry = pendingLog;
|
|
186
|
+
stats.addLog(entry);
|
|
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.
|
|
194
|
+
const contentType = String(proxyRes.headers["content-type"] ?? "");
|
|
195
|
+
const encoding = String(proxyRes.headers["content-encoding"] ?? "");
|
|
196
|
+
const isCompressed = /gzip|br|deflate/.test(encoding);
|
|
197
|
+
if (!isCompressed && (contentType.includes("text/event-stream") || contentType.includes("application/json"))) {
|
|
198
|
+
const isSSE = contentType.includes("text/event-stream");
|
|
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
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
if (!line.startsWith("data: "))
|
|
211
|
+
continue;
|
|
212
|
+
try {
|
|
213
|
+
const evt = JSON.parse(line.slice(6));
|
|
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;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch { /* partial JSON across chunk boundary — next chunk will complete it */ }
|
|
224
|
+
}
|
|
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"); });
|
|
231
|
+
proxyRes.on("end", () => {
|
|
232
|
+
try {
|
|
233
|
+
const body = JSON.parse(buf);
|
|
234
|
+
if (body.usage) {
|
|
235
|
+
applyInputUsage(entry, body.usage);
|
|
236
|
+
applyOutputUsage(entry, body.usage);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch { /* ignore */ }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
167
243
|
},
|
|
168
244
|
error: (err, _req, res) => {
|
|
169
245
|
stats.totalErrors++;
|
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(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,9 +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 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;
|
|
116
|
+
const cacheColor = cacheHitPct === null ? undefined
|
|
117
|
+
: cacheHitPct >= 70 ? "green"
|
|
118
|
+
: cacheHitPct >= 30 ? "yellow"
|
|
119
|
+
: "red";
|
|
112
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
|
|
113
121
|
? _jsxs(Text, { backgroundColor: bg, color: fg("white"), children: [" ", log.method, " ", log.path.padEnd(14)] })
|
|
114
|
-
: _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"] })), 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] }))] }));
|
|
115
123
|
}
|
|
116
124
|
// ─── Detail panel ─────────────────────────────────────────────────────────────
|
|
117
125
|
function DetailPanel({ log }) {
|
|
@@ -129,7 +137,7 @@ function DetailPanel({ log }) {
|
|
|
129
137
|
: log.statusCode >= 500 ? "red"
|
|
130
138
|
: log.statusCode >= 400 ? "yellow"
|
|
131
139
|
: "green";
|
|
132
|
-
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 }) }))] })] }));
|
|
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 }) }))] })] }));
|
|
133
141
|
}
|
|
134
142
|
function Field({ label, value }) {
|
|
135
143
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: "white", children: value })] }));
|
|
@@ -137,6 +145,39 @@ function Field({ label, value }) {
|
|
|
137
145
|
function FieldColored({ label, value, color }) {
|
|
138
146
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: color, children: value })] }));
|
|
139
147
|
}
|
|
148
|
+
// ─── Cache health badge (aggregated) ─────────────────────────────────────────
|
|
149
|
+
function CacheHealthBadge({ read, created, input }) {
|
|
150
|
+
const total = read + created + input;
|
|
151
|
+
if (total === 0)
|
|
152
|
+
return null;
|
|
153
|
+
const hitPct = Math.round((read / total) * 100);
|
|
154
|
+
const color = hitPct >= 70 ? "green" : hitPct >= 30 ? "yellow" : "red";
|
|
155
|
+
const label = hitPct >= 70 ? "healthy" : hitPct >= 30 ? "fair" : "poor";
|
|
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, ")"] })] }));
|
|
157
|
+
}
|
|
158
|
+
// ─── Cache breakdown (per-request detail) ────────────────────────────────────
|
|
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) })] }));
|
|
173
|
+
}
|
|
174
|
+
function fmtTok(n) {
|
|
175
|
+
if (n >= 1_000_000)
|
|
176
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
177
|
+
if (n >= 1_000)
|
|
178
|
+
return `${(n / 1_000).toFixed(1)}k`;
|
|
179
|
+
return String(n);
|
|
180
|
+
}
|
|
140
181
|
// ─── HTTP status text ─────────────────────────────────────────────────────────
|
|
141
182
|
function httpStatusText(code) {
|
|
142
183
|
const map = {
|