ai-cc-router 0.1.7 → 0.1.9
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 +32 -0
- package/dist/proxy/token-pool.js +18 -6
- package/dist/proxy/types.js +11 -1
- package/dist/ui/Dashboard.js +38 -7
- 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
|
@@ -24,6 +24,34 @@ function applyOutputUsage(entry, usage) {
|
|
|
24
24
|
entry.outputTokens = usage["output_tokens"] ?? 0;
|
|
25
25
|
stats.totalOutputTokens += entry.outputTokens;
|
|
26
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
|
+
}
|
|
27
55
|
export async function startServer(opts = {}) {
|
|
28
56
|
const port = opts.port ?? PROXY_PORT;
|
|
29
57
|
// Direct-to-Anthropic (standalone) or via LiteLLM (full mode).
|
|
@@ -182,6 +210,10 @@ export async function startServer(opts = {}) {
|
|
|
182
210
|
account.busy = true;
|
|
183
211
|
setTimeout(() => { account.busy = false; }, 30_000);
|
|
184
212
|
}
|
|
213
|
+
// ── Capture rate limit utilization from response headers ────────────
|
|
214
|
+
const rl = extractRateLimits(proxyRes.headers);
|
|
215
|
+
if (rl)
|
|
216
|
+
account.rateLimits = rl;
|
|
185
217
|
const entry = pendingLog;
|
|
186
218
|
stats.addLog(entry);
|
|
187
219
|
// ── Capture token usage from Anthropic response body ─────────────────
|
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
|
@@ -3,6 +3,11 @@ import { useState, useEffect } from "react";
|
|
|
3
3
|
import { Box, Text, useInput, useApp } from "ink";
|
|
4
4
|
const POLL_INTERVAL_MS = 2_000;
|
|
5
5
|
const LOG_VISIBLE = 20;
|
|
6
|
+
const EMPTY_RL = {
|
|
7
|
+
status: "unknown", fiveHourUtil: 0, fiveHourReset: 0,
|
|
8
|
+
sevenDayUtil: 0, sevenDayReset: 0, claim: "", plan: "",
|
|
9
|
+
requestsLimit: 0, lastUpdated: 0,
|
|
10
|
+
};
|
|
6
11
|
// ─── Dashboard component ──────────────────────────────────────────────────────
|
|
7
12
|
export function Dashboard({ port }) {
|
|
8
13
|
const { exit } = useApp();
|
|
@@ -79,21 +84,47 @@ function LiveDashboard({ data, port, lastUpdate }) {
|
|
|
79
84
|
});
|
|
80
85
|
const selectedLog = logs[selectedIndex] ?? null;
|
|
81
86
|
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, { 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
|
|
87
|
+
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 ?? 0 })] }), _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
88
|
? _jsx(Text, { color: "gray", children: " No activity yet" })
|
|
84
89
|
: 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
90
|
}
|
|
86
|
-
// ─── Account row
|
|
91
|
+
// ─── Account row (two-line: status + utilization bars) ───────────────────────
|
|
87
92
|
function AccountRow({ account: a }) {
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
93
|
+
const rl = a.rateLimits ?? EMPTY_RL;
|
|
94
|
+
const isLimited = rl.status === "rate_limited";
|
|
95
|
+
const dot = isLimited ? "⊘" : a.busy ? "◌" : a.healthy ? "●" : "●";
|
|
96
|
+
const dotColor = isLimited ? "red" : a.busy ? "yellow" : a.healthy ? "green" : "red";
|
|
97
|
+
const statusLabel = isLimited ? "LIMITED" : a.busy ? "busy " : a.healthy ? "ok " : "ERROR ";
|
|
98
|
+
const statusColor = isLimited ? "red" : a.busy ? "yellow" : a.healthy ? "green" : "red";
|
|
92
99
|
const expiryLabel = a.expiresInMs > 0 ? formatMs(a.expiresInMs) : "EXPIRED";
|
|
93
100
|
const expiryColor = a.expiresInMs < 10 * 60 * 1000 ? "red"
|
|
94
101
|
: a.expiresInMs < 30 * 60 * 1000 ? "yellow"
|
|
95
102
|
: "white";
|
|
96
|
-
|
|
103
|
+
const planTag = rl.plan ? ` [${rl.plan}]` : "";
|
|
104
|
+
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" })] }))] }));
|
|
105
|
+
}
|
|
106
|
+
// ─── Utilization bar ─────────────────────────────────────────────────────────
|
|
107
|
+
function UtilBar({ label, util, resetTs, isActive }) {
|
|
108
|
+
const pct = Math.round(util * 100);
|
|
109
|
+
const BAR_W = 12;
|
|
110
|
+
const filled = Math.round(util * BAR_W);
|
|
111
|
+
const bar = "█".repeat(Math.min(filled, BAR_W)) + "░".repeat(Math.max(BAR_W - filled, 0));
|
|
112
|
+
const color = pct >= 90 ? "red" : pct >= 70 ? "yellow" : "green";
|
|
113
|
+
const resetLabel = resetTs > 0 ? formatResetIn(resetTs) : "";
|
|
114
|
+
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] })] }));
|
|
115
|
+
}
|
|
116
|
+
function formatResetIn(unixSeconds) {
|
|
117
|
+
const diff = unixSeconds - Date.now() / 1000;
|
|
118
|
+
if (diff <= 0)
|
|
119
|
+
return "now";
|
|
120
|
+
const d = Math.floor(diff / 86400);
|
|
121
|
+
const h = Math.floor((diff % 86400) / 3600);
|
|
122
|
+
const m = Math.floor((diff % 3600) / 60);
|
|
123
|
+
if (d > 0)
|
|
124
|
+
return `${d}d${h}h`;
|
|
125
|
+
if (h > 0)
|
|
126
|
+
return `${h}h${m}m`;
|
|
127
|
+
return `${m}m`;
|
|
97
128
|
}
|
|
98
129
|
// ─── Log row ──────────────────────────────────────────────────────────────────
|
|
99
130
|
function LogRow({ log, selected }) {
|