@timo972/cc-router 0.9.0 → 0.10.0-rc.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/CHANGELOG.md +88 -2
- package/README.md +2 -0
- package/dist/config/manager.js +15 -4
- package/dist/protocol/openai-response-to-anthropic.js +16 -1
- package/dist/protocol/openai-responses-collect.js +190 -11
- package/dist/protocol/openai-stream-to-anthropic.js +20 -2
- package/dist/protocol/sse.js +10 -1
- package/dist/providers/openai/account-state.js +377 -0
- package/dist/providers/openai/codex-transport.js +8 -0
- package/dist/providers/openai/failure-routing.js +141 -0
- package/dist/providers/openai/token-pool.js +401 -0
- package/dist/providers/openai/token-refresher.js +144 -11
- package/dist/providers/openai/usage.js +160 -0
- package/dist/proxy/account-add.js +10 -7
- package/dist/proxy/account-deletion.js +5 -1
- package/dist/proxy/account-patch.js +64 -0
- package/dist/proxy/account-pool.js +19 -0
- package/dist/proxy/anthropic-routing.js +2 -2
- package/dist/proxy/lease-lifecycle.js +6 -4
- package/dist/proxy/messages-cross-route.js +243 -62
- package/dist/proxy/openai-ingress.js +379 -0
- package/dist/proxy/openai-routing.js +61 -0
- package/dist/proxy/provider-routing.js +8 -4
- package/dist/proxy/responses-server.js +109 -48
- package/dist/proxy/server.js +241 -65
- package/dist/proxy/stats.js +30 -1
- package/dist/proxy/token-pool.js +3 -19
- package/dist/ui/Dashboard.js +93 -17
- package/package.json +1 -1
- package/dist/providers/openai/account-pool.js +0 -11
package/dist/ui/Dashboard.js
CHANGED
|
@@ -92,6 +92,79 @@ export function getAccountCapacityRows(account) {
|
|
|
92
92
|
}
|
|
93
93
|
return rows;
|
|
94
94
|
}
|
|
95
|
+
function codexWindowLabel(windowMinutes, fallback) {
|
|
96
|
+
// 10_080 minutes reads better as "weekly" than the generic "168h"; 300 needs
|
|
97
|
+
// no special case because the generic branch already renders it as "5h".
|
|
98
|
+
if (windowMinutes === 10_080)
|
|
99
|
+
return "weekly";
|
|
100
|
+
if (windowMinutes > 0) {
|
|
101
|
+
return windowMinutes % 60 === 0 ? `${windowMinutes / 60}h` : `${windowMinutes}m`;
|
|
102
|
+
}
|
|
103
|
+
return fallback;
|
|
104
|
+
}
|
|
105
|
+
/** Named Codex metered buckets as compact capacity rows (default bucket renders as bars). */
|
|
106
|
+
export function getCodexCapacityRows(codex, globalCooldownUntilMs, now = Date.now()) {
|
|
107
|
+
const rows = [];
|
|
108
|
+
for (const bucket of codex?.buckets ?? []) {
|
|
109
|
+
if (bucket.limitId === "codex")
|
|
110
|
+
continue;
|
|
111
|
+
const cooling = bucket.cooldownUntilMs > now;
|
|
112
|
+
const windows = [];
|
|
113
|
+
if (bucket.primary) {
|
|
114
|
+
windows.push({ label: codexWindowLabel(bucket.primary.windowMinutes, "5h"), ...bucket.primary });
|
|
115
|
+
}
|
|
116
|
+
if (bucket.secondary) {
|
|
117
|
+
windows.push({ label: codexWindowLabel(bucket.secondary.windowMinutes, "weekly"), ...bucket.secondary });
|
|
118
|
+
}
|
|
119
|
+
for (const window of windows) {
|
|
120
|
+
const exhausted = window.utilization >= 1;
|
|
121
|
+
// While cooling, the countdown that matters is the cooldown's own expiry
|
|
122
|
+
// — it can come from Retry-After and outlast (or replace) the window
|
|
123
|
+
// reset, and a row labeled "bucket cooldown" showing the window's reset
|
|
124
|
+
// instant, or no time at all when that reset is unknown, tells the
|
|
125
|
+
// operator the wrong thing about when routing resumes.
|
|
126
|
+
const resetAt = cooling
|
|
127
|
+
? Math.floor(bucket.cooldownUntilMs / 1000)
|
|
128
|
+
: window.resetAt;
|
|
129
|
+
rows.push({
|
|
130
|
+
label: `${bucket.label} ${window.label}`,
|
|
131
|
+
state: cooling ? "bucket cooldown" : exhausted ? "exhausted" : "available",
|
|
132
|
+
color: cooling ? "yellow" : exhausted ? "red" : window.utilization >= 0.7 ? "yellow" : "green",
|
|
133
|
+
utilization: window.utilization,
|
|
134
|
+
...(resetAt > 0 ? { resetAt } : {}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (windows.length === 0 && cooling) {
|
|
138
|
+
rows.push({
|
|
139
|
+
label: bucket.label,
|
|
140
|
+
state: "bucket cooldown",
|
|
141
|
+
color: "yellow",
|
|
142
|
+
resetAt: Math.floor(bucket.cooldownUntilMs / 1000),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (globalCooldownUntilMs && globalCooldownUntilMs > now) {
|
|
147
|
+
rows.push({ label: "cooldown", state: "global", color: "red", resetAt: Math.floor(globalCooldownUntilMs / 1000) });
|
|
148
|
+
}
|
|
149
|
+
return rows;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* True when an OpenAI/Codex account is hard-blocked from routing: the
|
|
153
|
+
* account-wide status reports `rate_limited`, or the default bucket
|
|
154
|
+
* (`limitId === "codex"`) has a fully exhausted primary or secondary window.
|
|
155
|
+
* Named model-scoped buckets exhausting on their own does not count — the
|
|
156
|
+
* account can still serve other models via those buckets' fallback.
|
|
157
|
+
*/
|
|
158
|
+
export function isCodexLimited(codex) {
|
|
159
|
+
if (!codex)
|
|
160
|
+
return false;
|
|
161
|
+
if (codex.status === "rate_limited")
|
|
162
|
+
return true;
|
|
163
|
+
const defaultBucket = codex.buckets.find(bucket => bucket.limitId === "codex");
|
|
164
|
+
if (!defaultBucket)
|
|
165
|
+
return false;
|
|
166
|
+
return (defaultBucket.primary?.utilization ?? 0) >= 1 || (defaultBucket.secondary?.utilization ?? 0) >= 1;
|
|
167
|
+
}
|
|
95
168
|
export function Dashboard({ port, baseUrl, authToken, onIntent }) {
|
|
96
169
|
const { exit } = useApp();
|
|
97
170
|
const [data, setData] = useState(null);
|
|
@@ -251,10 +324,6 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
251
324
|
const doSetLimit = useCallback(async (field, value) => {
|
|
252
325
|
if (!selectedAccount)
|
|
253
326
|
return;
|
|
254
|
-
if (selectedAccount.provider === "openai_subscription") {
|
|
255
|
-
showBanner("OpenAI accounts do not use Anthropic caps", "yellow");
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
327
|
try {
|
|
259
328
|
await api.patch(selectedAccount.id, { [field]: value });
|
|
260
329
|
const label = field === "sessionLimitPercent" ? "5h cap" : "7d cap";
|
|
@@ -415,19 +484,11 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
415
484
|
return;
|
|
416
485
|
}
|
|
417
486
|
if (input === "w") {
|
|
418
|
-
if (!selectedAccountIsAnthropic) {
|
|
419
|
-
showBanner("OpenAI accounts do not use Anthropic caps", "yellow");
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
487
|
setMode("editWeekly");
|
|
423
488
|
setEditBuffer("");
|
|
424
489
|
return;
|
|
425
490
|
}
|
|
426
491
|
if (input === "s") {
|
|
427
|
-
if (!selectedAccountIsAnthropic) {
|
|
428
|
-
showBanner("OpenAI accounts do not use Anthropic caps", "yellow");
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
492
|
setMode("editSession");
|
|
432
493
|
setEditBuffer("");
|
|
433
494
|
return;
|
|
@@ -536,8 +597,13 @@ function AccountRow({ account: a, selected }) {
|
|
|
536
597
|
const rl = a.rateLimits ?? EMPTY_RL;
|
|
537
598
|
const usage = rl.usage;
|
|
538
599
|
const globalCapacity = getGlobalCapacityView(rl);
|
|
539
|
-
const
|
|
540
|
-
const
|
|
600
|
+
const isOpenAI = a.provider === "openai_subscription";
|
|
601
|
+
const codex = a.codexRateLimits;
|
|
602
|
+
const codexDefaultBucket = codex?.buckets.find(bucket => bucket.limitId === "codex");
|
|
603
|
+
const capacityRows = isOpenAI
|
|
604
|
+
? getCodexCapacityRows(a.codexRateLimits, a.globalCooldownUntilMs)
|
|
605
|
+
: getAccountCapacityRows(a);
|
|
606
|
+
const isLimited = isOpenAI ? isCodexLimited(a.codexRateLimits) : rl.status === "rate_limited";
|
|
541
607
|
const isDisabled = a.enabled === false;
|
|
542
608
|
const dot = isDisabled ? "⊘" : isLimited ? "⊘" : a.busy ? "◌" : a.healthy ? "●" : "●";
|
|
543
609
|
const dotColor = isDisabled ? "gray" : isLimited ? "red" : a.busy ? "yellow" : a.healthy ? "green" : "red";
|
|
@@ -547,8 +613,8 @@ function AccountRow({ account: a, selected }) {
|
|
|
547
613
|
const expiryColor = a.expiresInMs < 10 * 60 * 1000 ? "red"
|
|
548
614
|
: a.expiresInMs < 30 * 60 * 1000 ? "yellow"
|
|
549
615
|
: "white";
|
|
550
|
-
const providerTag =
|
|
551
|
-
?
|
|
616
|
+
const providerTag = isOpenAI
|
|
617
|
+
? ` [OpenAI${codex?.plan ? ` ${codex.plan}` : ""}]`
|
|
552
618
|
: rl.plan ? ` [${rl.plan}]` : "";
|
|
553
619
|
// User-defined caps hint
|
|
554
620
|
const s5 = a.sessionLimitPercent ?? 100;
|
|
@@ -559,7 +625,17 @@ function AccountRow({ account: a, selected }) {
|
|
|
559
625
|
: "";
|
|
560
626
|
const pointer = selected ? "▶" : " ";
|
|
561
627
|
const nameColor = isDisabled ? "gray" : undefined;
|
|
562
|
-
|
|
628
|
+
const creditsLabel = codex?.credits
|
|
629
|
+
? codex.credits.unlimited
|
|
630
|
+
? "∞"
|
|
631
|
+
: codex.credits.balance !== undefined
|
|
632
|
+
? codex.credits.balance
|
|
633
|
+
: codex.credits.hasCredits ? "yes" : "no"
|
|
634
|
+
: undefined;
|
|
635
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: pointer }), _jsxs(Text, { color: dotColor, children: [" ", dot, " "] }), _jsx(Text, { color: nameColor, dimColor: isDisabled, children: a.id.slice(0, 20).padEnd(20) }), _jsx(Text, { color: statusColor, children: statusLabel }), providerTag && _jsx(Text, { color: isOpenAI ? "cyan" : "magenta", children: providerTag.padEnd(10) }), !providerTag && _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) }), _jsxs(Text, { color: "gray", children: [" ", a.activeSessions ?? 0, " active / ", a.inFlightRequests ?? 0, " streams"] }), capsHint && _jsx(Text, { color: "yellow", children: capsHint }), a.credentialsPendingWrite && (
|
|
636
|
+
// The account still works — its rotated token is live in memory — but a
|
|
637
|
+
// restart before the pending write lands would need a re-login.
|
|
638
|
+
_jsx(Text, { color: "yellow", children: " creds unsaved" }))] }), (rl.lastUpdated > 0 || usage) && (_jsxs(Box, { paddingLeft: 4, children: [_jsx(UtilBar, { label: "5h", util: globalCapacity.fiveHour.utilization, resetTs: globalCapacity.fiveHour.resetAt, isActive: rl.claim === "five_hour", cap: s5 }), _jsx(Text, { children: " " }), _jsx(UtilBar, { label: "7d all-model", util: globalCapacity.sevenDay.utilization, resetTs: globalCapacity.sevenDay.resetAt, isActive: rl.claim === "seven_day", cap: w7 }), usage && _jsx(Text, { color: globalCapacity.usageFetchStatus === "fresh" ? "gray" : "yellow", children: ` usage ${globalCapacity.usageFetchStatus} ${usage.fetchedAt > 0 ? formatAgo(usage.fetchedAt) : ""}` })] })), isOpenAI && codexDefaultBucket && (codexDefaultBucket.primary || codexDefaultBucket.secondary) && (_jsxs(Box, { paddingLeft: 4, children: [_jsx(UtilBar, { label: "5h", util: codexDefaultBucket.primary?.utilization ?? 0, resetTs: codexDefaultBucket.primary?.resetAt ?? 0, isActive: false, cap: s5 }), _jsx(Text, { children: " " }), _jsx(UtilBar, { label: "weekly", util: codexDefaultBucket.secondary?.utilization ?? 0, resetTs: codexDefaultBucket.secondary?.resetAt ?? 0, isActive: false, cap: w7 }), creditsLabel !== undefined && _jsx(Text, { color: "gray", children: ` credits ${creditsLabel}` })] })), capacityRows.map((row, index) => (_jsxs(Box, { paddingLeft: 4, children: [_jsxs(Text, { color: row.color, children: [" ", row.label] }), _jsxs(Text, { color: "gray", children: [" \u00B7 ", row.state] }), row.utilization !== undefined && _jsx(Text, { color: row.color, children: ` ${Math.round(row.utilization * 100)}%` }), row.resetAt !== undefined && row.resetAt > 0 && (_jsxs(Text, { color: "gray", children: [" ", `↻${formatResetIn(row.resetAt)}`] }))] }, `${row.label}-${index}`)))] }));
|
|
563
639
|
}
|
|
564
640
|
// ─── Utilization bar ─────────────────────────────────────────────────────────
|
|
565
641
|
function UtilBar({ label, util, resetTs, isActive, cap }) {
|
package/package.json
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export function createOpenAIAccountPicker(accounts) {
|
|
2
|
-
let index = 0;
|
|
3
|
-
return () => {
|
|
4
|
-
const enabled = accounts.filter(account => account.enabled);
|
|
5
|
-
if (enabled.length === 0)
|
|
6
|
-
return null;
|
|
7
|
-
const account = enabled[index % enabled.length];
|
|
8
|
-
index = (index + 1) % enabled.length;
|
|
9
|
-
return account;
|
|
10
|
-
};
|
|
11
|
-
}
|