@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.
@@ -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 capacityRows = getAccountCapacityRows(a);
540
- const isLimited = rl.status === "rate_limited";
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 = a.provider === "openai_subscription"
551
- ? " [OpenAI]"
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
- 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: a.provider === "openai_subscription" ? "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) }), a.provider !== "openai_subscription" && (_jsxs(Text, { color: "gray", children: [" ", a.activeSessions ?? 0, " active / ", a.inFlightRequests ?? 0, " streams"] })), capsHint && _jsx(Text, { color: "yellow", children: capsHint })] }), (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) : ""}` })] })), 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}`)))] }));
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@timo972/cc-router",
3
- "version": "0.9.0",
3
+ "version": "0.10.0-rc.0",
4
4
  "description": "Cache-aware session router for Claude Max OAuth tokens — use multiple Claude Max accounts with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- }