ai-cc-router 0.1.6 → 0.1.8
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/cli/cmd-setup.js +2 -0
- package/dist/config/manager.js +2 -0
- package/dist/proxy/server.js +75 -31
- package/dist/proxy/stats.js +1 -0
- package/dist/proxy/token-pool.js +18 -6
- package/dist/proxy/types.js +11 -1
- package/dist/ui/Dashboard.js +53 -17
- package/package.json +1 -1
package/dist/cli/cmd-setup.js
CHANGED
|
@@ -9,6 +9,7 @@ import { writeClaudeSettings, readClaudeProxySettings } from "../utils/claude-co
|
|
|
9
9
|
import { saveAccounts } from "../proxy/token-refresher.js";
|
|
10
10
|
import { loadAccounts, accountsFileExists, readConfig, writeConfig, generateProxySecret } from "../config/manager.js";
|
|
11
11
|
import { PROXY_PORT } from "../config/paths.js";
|
|
12
|
+
import { DEFAULT_RATE_LIMITS } from "../proxy/types.js";
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
// ─── Public registration ──────────────────────────────────────────────────────
|
|
14
15
|
export function registerSetup(program) {
|
|
@@ -101,6 +102,7 @@ export async function setupSingleAccount(index) {
|
|
|
101
102
|
lastUsed: 0,
|
|
102
103
|
lastRefresh: 0,
|
|
103
104
|
consecutiveErrors: 0,
|
|
105
|
+
rateLimits: { ...DEFAULT_RATE_LIMITS },
|
|
104
106
|
};
|
|
105
107
|
}
|
|
106
108
|
// ─── Full wizard ──────────────────────────────────────────────────────────────
|
package/dist/config/manager.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "fs";
|
|
2
2
|
import { randomBytes } from "crypto";
|
|
3
3
|
import { CONFIG_DIR, ACCOUNTS_PATH, CONFIG_PATH } from "./paths.js";
|
|
4
|
+
import { DEFAULT_RATE_LIMITS } from "../proxy/types.js";
|
|
4
5
|
export function ensureConfigDir() {
|
|
5
6
|
if (!existsSync(CONFIG_DIR)) {
|
|
6
7
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
@@ -73,5 +74,6 @@ function deserialize(records) {
|
|
|
73
74
|
lastUsed: 0,
|
|
74
75
|
lastRefresh: 0,
|
|
75
76
|
consecutiveErrors: 0,
|
|
77
|
+
rateLimits: { ...DEFAULT_RATE_LIMITS },
|
|
76
78
|
}));
|
|
77
79
|
}
|
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,38 @@ 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
|
+
}
|
|
27
|
+
// ─── Rate limit header extraction ──────────────────────────────────────────
|
|
28
|
+
function inferPlan(requestsLimit) {
|
|
29
|
+
if (requestsLimit <= 0)
|
|
30
|
+
return "";
|
|
31
|
+
if (requestsLimit <= 100)
|
|
32
|
+
return "Pro";
|
|
33
|
+
if (requestsLimit <= 500)
|
|
34
|
+
return "Max 5x";
|
|
35
|
+
return "Max 20x";
|
|
36
|
+
}
|
|
37
|
+
function extractRateLimits(headers) {
|
|
38
|
+
const h = (name) => String(headers[name] ?? "");
|
|
39
|
+
const status = h("anthropic-ratelimit-unified-status");
|
|
40
|
+
if (!status)
|
|
41
|
+
return null; // No unified headers in this response
|
|
42
|
+
const requestsLimit = parseInt(h("anthropic-ratelimit-requests-limit"), 10) || 0;
|
|
43
|
+
return {
|
|
44
|
+
status: status === "rate_limited" ? "rate_limited" : "allowed",
|
|
45
|
+
fiveHourUtil: parseFloat(h("anthropic-ratelimit-unified-5h-utilization")) || 0,
|
|
46
|
+
fiveHourReset: parseInt(h("anthropic-ratelimit-unified-5h-reset"), 10) || 0,
|
|
47
|
+
sevenDayUtil: parseFloat(h("anthropic-ratelimit-unified-7d-utilization")) || 0,
|
|
48
|
+
sevenDayReset: parseInt(h("anthropic-ratelimit-unified-7d-reset"), 10) || 0,
|
|
49
|
+
claim: h("anthropic-ratelimit-unified-representative-claim"),
|
|
50
|
+
plan: inferPlan(requestsLimit),
|
|
51
|
+
requestsLimit,
|
|
52
|
+
lastUpdated: Date.now(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
23
55
|
export async function startServer(opts = {}) {
|
|
24
56
|
const port = opts.port ?? PROXY_PORT;
|
|
25
57
|
// Direct-to-Anthropic (standalone) or via LiteLLM (full mode).
|
|
@@ -79,6 +111,7 @@ export async function startServer(opts = {}) {
|
|
|
79
111
|
totalCacheReadTokens: stats.totalCacheReadTokens,
|
|
80
112
|
totalCacheCreationTokens: stats.totalCacheCreationTokens,
|
|
81
113
|
totalInputTokens: stats.totalInputTokens,
|
|
114
|
+
totalOutputTokens: stats.totalOutputTokens,
|
|
82
115
|
accounts: pool.getStats(),
|
|
83
116
|
recentLogs: stats.getRecentLogs(50),
|
|
84
117
|
});
|
|
@@ -177,52 +210,63 @@ export async function startServer(opts = {}) {
|
|
|
177
210
|
account.busy = true;
|
|
178
211
|
setTimeout(() => { account.busy = false; }, 30_000);
|
|
179
212
|
}
|
|
213
|
+
// ── Capture rate limit utilization from response headers ────────────
|
|
214
|
+
const rl = extractRateLimits(proxyRes.headers);
|
|
215
|
+
if (rl)
|
|
216
|
+
account.rateLimits = rl;
|
|
180
217
|
const entry = pendingLog;
|
|
181
218
|
stats.addLog(entry);
|
|
182
|
-
// ── Capture
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// streaming
|
|
219
|
+
// ── Capture token usage from Anthropic response body ─────────────────
|
|
220
|
+
// SSE streams carry usage across two events:
|
|
221
|
+
// message_start → input_tokens, cache_read/creation_input_tokens
|
|
222
|
+
// message_delta → output_tokens
|
|
223
|
+
// Non-streaming JSON carries all fields in a single usage object.
|
|
224
|
+
// We use incremental line parsing (not buffering) so we can capture
|
|
225
|
+
// both events without holding the full stream in memory.
|
|
187
226
|
const contentType = String(proxyRes.headers["content-type"] ?? "");
|
|
188
227
|
const encoding = String(proxyRes.headers["content-encoding"] ?? "");
|
|
189
228
|
const isCompressed = /gzip|br|deflate/.test(encoding);
|
|
190
229
|
if (!isCompressed && (contentType.includes("text/event-stream") || contentType.includes("application/json"))) {
|
|
191
230
|
const isSSE = contentType.includes("text/event-stream");
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
231
|
+
if (isSSE) {
|
|
232
|
+
let lineBuf = "";
|
|
233
|
+
let gotInput = false;
|
|
234
|
+
let gotOutput = false;
|
|
235
|
+
proxyRes.on("data", (chunk) => {
|
|
236
|
+
if (gotInput && gotOutput)
|
|
237
|
+
return;
|
|
238
|
+
lineBuf += chunk.toString("utf8");
|
|
239
|
+
const lines = lineBuf.split("\n");
|
|
240
|
+
lineBuf = lines.pop() ?? ""; // keep incomplete last line
|
|
202
241
|
for (const line of lines) {
|
|
203
242
|
if (!line.startsWith("data: "))
|
|
204
243
|
continue;
|
|
205
244
|
try {
|
|
206
245
|
const evt = JSON.parse(line.slice(6));
|
|
207
|
-
if (evt.type === "message_start" && evt.message?.usage) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
246
|
+
if (!gotInput && evt.type === "message_start" && evt.message?.usage) {
|
|
247
|
+
applyInputUsage(entry, evt.message.usage);
|
|
248
|
+
gotInput = true;
|
|
249
|
+
}
|
|
250
|
+
if (!gotOutput && evt.type === "message_delta" && evt.usage) {
|
|
251
|
+
applyOutputUsage(entry, evt.usage);
|
|
252
|
+
gotOutput = true;
|
|
211
253
|
}
|
|
212
254
|
}
|
|
213
|
-
catch { /* partial JSON
|
|
255
|
+
catch { /* partial JSON across chunk boundary — next chunk will complete it */ }
|
|
214
256
|
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Non-streaming:
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Non-streaming JSON: buffer full body then parse once
|
|
261
|
+
let buf = "";
|
|
262
|
+
proxyRes.on("data", (chunk) => { buf += chunk.toString("utf8"); });
|
|
219
263
|
proxyRes.on("end", () => {
|
|
220
|
-
if (captured)
|
|
221
|
-
return;
|
|
222
264
|
try {
|
|
223
265
|
const body = JSON.parse(buf);
|
|
224
|
-
if (body.usage)
|
|
225
|
-
|
|
266
|
+
if (body.usage) {
|
|
267
|
+
applyInputUsage(entry, body.usage);
|
|
268
|
+
applyOutputUsage(entry, body.usage);
|
|
269
|
+
}
|
|
226
270
|
}
|
|
227
271
|
catch { /* ignore */ }
|
|
228
272
|
});
|
package/dist/proxy/stats.js
CHANGED
package/dist/proxy/token-pool.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/** Returns the earliest non-zero reset timestamp (seconds) for an account. */
|
|
2
|
+
function earliestReset(a) {
|
|
3
|
+
const r = a.rateLimits;
|
|
4
|
+
if (r.fiveHourReset && r.sevenDayReset)
|
|
5
|
+
return Math.min(r.fiveHourReset, r.sevenDayReset);
|
|
6
|
+
return r.fiveHourReset || r.sevenDayReset || Infinity;
|
|
7
|
+
}
|
|
1
8
|
export class TokenPool {
|
|
2
9
|
accounts;
|
|
3
10
|
currentIndex = 0;
|
|
@@ -5,20 +12,24 @@ export class TokenPool {
|
|
|
5
12
|
this.accounts = accounts;
|
|
6
13
|
}
|
|
7
14
|
/**
|
|
8
|
-
* Round-robin selection among healthy, non-busy accounts.
|
|
9
|
-
* Falls back to least-loaded if all are busy.
|
|
15
|
+
* Round-robin selection among healthy, non-busy, non-rate-limited accounts.
|
|
16
|
+
* Falls back to least-loaded if all are busy/limited.
|
|
17
|
+
* When all are rate-limited, picks the one with the earliest reset.
|
|
10
18
|
* Falls back to accounts[0] if all are unhealthy.
|
|
11
19
|
*/
|
|
12
20
|
getNext() {
|
|
13
|
-
const available = this.accounts.filter(a => a.healthy && !a.busy);
|
|
21
|
+
const available = this.accounts.filter(a => a.healthy && !a.busy && a.rateLimits.status !== "rate_limited");
|
|
14
22
|
if (available.length === 0) {
|
|
15
23
|
const healthy = this.accounts.filter(a => a.healthy);
|
|
16
24
|
if (healthy.length === 0) {
|
|
17
|
-
// Complete fallback: nothing healthy — return first account and hope it recovers
|
|
18
25
|
return this.accounts[0];
|
|
19
26
|
}
|
|
20
|
-
// All healthy but busy —
|
|
21
|
-
return healthy.reduce((
|
|
27
|
+
// All healthy but busy/limited — pick earliest reset time
|
|
28
|
+
return healthy.reduce((best, a) => {
|
|
29
|
+
const resetA = earliestReset(a);
|
|
30
|
+
const resetBest = earliestReset(best);
|
|
31
|
+
return resetA < resetBest ? a : best;
|
|
32
|
+
});
|
|
22
33
|
}
|
|
23
34
|
const account = available[this.currentIndex % available.length];
|
|
24
35
|
this.currentIndex = (this.currentIndex + 1) % available.length;
|
|
@@ -42,6 +53,7 @@ export class TokenPool {
|
|
|
42
53
|
expiresInMs: a.tokens.expiresAt - Date.now(),
|
|
43
54
|
lastUsedMs: a.lastUsed,
|
|
44
55
|
lastRefreshMs: a.lastRefresh,
|
|
56
|
+
rateLimits: a.rateLimits,
|
|
45
57
|
}));
|
|
46
58
|
}
|
|
47
59
|
}
|
package/dist/proxy/types.js
CHANGED
package/dist/ui/Dashboard.js
CHANGED
|
@@ -79,21 +79,47 @@ 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
|
}
|
|
86
|
-
// ─── Account row
|
|
86
|
+
// ─── Account row (two-line: status + utilization bars) ───────────────────────
|
|
87
87
|
function AccountRow({ account: a }) {
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
88
|
+
const rl = a.rateLimits;
|
|
89
|
+
const isLimited = rl.status === "rate_limited";
|
|
90
|
+
const dot = isLimited ? "⊘" : a.busy ? "◌" : a.healthy ? "●" : "●";
|
|
91
|
+
const dotColor = isLimited ? "red" : a.busy ? "yellow" : a.healthy ? "green" : "red";
|
|
92
|
+
const statusLabel = isLimited ? "LIMITED" : a.busy ? "busy " : a.healthy ? "ok " : "ERROR ";
|
|
93
|
+
const statusColor = isLimited ? "red" : a.busy ? "yellow" : a.healthy ? "green" : "red";
|
|
92
94
|
const expiryLabel = a.expiresInMs > 0 ? formatMs(a.expiresInMs) : "EXPIRED";
|
|
93
95
|
const expiryColor = a.expiresInMs < 10 * 60 * 1000 ? "red"
|
|
94
96
|
: a.expiresInMs < 30 * 60 * 1000 ? "yellow"
|
|
95
97
|
: "white";
|
|
96
|
-
|
|
98
|
+
const planTag = rl.plan ? ` [${rl.plan}]` : "";
|
|
99
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: dotColor, children: [" ", dot, " "] }), _jsx(Text, { children: a.id.slice(0, 20).padEnd(20) }), _jsx(Text, { color: statusColor, children: statusLabel }), planTag && _jsx(Text, { color: "magenta", children: planTag.padEnd(10) }), !planTag && _jsx(Text, { children: "".padEnd(10) }), _jsx(Text, { color: "gray", children: " req " }), _jsx(Text, { color: "white", children: String(a.requestCount).padStart(5) }), _jsx(Text, { color: "gray", children: " err " }), _jsx(Text, { color: a.errorCount > 0 ? "red" : "gray", children: String(a.errorCount).padStart(3) }), _jsx(Text, { color: "gray", children: " tok " }), _jsx(Text, { color: expiryColor, children: expiryLabel.padEnd(8) }), _jsx(Text, { color: "gray", children: " last " }), _jsx(Text, { color: "gray", children: formatAgo(a.lastUsedMs) })] }), rl.lastUpdated > 0 && (_jsxs(Box, { paddingLeft: 4, children: [_jsx(UtilBar, { label: "5h", util: rl.fiveHourUtil, resetTs: rl.fiveHourReset, isActive: rl.claim === "five_hour" }), _jsx(Text, { children: " " }), _jsx(UtilBar, { label: "7d", util: rl.sevenDayUtil, resetTs: rl.sevenDayReset, isActive: rl.claim === "seven_day" })] }))] }));
|
|
100
|
+
}
|
|
101
|
+
// ─── Utilization bar ─────────────────────────────────────────────────────────
|
|
102
|
+
function UtilBar({ label, util, resetTs, isActive }) {
|
|
103
|
+
const pct = Math.round(util * 100);
|
|
104
|
+
const BAR_W = 12;
|
|
105
|
+
const filled = Math.round(util * BAR_W);
|
|
106
|
+
const bar = "█".repeat(Math.min(filled, BAR_W)) + "░".repeat(Math.max(BAR_W - filled, 0));
|
|
107
|
+
const color = pct >= 90 ? "red" : pct >= 70 ? "yellow" : "green";
|
|
108
|
+
const resetLabel = resetTs > 0 ? formatResetIn(resetTs) : "";
|
|
109
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? "white" : "gray", bold: isActive, children: [label, " "] }), _jsx(Text, { color: color, children: bar }), _jsxs(Text, { color: color, children: [String(pct).padStart(4), "%"] }), resetLabel && _jsxs(Text, { color: "gray", children: [" \u21BB", resetLabel] })] }));
|
|
110
|
+
}
|
|
111
|
+
function formatResetIn(unixSeconds) {
|
|
112
|
+
const diff = unixSeconds - Date.now() / 1000;
|
|
113
|
+
if (diff <= 0)
|
|
114
|
+
return "now";
|
|
115
|
+
const d = Math.floor(diff / 86400);
|
|
116
|
+
const h = Math.floor((diff % 86400) / 3600);
|
|
117
|
+
const m = Math.floor((diff % 3600) / 60);
|
|
118
|
+
if (d > 0)
|
|
119
|
+
return `${d}d${h}h`;
|
|
120
|
+
if (h > 0)
|
|
121
|
+
return `${h}h${m}m`;
|
|
122
|
+
return `${m}m`;
|
|
97
123
|
}
|
|
98
124
|
// ─── Log row ──────────────────────────────────────────────────────────────────
|
|
99
125
|
function LogRow({ log, selected }) {
|
|
@@ -109,16 +135,17 @@ function LogRow({ log, selected }) {
|
|
|
109
135
|
: "gray";
|
|
110
136
|
const bg = selected ? "white" : undefined;
|
|
111
137
|
const fg = (c) => selected ? "black" : c;
|
|
112
|
-
// Per-request
|
|
113
|
-
const
|
|
114
|
-
const
|
|
138
|
+
// Per-request token stats
|
|
139
|
+
const inputTok = (log.cacheReadTokens ?? 0) + (log.cacheCreationTokens ?? 0) + (log.inputTokens ?? 0);
|
|
140
|
+
const outputTok = log.outputTokens ?? 0;
|
|
141
|
+
const cacheHitPct = inputTok > 0 ? Math.round(((log.cacheReadTokens ?? 0) / inputTok) * 100) : null;
|
|
115
142
|
const cacheColor = cacheHitPct === null ? undefined
|
|
116
143
|
: cacheHitPct >= 70 ? "green"
|
|
117
144
|
: cacheHitPct >= 30 ? "yellow"
|
|
118
145
|
: "red";
|
|
119
146
|
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
147
|
? _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] }))] }));
|
|
148
|
+
: _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
149
|
}
|
|
123
150
|
// ─── Detail panel ─────────────────────────────────────────────────────────────
|
|
124
151
|
function DetailPanel({ log }) {
|
|
@@ -136,7 +163,7 @@ function DetailPanel({ log }) {
|
|
|
136
163
|
: log.statusCode >= 500 ? "red"
|
|
137
164
|
: log.statusCode >= 400 ? "yellow"
|
|
138
165
|
: "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 }) }))] })] }));
|
|
166
|
+
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
167
|
}
|
|
141
168
|
function Field({ label, value }) {
|
|
142
169
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: "white", children: value })] }));
|
|
@@ -155,11 +182,20 @@ function CacheHealthBadge({ read, created, input }) {
|
|
|
155
182
|
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
183
|
}
|
|
157
184
|
// ─── 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:
|
|
185
|
+
function CacheBreakdown({ read, created, input, output }) {
|
|
186
|
+
const totalInput = read + created + input;
|
|
187
|
+
const hitPct = totalInput > 0 ? (read / totalInput) * 100 : 0;
|
|
188
|
+
const color = totalInput === 0 ? "gray" : hitPct >= 70 ? "green" : hitPct >= 30 ? "yellow" : "red";
|
|
189
|
+
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" })] }));
|
|
190
|
+
}
|
|
191
|
+
// ─── Token summary (aggregated totals) ──────────────────────────────────────
|
|
192
|
+
function TokenSummary({ cacheRead, cacheCreated, uncached, output }) {
|
|
193
|
+
const totalInput = cacheRead + cacheCreated + uncached;
|
|
194
|
+
const totalAll = totalInput + output;
|
|
195
|
+
if (totalAll === 0)
|
|
196
|
+
return null;
|
|
197
|
+
const hitPct = totalInput > 0 ? (cacheRead / totalInput) * 100 : 0;
|
|
198
|
+
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
199
|
}
|
|
164
200
|
function fmtTok(n) {
|
|
165
201
|
if (n >= 1_000_000)
|