@timo972/cc-router 0.11.0 → 0.12.0-rc.1
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 +12 -0
- package/README.md +33 -4
- package/dist/cli/cmd-accounts.js +91 -13
- package/dist/cli/cmd-cli-targets.js +177 -0
- package/dist/cli/cmd-client.js +3 -2
- package/dist/cli/cmd-configure.js +18 -4
- package/dist/cli/cmd-status.js +4 -1
- package/dist/cli/cmd-stop.js +18 -0
- package/dist/cli/index.js +10 -3
- package/dist/config/manager.js +19 -3
- package/dist/config/paths.js +1 -0
- package/dist/protocol/anthropic-to-openai.js +31 -19
- package/dist/protocol/model-ref.js +1 -1
- package/dist/protocol/openai-function-call.js +16 -0
- package/dist/protocol/openai-response-to-anthropic.js +34 -6
- package/dist/protocol/openai-stream-to-anthropic.js +116 -16
- package/dist/protocol/openai-to-anthropic.js +54 -29
- package/dist/providers/openai/account-state.js +4 -1
- package/dist/providers/openai/usage.js +14 -1
- package/dist/providers/xai/account-record.js +42 -0
- package/dist/providers/xai/device-oauth.js +116 -0
- package/dist/providers/xai/import-auth.js +70 -0
- package/dist/providers/xai/overview.js +279 -0
- package/dist/providers/xai/subscription-fetch.js +56 -0
- package/dist/proxy/allowance.js +142 -0
- package/dist/proxy/logger.js +4 -3
- package/dist/proxy/messages-cross-route.js +113 -8
- package/dist/proxy/models-server.js +30 -2
- package/dist/proxy/openai-ingress.js +7 -1
- package/dist/proxy/openai-routing.js +12 -7
- package/dist/proxy/server.js +110 -6
- package/dist/ui/Dashboard.js +383 -124
- package/dist/ui/accountsApi.js +3 -1
- package/dist/utils/cli-routing.js +80 -0
- package/dist/utils/codex-config.js +337 -15
- package/package.json +3 -2
package/dist/ui/Dashboard.js
CHANGED
|
@@ -4,7 +4,11 @@ import { Box, Text, useInput, useApp, useStdout, measureElement } from "ink";
|
|
|
4
4
|
import { createAccountsApi } from "./accountsApi.js";
|
|
5
5
|
import { createModelsApi } from "./modelsApi.js";
|
|
6
6
|
import { getCurrentVersion } from "../utils/self-update.js";
|
|
7
|
+
import { readClaudeRouting, readCodexRouting, setClaudeRouting, setCodexRouting, } from "../utils/cli-routing.js";
|
|
8
|
+
import { mergeGrokIntoHealth, loadGrokHealthSnapshotsWithSubscription } from "../providers/xai/overview.js";
|
|
7
9
|
const POLL_INTERVAL_MS = 2_000;
|
|
10
|
+
/** Grok's plan/code-access rarely change; refresh far slower than the 2s poll. */
|
|
11
|
+
const GROK_SUBSCRIPTION_INTERVAL_MS = 60_000;
|
|
8
12
|
/** Most activity rows the dashboard will show — the list shrinks below this
|
|
9
13
|
* (down to MIN_LOG_VISIBLE) when the terminal is too short for the full
|
|
10
14
|
* frame. Ink can only erase as many lines as the viewport holds, so a frame
|
|
@@ -223,6 +227,123 @@ export function isCodexLimited(codex) {
|
|
|
223
227
|
return false;
|
|
224
228
|
return (defaultBucket.primary?.utilization ?? 0) >= 1 || (defaultBucket.secondary?.utilization ?? 0) >= 1;
|
|
225
229
|
}
|
|
230
|
+
export function isOpenAIAccount(account) {
|
|
231
|
+
return account.provider === "openai_subscription";
|
|
232
|
+
}
|
|
233
|
+
export function isXaiAccount(account) {
|
|
234
|
+
return account.provider === "xai_subscription";
|
|
235
|
+
}
|
|
236
|
+
export function isClaudeAccount(account) {
|
|
237
|
+
return !isOpenAIAccount(account) && !isXaiAccount(account);
|
|
238
|
+
}
|
|
239
|
+
/** Compact `rst` column: banked Codex usage-limit resets. Claude is an em dash. */
|
|
240
|
+
export function resetCreditsColumnLabel(account) {
|
|
241
|
+
if (!isOpenAIAccount(account))
|
|
242
|
+
return "—";
|
|
243
|
+
return String(account.codexRateLimits?.resetCredits?.available ?? 0);
|
|
244
|
+
}
|
|
245
|
+
export function isLimitedAccount(account) {
|
|
246
|
+
if (isXaiAccount(account))
|
|
247
|
+
return false;
|
|
248
|
+
if (isOpenAIAccount(account))
|
|
249
|
+
return isCodexLimited(account.codexRateLimits);
|
|
250
|
+
if (account.rateLimits?.status === "rate_limited")
|
|
251
|
+
return true;
|
|
252
|
+
if (!account.rateLimits)
|
|
253
|
+
return false;
|
|
254
|
+
const view = getGlobalCapacityView(account.rateLimits);
|
|
255
|
+
return view.fiveHour.utilization >= 1 || view.sevenDay.utilization >= 1;
|
|
256
|
+
}
|
|
257
|
+
/** Claude, then ChatGPT, then Grok; within each group, accounts at limit last. */
|
|
258
|
+
export function orderAccountsForDashboard(accounts) {
|
|
259
|
+
const usableThenLimited = (list) => [
|
|
260
|
+
...list.filter(account => !isLimitedAccount(account)),
|
|
261
|
+
...list.filter(account => isLimitedAccount(account)),
|
|
262
|
+
];
|
|
263
|
+
return [
|
|
264
|
+
...usableThenLimited(accounts.filter(isClaudeAccount)),
|
|
265
|
+
...usableThenLimited(accounts.filter(isOpenAIAccount)),
|
|
266
|
+
...usableThenLimited(accounts.filter(isXaiAccount)),
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
/** Compact view hides healthy/inactive model rows; selection shows the full set. */
|
|
270
|
+
export function visibleCapacityRows(rows, selected) {
|
|
271
|
+
if (selected)
|
|
272
|
+
return rows;
|
|
273
|
+
return rows.filter(row => row.color === "red" || row.color === "yellow");
|
|
274
|
+
}
|
|
275
|
+
export function noteModelLimit(account) {
|
|
276
|
+
const limits = account.rateLimits?.usage?.modelLimits ?? [];
|
|
277
|
+
if (limits.length === 0)
|
|
278
|
+
return undefined;
|
|
279
|
+
const fable = limits.find(limit => limit.modelFamily === "fable" || /fable/i.test(limit.displayName));
|
|
280
|
+
const limit = fable ?? limits.find(entry => entry.active) ?? limits[0];
|
|
281
|
+
if (!limit)
|
|
282
|
+
return undefined;
|
|
283
|
+
const utilization = limit.utilization;
|
|
284
|
+
const color = utilization >= 1 || limit.severity === "critical"
|
|
285
|
+
? "red"
|
|
286
|
+
: utilization >= 0.7 || limit.severity === "warning"
|
|
287
|
+
? "yellow"
|
|
288
|
+
: "green";
|
|
289
|
+
return {
|
|
290
|
+
label: limit.displayName.replace(/^Claude\s+/i, ""),
|
|
291
|
+
utilization,
|
|
292
|
+
color,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Grok has no usage windows, so the note carries the plan instead. Prefer the
|
|
297
|
+
* live plan name from `/v1/user` ("GrokPro"); fall back to the coarse
|
|
298
|
+
* access-token `tier` when the live lookup has not landed (offline / first
|
|
299
|
+
* render), and to "cli" when neither is known.
|
|
300
|
+
*/
|
|
301
|
+
export function grokQuotaNote(account) {
|
|
302
|
+
if (!isXaiAccount(account))
|
|
303
|
+
return undefined;
|
|
304
|
+
if (account.healthy === false)
|
|
305
|
+
return "expired";
|
|
306
|
+
const plan = account.xai?.subscriptionTier?.trim();
|
|
307
|
+
if (plan)
|
|
308
|
+
return plan;
|
|
309
|
+
const tier = account.xai?.tier;
|
|
310
|
+
return typeof tier === "number" && Number.isFinite(tier) ? `tier ${tier}` : "cli";
|
|
311
|
+
}
|
|
312
|
+
export function openaiQuotaGapNote(account) {
|
|
313
|
+
if (!isOpenAIAccount(account))
|
|
314
|
+
return undefined;
|
|
315
|
+
if (getCodexDefaultWindows(account.codexRateLimits).length > 0)
|
|
316
|
+
return undefined;
|
|
317
|
+
const plan = account.codexRateLimits?.plan?.trim();
|
|
318
|
+
return plan ? `${plan} · no quota` : "no quota";
|
|
319
|
+
}
|
|
320
|
+
export function isWeeklyLimited(account) {
|
|
321
|
+
if (isOpenAIAccount(account)) {
|
|
322
|
+
return getCodexDefaultWindows(account.codexRateLimits).some(window => window.kind === "weekly" && window.utilization >= 1);
|
|
323
|
+
}
|
|
324
|
+
if (!account.rateLimits)
|
|
325
|
+
return false;
|
|
326
|
+
return getGlobalCapacityView(account.rateLimits).sevenDay.utilization >= 1;
|
|
327
|
+
}
|
|
328
|
+
export function earliestWeeklyReset(accounts) {
|
|
329
|
+
let earliest;
|
|
330
|
+
for (const account of accounts) {
|
|
331
|
+
if (isOpenAIAccount(account)) {
|
|
332
|
+
for (const window of getCodexDefaultWindows(account.codexRateLimits)) {
|
|
333
|
+
if (window.kind !== "weekly" || window.resetAt <= 0)
|
|
334
|
+
continue;
|
|
335
|
+
if (earliest === undefined || window.resetAt < earliest)
|
|
336
|
+
earliest = window.resetAt;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else if (account.rateLimits) {
|
|
340
|
+
const resetAt = getGlobalCapacityView(account.rateLimits).sevenDay.resetAt;
|
|
341
|
+
if (resetAt > 0 && (earliest === undefined || resetAt < earliest))
|
|
342
|
+
earliest = resetAt;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return earliest;
|
|
346
|
+
}
|
|
226
347
|
/**
|
|
227
348
|
* First visible row of a scrolling list window that follows its selection.
|
|
228
349
|
*
|
|
@@ -417,6 +538,27 @@ export function Dashboard({ port, baseUrl, authToken, onIntent }) {
|
|
|
417
538
|
if (!data && (input === "q" || key.escape))
|
|
418
539
|
exit();
|
|
419
540
|
});
|
|
541
|
+
// Live Grok plan name / code-access, refreshed on a slow cadence and merged
|
|
542
|
+
// synchronously into each 2s health poll — so the plan shows without a
|
|
543
|
+
// /v1/user round-trip every tick. Undefined until the first fetch lands, at
|
|
544
|
+
// which point `mergeGrokIntoHealth` falls back to its access-token tier.
|
|
545
|
+
const grokSnapshotsRef = useRef(undefined);
|
|
546
|
+
useEffect(() => {
|
|
547
|
+
let cancelled = false;
|
|
548
|
+
const refresh = async () => {
|
|
549
|
+
try {
|
|
550
|
+
const snapshots = await loadGrokHealthSnapshotsWithSubscription();
|
|
551
|
+
if (!cancelled)
|
|
552
|
+
grokSnapshotsRef.current = snapshots;
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
// Keep the last known snapshots; the poll degrades to the tier fallback.
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
refresh();
|
|
559
|
+
const timer = setInterval(refresh, GROK_SUBSCRIPTION_INTERVAL_MS);
|
|
560
|
+
return () => { cancelled = true; clearInterval(timer); };
|
|
561
|
+
}, []);
|
|
420
562
|
useEffect(() => {
|
|
421
563
|
let cancelled = false;
|
|
422
564
|
const healthUrl = `${resolvedBase}/cc-router/health`;
|
|
@@ -432,7 +574,7 @@ export function Dashboard({ port, baseUrl, authToken, onIntent }) {
|
|
|
432
574
|
if (cancelled)
|
|
433
575
|
return;
|
|
434
576
|
if (res.ok) {
|
|
435
|
-
setData(await res.json());
|
|
577
|
+
setData(mergeGrokIntoHealth(await res.json(), grokSnapshotsRef.current));
|
|
436
578
|
setConnectError(null);
|
|
437
579
|
setLastUpdate(Date.now());
|
|
438
580
|
setRetryCount(0);
|
|
@@ -466,13 +608,36 @@ function ErrorScreen({ error, port, retries }) {
|
|
|
466
608
|
}
|
|
467
609
|
// ─── Live dashboard ───────────────────────────────────────────────────────────
|
|
468
610
|
function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onIntent, }) {
|
|
611
|
+
const [cliRouting, setCliRouting] = useState(() => ({
|
|
612
|
+
claude: readClaudeRouting(),
|
|
613
|
+
codex: readCodexRouting(),
|
|
614
|
+
}));
|
|
615
|
+
const refreshCliRouting = useCallback(() => {
|
|
616
|
+
setCliRouting({
|
|
617
|
+
claude: readClaudeRouting(),
|
|
618
|
+
codex: readCodexRouting(),
|
|
619
|
+
});
|
|
620
|
+
}, []);
|
|
621
|
+
useEffect(() => {
|
|
622
|
+
const timer = setInterval(refreshCliRouting, POLL_INTERVAL_MS);
|
|
623
|
+
return () => clearInterval(timer);
|
|
624
|
+
}, [refreshCliRouting]);
|
|
469
625
|
const { exit } = useApp();
|
|
470
|
-
const
|
|
471
|
-
const
|
|
626
|
+
const orderedAccounts = orderAccountsForDashboard(data.accounts);
|
|
627
|
+
const healthyCount = orderedAccounts.filter(a => a.healthy).length;
|
|
628
|
+
// Fleet-wide weekly-cap rollup surfaced in the always-visible ACCOUNTS title,
|
|
629
|
+
// so the "N 7d full" signal is legible at a glance even when a provider group
|
|
630
|
+
// is scrolled out of the account window (or TOTALS is hidden in compact view).
|
|
631
|
+
const weeklyFullCount = orderedAccounts.filter(isWeeklyLimited).length;
|
|
472
632
|
const logs = data.recentLogs;
|
|
473
633
|
// ── Focus / mode ──────────────────────────────────────────────────────────
|
|
474
634
|
const [focus, setFocus] = useState("logs");
|
|
475
635
|
const [mode, setMode] = useState("view");
|
|
636
|
+
// Compact ("zen") view: hides TOTALS + RECENT ACTIVITY so the account list
|
|
637
|
+
// gets the whole vertical budget — the fix for a short terminal starving a
|
|
638
|
+
// long fleet (e.g. showing 1 of 11 accounts). Toggled with [z], view-only,
|
|
639
|
+
// never touches the default layout's carefully-tuned fit.
|
|
640
|
+
const [compact, setCompact] = useState(false);
|
|
476
641
|
// Selected log by timestamp (existing)
|
|
477
642
|
const [selectedTs, setSelectedTs] = useState(null);
|
|
478
643
|
const selectedLogIndex = selectedTs !== null
|
|
@@ -497,7 +662,8 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
497
662
|
const [logVisible, setLogVisible] = useState(MIN_LOG_VISIBLE);
|
|
498
663
|
const [accountsVisible, setAccountsVisible] = useState(Number.MAX_SAFE_INTEGER);
|
|
499
664
|
const [modelsVisible, setModelsVisible] = useState(MODEL_VISIBLE_ROWS);
|
|
500
|
-
const
|
|
665
|
+
const [selectedAccountId, setSelectedAccountId] = useState(null);
|
|
666
|
+
const shownAccounts = Math.max(1, Math.min(accountsVisible, orderedAccounts.length || 1));
|
|
501
667
|
const contentRef = useRef(null);
|
|
502
668
|
const accountRowsRef = useRef(null);
|
|
503
669
|
const logRowsRef = useRef(null);
|
|
@@ -523,7 +689,7 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
523
689
|
// denial, re-enabling the grow/shrink oscillation. Geometry drift from
|
|
524
690
|
// scrolling (like every other content mutation the key cannot see) is
|
|
525
691
|
// covered by the denial TTL instead.
|
|
526
|
-
const fitKey = `${terminalRows}:${terminalColumns}:${
|
|
692
|
+
const fitKey = `${terminalRows}:${terminalColumns}:${orderedAccounts.length}:${modelsStatus?.models.length ?? 0}:${logs[0]?.ts ?? 0}:${compact}`;
|
|
527
693
|
if (fitKeyRef.current !== fitKey) {
|
|
528
694
|
fitKeyRef.current = fitKey;
|
|
529
695
|
fitMemoryRef.current = { attempts: {}, denials: {} };
|
|
@@ -550,12 +716,14 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
550
716
|
// mechanics — each list got its own oscillation bug while the paths were
|
|
551
717
|
// separate (tall accounts, wrapped activity details, wrapped model ids).
|
|
552
718
|
const lists = [
|
|
719
|
+
// In compact view the activity list is not rendered, so it must not
|
|
720
|
+
// compete for rows — dropping it here hands the whole budget to accounts.
|
|
721
|
+
...(compact ? [] : [{
|
|
722
|
+
key: "logs", current: logVisible, min: MIN_LOG_VISIBLE, max: LOG_VISIBLE,
|
|
723
|
+
avgRow: shownLogs > 0 ? Math.max(1, logsH / shownLogs) : 1,
|
|
724
|
+
}]),
|
|
553
725
|
{
|
|
554
|
-
key: "
|
|
555
|
-
avgRow: shownLogs > 0 ? Math.max(1, logsH / shownLogs) : 1,
|
|
556
|
-
},
|
|
557
|
-
{
|
|
558
|
-
key: "accounts", current: shownAccounts, min: 1, max: data.accounts.length, growOne: true,
|
|
726
|
+
key: "accounts", current: shownAccounts, min: 1, max: Math.max(1, orderedAccounts.length), growOne: true,
|
|
559
727
|
avgRow: shownAccounts > 0 ? Math.max(1, accountsH / shownAccounts) : 2,
|
|
560
728
|
},
|
|
561
729
|
...(modelsPanelOpen ? [{
|
|
@@ -577,18 +745,16 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
577
745
|
// selected row out of the stored window, and it must stay visible anyway.
|
|
578
746
|
const [logScrollTop, setLogScrollTop] = useState(0);
|
|
579
747
|
const logWindowTop = followScrollWindow(logScrollTop, selectedLogIndex, logs.length, logVisible);
|
|
580
|
-
// Selected account by id
|
|
581
|
-
const [selectedAccountId, setSelectedAccountId] = useState(null);
|
|
582
748
|
const selectedAccountIndex = selectedAccountId !== null
|
|
583
|
-
? Math.max(0,
|
|
749
|
+
? Math.max(0, orderedAccounts.findIndex(a => a.id === selectedAccountId))
|
|
584
750
|
: 0;
|
|
585
|
-
const selectedAccount =
|
|
751
|
+
const selectedAccount = orderedAccounts[selectedAccountIndex] ?? null;
|
|
586
752
|
// Same follow-scroll for the accounts window: when the fitting controller
|
|
587
753
|
// shrinks the list below the fleet size, the selected account must stay on
|
|
588
754
|
// screen — account actions (caps, toggle, delete confirmation) target the
|
|
589
755
|
// selection, and acting on an invisible account is how a wrong one dies.
|
|
590
756
|
const [accountScrollTop, setAccountScrollTop] = useState(0);
|
|
591
|
-
const accountWindowTop = followScrollWindow(accountScrollTop, selectedAccountIndex,
|
|
757
|
+
const accountWindowTop = followScrollWindow(accountScrollTop, selectedAccountIndex, orderedAccounts.length, shownAccounts);
|
|
592
758
|
const [modelsStatus, setModelsStatus] = useState(null);
|
|
593
759
|
const [selectedModelId, setSelectedModelId] = useState(null);
|
|
594
760
|
const modelRows = modelsStatus?.models ?? [];
|
|
@@ -635,6 +801,10 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
635
801
|
const doToggleEnabled = useCallback(async () => {
|
|
636
802
|
if (!selectedAccount)
|
|
637
803
|
return;
|
|
804
|
+
if (isXaiAccount(selectedAccount)) {
|
|
805
|
+
showBanner("Grok is read-only here — use grok login / grok logout", "yellow");
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
638
808
|
const newValue = !(selectedAccount.enabled !== false);
|
|
639
809
|
try {
|
|
640
810
|
await api.patch(selectedAccount.id, { enabled: newValue });
|
|
@@ -647,8 +817,12 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
647
817
|
const doToggleProvider = useCallback(async (provider) => {
|
|
648
818
|
const providerStatus = provider === "anthropic_subscription"
|
|
649
819
|
? data.operational?.providers.anthropic
|
|
650
|
-
:
|
|
651
|
-
|
|
820
|
+
: provider === "openai_subscription"
|
|
821
|
+
? data.operational?.providers.openai
|
|
822
|
+
: data.operational?.providers.xai;
|
|
823
|
+
const label = provider === "anthropic_subscription" ? "Claude"
|
|
824
|
+
: provider === "openai_subscription" ? "OpenAI"
|
|
825
|
+
: "Grok";
|
|
652
826
|
if (!providerStatus?.configured || providerStatus.accounts === 0) {
|
|
653
827
|
showBanner(`${label} accounts are not configured`, "yellow");
|
|
654
828
|
return;
|
|
@@ -698,6 +872,19 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
698
872
|
showBanner(`Models error: ${errMsg(err)}`, "red");
|
|
699
873
|
}
|
|
700
874
|
}, [modelsApi, showBanner]);
|
|
875
|
+
const doToggleCli = useCallback((target) => {
|
|
876
|
+
const current = target === "claude" ? cliRouting.claude.enabled : cliRouting.codex.enabled;
|
|
877
|
+
const result = target === "claude"
|
|
878
|
+
? setClaudeRouting(!current)
|
|
879
|
+
: setCodexRouting(!current);
|
|
880
|
+
refreshCliRouting();
|
|
881
|
+
const label = target === "claude" ? "Claude CLI" : "Codex CLI";
|
|
882
|
+
if (!result.changed) {
|
|
883
|
+
showBanner(`${label} already ${result.enabled ? "on" : "off"}`, "gray");
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
showBanner(`${label} → ${result.enabled ? "proxy" : "native"} (restart the CLI to pick up)`, result.enabled ? "green" : "yellow");
|
|
887
|
+
}, [cliRouting, refreshCliRouting, showBanner]);
|
|
701
888
|
const doSetSelectedModel = useCallback(async (provider) => {
|
|
702
889
|
if (!selectedModel)
|
|
703
890
|
return;
|
|
@@ -784,6 +971,11 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
784
971
|
return;
|
|
785
972
|
}
|
|
786
973
|
if (key.tab) {
|
|
974
|
+
// Compact view hides the activity list, so skip "logs" in the cycle.
|
|
975
|
+
if (compact) {
|
|
976
|
+
setFocus(f => f === "models" ? "accounts" : "models");
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
787
979
|
setFocus(f => f === "logs" ? "accounts" : f === "accounts" ? "models" : "logs");
|
|
788
980
|
return;
|
|
789
981
|
}
|
|
@@ -803,13 +995,13 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
803
995
|
if (focus === "accounts") {
|
|
804
996
|
if (key.upArrow) {
|
|
805
997
|
const next = Math.max(0, selectedAccountIndex - 1);
|
|
806
|
-
setSelectedAccountId(
|
|
807
|
-
setAccountScrollTop(followScrollWindow(accountWindowTop, next,
|
|
998
|
+
setSelectedAccountId(orderedAccounts[next]?.id ?? null);
|
|
999
|
+
setAccountScrollTop(followScrollWindow(accountWindowTop, next, orderedAccounts.length, shownAccounts));
|
|
808
1000
|
}
|
|
809
1001
|
if (key.downArrow) {
|
|
810
|
-
const next = Math.min(
|
|
811
|
-
setSelectedAccountId(
|
|
812
|
-
setAccountScrollTop(followScrollWindow(accountWindowTop, next,
|
|
1002
|
+
const next = Math.min(orderedAccounts.length - 1, selectedAccountIndex + 1);
|
|
1003
|
+
setSelectedAccountId(orderedAccounts[next]?.id ?? null);
|
|
1004
|
+
setAccountScrollTop(followScrollWindow(accountWindowTop, next, orderedAccounts.length, shownAccounts));
|
|
813
1005
|
}
|
|
814
1006
|
// Account actions (only when focus = accounts)
|
|
815
1007
|
if (input === "e") {
|
|
@@ -824,12 +1016,24 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
824
1016
|
void doToggleProvider("openai_subscription");
|
|
825
1017
|
return;
|
|
826
1018
|
}
|
|
1019
|
+
if (input === "g") {
|
|
1020
|
+
void doToggleProvider("xai_subscription");
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
827
1023
|
if (input === "w") {
|
|
1024
|
+
if (selectedAccount && isXaiAccount(selectedAccount)) {
|
|
1025
|
+
showBanner("Grok has no 7d cap in cc-router", "yellow");
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
828
1028
|
setMode("editWeekly");
|
|
829
1029
|
setEditBuffer("");
|
|
830
1030
|
return;
|
|
831
1031
|
}
|
|
832
1032
|
if (input === "s") {
|
|
1033
|
+
if (selectedAccount && isXaiAccount(selectedAccount)) {
|
|
1034
|
+
showBanner("Grok has no 5h cap in cc-router", "yellow");
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
833
1037
|
setMode("editSession");
|
|
834
1038
|
setEditBuffer("");
|
|
835
1039
|
return;
|
|
@@ -840,6 +1044,10 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
840
1044
|
// the CLI for something the dashboard can do was left over from before
|
|
841
1045
|
// that existed.
|
|
842
1046
|
if (input === "d") {
|
|
1047
|
+
if (selectedAccount && isXaiAccount(selectedAccount)) {
|
|
1048
|
+
showBanner("Grok accounts live in ~/.grok — run grok logout", "yellow");
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
843
1051
|
setMode("confirmDelete");
|
|
844
1052
|
return;
|
|
845
1053
|
}
|
|
@@ -866,10 +1074,30 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
866
1074
|
return;
|
|
867
1075
|
}
|
|
868
1076
|
}
|
|
1077
|
+
// CLI routing toggles — [c] is model-default only while MODELS is focused.
|
|
1078
|
+
if (input === "c" && focus !== "models") {
|
|
1079
|
+
doToggleCli("claude");
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (input === "x") {
|
|
1083
|
+
doToggleCli("codex");
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
869
1086
|
if (input === "m") {
|
|
870
1087
|
void doLoadModels();
|
|
871
1088
|
return;
|
|
872
1089
|
}
|
|
1090
|
+
// z = compact view — hide TOTALS + RECENT ACTIVITY so the account list
|
|
1091
|
+
// gets the freed rows. On small terminals this is the difference between
|
|
1092
|
+
// "showing 1–1" and the full fleet. Move focus off the (now hidden) logs.
|
|
1093
|
+
if (input === "z") {
|
|
1094
|
+
const next = !compact;
|
|
1095
|
+
setCompact(next);
|
|
1096
|
+
if (next && focus === "logs")
|
|
1097
|
+
setFocus("accounts");
|
|
1098
|
+
showBanner(next ? "Compact on — activity & totals hidden" : "Compact off", "cyan");
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
873
1101
|
// n = add account — works regardless of focus.
|
|
874
1102
|
// Requires an onIntent handler because the outer loop runs the OAuth
|
|
875
1103
|
// flow after Ink unmounts; if none is wired, this key is a no-op.
|
|
@@ -883,23 +1111,26 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
883
1111
|
});
|
|
884
1112
|
const selectedLog = logs[selectedLogIndex] ?? null;
|
|
885
1113
|
const visibleLogs = logs.slice(logWindowTop, logWindowTop + logVisible);
|
|
886
|
-
return (_jsx(Box, { flexDirection: "column", height: frameBound, overflowY: "hidden", children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, ref: contentRef, children: [_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 }),
|
|
1114
|
+
return (_jsx(Box, { flexDirection: "column", height: frameBound, overflowY: "hidden", children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, ref: contentRef, children: [_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 }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsxs(Text, { children: ["up ", formatUptime(data.uptime)] }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { color: "cyan", children: data.totalRequests }), _jsx(Text, { color: "gray", children: " req " }), _jsx(Text, { color: data.totalErrors > 0 ? "red" : "green", children: data.totalErrors }), _jsx(Text, { color: "gray", children: " err" }), _jsx(CacheHealthBadge, { read: data.totalCacheReadTokens, created: data.totalCacheCreationTokens, input: data.totalInputTokens }), _jsx(Text, { color: "gray", children: " \u00B7 [q] quit" })] }), mode === "editWeekly" && selectedAccount && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: "Set 7d cap for " }), _jsx(Text, { color: "white", bold: true, children: selectedAccount.id }), _jsx(Text, { color: "cyan", children: " (0\u2013100%): " }), _jsx(Text, { color: "white", bold: true, children: editBuffer }), _jsx(Text, { color: "gray", children: "\u2588 [Enter] save [Esc] cancel" })] })), mode === "editSession" && selectedAccount && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: "Set 5h cap for " }), _jsx(Text, { color: "white", bold: true, children: selectedAccount.id }), _jsx(Text, { color: "cyan", children: " (0\u2013100%): " }), _jsx(Text, { color: "white", bold: true, children: editBuffer }), _jsx(Text, { color: "gray", children: "\u2588 [Enter] save [Esc] cancel" })] })), mode === "confirmDelete" && selectedAccount && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: "red", bold: true, children: ["Delete \"", selectedAccount.id, "\"? [y] yes [n/Esc] cancel"] }) })), data.version !== DASHBOARD_VERSION && (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "yellow", children: " \u26A0 VERSION MISMATCH " }), _jsxs(Text, { color: "yellow", children: [data.version !== undefined
|
|
887
1115
|
? `daemon v${data.version}`
|
|
888
1116
|
: "daemon version unreported (older build)", ` · dashboard v${DASHBOARD_VERSION}`] }), LOCAL_TARGET_RE.test(baseUrl) ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2014 restart: " }), _jsx(Text, { color: "cyan", children: "cc-router stop --keep-config && cc-router start" })] })) : (
|
|
889
1117
|
// A remote router can only be restarted where it runs; printing a
|
|
890
1118
|
// local restart command here would never clear the banner.
|
|
891
|
-
_jsxs(Text, { color: "gray", children: [" \u2014 update and restart the daemon on ", baseUrl] }))] })), _jsx(Box, { marginTop: 1 }), data.operational && (_jsxs(_Fragment, { children: [_jsx(OperationsPanel, { operational: data.operational, baseUrl: baseUrl, focus: focus }), _jsx(Box, { marginTop: 1 })] })), (focus === "models" || modelsStatus) && (_jsxs(_Fragment, { children: [_jsx(ModelsPanel, { status: modelsStatus, selectedIndex: selectedModelIndex, focused: focus === "models", visibleRows: modelsVisible, rowsRef: modelRowsRef }), _jsx(Box, { marginTop: 1 })] })), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, children: [" ACCOUNTS ", _jsxs(Text, { color: healthyCount ===
|
|
1119
|
+
_jsxs(Text, { color: "gray", children: [" \u2014 update and restart the daemon on ", baseUrl] }))] })), _jsx(Box, { marginTop: 1 }), data.operational && (_jsxs(_Fragment, { children: [_jsx(OperationsPanel, { operational: data.operational, baseUrl: baseUrl, focus: focus, cliRouting: cliRouting }), _jsx(Box, { marginTop: 1 })] })), (focus === "models" || modelsStatus) && (_jsxs(_Fragment, { children: [_jsx(ModelsPanel, { status: modelsStatus, selectedIndex: selectedModelIndex, focused: focus === "models", visibleRows: modelsVisible, rowsRef: modelRowsRef }), _jsx(Box, { marginTop: 1 })] })), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, children: [" ACCOUNTS ", _jsxs(Text, { color: healthyCount === orderedAccounts.length ? "green" : "yellow", children: [healthyCount, "/", orderedAccounts.length, " healthy"] }), weeklyFullCount > 0 && _jsx(Text, { color: "red", children: ` · ${weeklyFullCount} 7d full` })] }), shownAccounts < orderedAccounts.length && (_jsxs(Text, { color: "gray", children: [" · showing ", accountWindowTop + 1, "\u2013", accountWindowTop + shownAccounts] })), compact && _jsx(Text, { color: "cyan", children: " · compact" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", ref: accountRowsRef, children: _jsx(AccountGroups, { visible: orderedAccounts.slice(accountWindowTop, accountWindowTop + shownAccounts), fleet: orderedAccounts, windowTop: accountWindowTop, selectedIndex: selectedAccountIndex, focused: focus === "accounts" }) })] }), banner && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsxs(Text, { color: banner.color, children: [" ", banner.text] }) })), !compact && (_jsxs(_Fragment, { children: [_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 }), _jsx(Text, { bold: true, children: " RECENT ACTIVITY" }), _jsx(Box, { marginTop: 1 })] }))] }), !compact && (_jsx(Box, { flexDirection: "column", ref: logRowsRef, children: visibleLogs.length === 0
|
|
892
1120
|
? _jsx(Text, { color: "gray", children: " No activity yet" })
|
|
893
|
-
: visibleLogs.map((log, i) => (_jsx(LogRow, { log: log, selected: focus === "logs" && logWindowTop + i === selectedLogIndex }, `${log.ts}-${i}`))) }), focus === "logs" && selectedLog && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginTop: 1 }), _jsx(DetailPanel, { log: selectedLog })] }))
|
|
1121
|
+
: visibleLogs.map((log, i) => (_jsx(LogRow, { log: log, selected: focus === "logs" && logWindowTop + i === selectedLogIndex }, `${log.ts}-${i}`))) })), !compact && focus === "logs" && selectedLog && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginTop: 1 }), _jsx(DetailPanel, { log: selectedLog })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: focus === "accounts"
|
|
1122
|
+
? " [Tab] [e] toggle [a]/[o]/[g] provider [n] add [d] delete [w] 7d [s] 5h [z] compact [q]"
|
|
1123
|
+
: focus === "models"
|
|
1124
|
+
? " [Tab] [m/r] refresh [c]/[o] default [Esc] logs [z] compact [q]"
|
|
1125
|
+
: " [Tab] [m] models [z] compact [q] quit" }) })] }) }));
|
|
894
1126
|
}
|
|
895
|
-
function OperationsPanel({ operational, baseUrl, focus }) {
|
|
1127
|
+
function OperationsPanel({ operational, baseUrl, focus, cliRouting }) {
|
|
896
1128
|
const authLabel = operational.auth.required ? "protected" : "open";
|
|
897
1129
|
const authColor = operational.auth.required ? "green" : "yellow";
|
|
898
1130
|
const claudeReady = operational.capabilities.anthropicMessages;
|
|
899
1131
|
const openAIReady = operational.capabilities.openAIResponses;
|
|
900
1132
|
const crossReady = operational.capabilities.crossProviderMessages;
|
|
901
|
-
|
|
902
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " OPERATIONS " }), _jsx(Text, { color: "gray", children: "base " }), _jsx(Text, { color: "cyan", children: baseUrl }), _jsx(Text, { color: "gray", children: " \u00B7 auth " }), _jsx(Text, { color: authColor, children: authLabel }), _jsx(Text, { color: "gray", children: " \u00B7 models " }), _jsx(Text, { color: modelsReady ? "green" : "red", children: modelsReady ? "dynamic" : "off" })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(ProviderBadge, { label: "Claude", status: operational.providers.anthropic, ready: claudeReady }), _jsx(Text, { color: "gray", children: " " }), _jsx(ProviderBadge, { label: "OpenAI", status: operational.providers.openai, ready: openAIReady }), _jsx(Text, { color: "gray", children: " \u00B7 cross-route " }), _jsx(Text, { color: crossReady ? "green" : "gray", children: crossReady ? "ready" : "needs OpenAI" })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "endpoints " }), _jsx(Text, { color: "white", children: operational.endpoints.messages }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "white", children: operational.endpoints.responses }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "white", children: operational.endpoints.models }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "white", children: operational.endpoints.accounts })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "routing " }), _jsxs(Text, { color: "white", children: ["claude=", operational.routing.anthropicDefaultModel ?? "default"] }), _jsxs(Text, { color: "gray", children: [" aliases[", operational.routing.anthropicAliases.join(",") || "-", "]"] }), _jsx(Text, { color: "gray", children: " " }), _jsxs(Text, { color: "white", children: ["openai=", operational.routing.openAIDefaultModel ?? "default"] }), _jsxs(Text, { color: "gray", children: [" aliases[", operational.routing.openAIAliases.join(",") || "-", "]"] })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "gray", children: "models " }), _jsx(Text, { color: focus === "models" ? "white" : "cyan", children: "[m] list/select" }), _jsx(Text, { color: "gray", children: " change " }), _jsx(Text, { color: focus === "models" ? "white" : "cyan", children: "[c] Claude [o] OpenAI" })] })] }));
|
|
1133
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: " OPERATIONS " }), _jsx(Text, { color: "cyan", children: baseUrl.replace(/^https?:\/\//, "") }), _jsx(Text, { color: "gray", children: " \u00B7 auth " }), _jsx(Text, { color: authColor, children: authLabel }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(ProviderBadge, { label: "Claude", status: operational.providers.anthropic, ready: claudeReady }), _jsx(Text, { color: "gray", children: " " }), _jsx(ProviderBadge, { label: "ChatGPT", status: operational.providers.openai, ready: openAIReady }), _jsx(Text, { color: "gray", children: " " }), _jsx(ProviderBadge, { label: "Grok", status: operational.providers.xai ?? { configured: false, accounts: 0, healthy: 0, enabled: 0 }, ready: (operational.providers.xai?.healthy ?? 0) > 0 }), crossReady && _jsx(Text, { color: "gray", children: " \u00B7 cross-route" }), _jsx(Text, { color: "gray", children: " \u00B7 cli " }), _jsx(Text, { color: cliRouting.claude.enabled ? "green" : "gray", children: "C" }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: cliRouting.codex.enabled ? "green" : "gray", children: "X" }), _jsx(Text, { color: focus === "models" ? "gray" : "cyan", children: " [c]/[x]" }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { color: focus === "models" ? "white" : "cyan", children: "[m]" })] }));
|
|
903
1134
|
}
|
|
904
1135
|
function ModelsPanel({ status, selectedIndex, focused, visibleRows = MODEL_VISIBLE_ROWS, rowsRef, }) {
|
|
905
1136
|
const models = status?.models ?? [];
|
|
@@ -935,66 +1166,134 @@ function ModelRow({ model, selected, currentClaude, currentOpenAI, }) {
|
|
|
935
1166
|
function ProviderBadge({ label, status, ready, }) {
|
|
936
1167
|
const color = !status.configured ? "gray" : ready ? "green" : "yellow";
|
|
937
1168
|
const text = status.configured
|
|
938
|
-
? `${label} ${status.healthy}/${status.accounts}
|
|
939
|
-
: `${label}
|
|
1169
|
+
? `${label} ${status.healthy}/${status.accounts}`
|
|
1170
|
+
: `${label} off`;
|
|
940
1171
|
return _jsx(Text, { color: color, children: text });
|
|
941
1172
|
}
|
|
942
|
-
|
|
1173
|
+
function AccountGroups({ visible, fleet, windowTop, selectedIndex, focused, }) {
|
|
1174
|
+
const claudeVisible = visible.filter(isClaudeAccount);
|
|
1175
|
+
const chatgptVisible = visible.filter(isOpenAIAccount);
|
|
1176
|
+
const grokVisible = visible.filter(isXaiAccount);
|
|
1177
|
+
const claudeFleet = fleet.filter(isClaudeAccount);
|
|
1178
|
+
const chatgptFleet = fleet.filter(isOpenAIAccount);
|
|
1179
|
+
const grokFleet = fleet.filter(isXaiAccount);
|
|
1180
|
+
if (visible.length === 0) {
|
|
1181
|
+
return _jsx(Text, { color: "gray", children: " No accounts" });
|
|
1182
|
+
}
|
|
1183
|
+
const renderRows = (accounts, offsetInVisible) => accounts.map((account, i) => (_jsx(AccountRow, { account: account, selected: focused && windowTop + offsetInVisible + i === selectedIndex }, account.id)));
|
|
1184
|
+
// Render a group whenever its provider has ANY account — even when the scroll
|
|
1185
|
+
// window currently shows none of its rows. A fully-clipped group still emits
|
|
1186
|
+
// its header + count so a tail provider (e.g. a single Grok account behind
|
|
1187
|
+
// many Claude/ChatGPT ones on a short terminal) never vanishes without a
|
|
1188
|
+
// trace. `offset` advances by VISIBLE rows only, so selection indexing is
|
|
1189
|
+
// unaffected by header-only groups.
|
|
1190
|
+
const groups = [];
|
|
1191
|
+
let offset = 0;
|
|
1192
|
+
if (claudeFleet.length > 0) {
|
|
1193
|
+
groups.push({ key: "CLAUDE", visible: claudeVisible, fleet: claudeFleet, offset });
|
|
1194
|
+
offset += claudeVisible.length;
|
|
1195
|
+
}
|
|
1196
|
+
if (chatgptFleet.length > 0) {
|
|
1197
|
+
groups.push({ key: "CHATGPT", visible: chatgptVisible, fleet: chatgptFleet, offset });
|
|
1198
|
+
offset += chatgptVisible.length;
|
|
1199
|
+
}
|
|
1200
|
+
if (grokFleet.length > 0) {
|
|
1201
|
+
groups.push({ key: "GROK", visible: grokVisible, fleet: grokFleet, offset });
|
|
1202
|
+
}
|
|
1203
|
+
return (_jsx(Box, { flexDirection: "column", children: groups.map((group, index) => (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsx(GroupHeader, { label: group.key, accounts: group.fleet }), group.visible.length > 0 && _jsx(ColumnLegend, {}), renderRows(group.visible, group.offset)] }, group.key))) }));
|
|
1204
|
+
}
|
|
1205
|
+
function GroupHeader({ label, accounts }) {
|
|
1206
|
+
const healthy = accounts.filter(account => account.healthy).length;
|
|
1207
|
+
const weeklyFull = accounts.filter(isWeeklyLimited);
|
|
1208
|
+
const color = healthy === accounts.length && weeklyFull.length === 0 ? "green" : "yellow";
|
|
1209
|
+
const fableHint = label === "CLAUDE" ? exhaustedModelHint(accounts) : undefined;
|
|
1210
|
+
// Grok has no usage windows, so surface its plan ("GrokPro") in the header —
|
|
1211
|
+
// this is the one signal a clipped-out Grok row would otherwise hide.
|
|
1212
|
+
const grokPlan = label === "GROK" && accounts[0] ? grokQuotaNote(accounts[0]) : undefined;
|
|
1213
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: "gray", children: [" ", label, " "] }), _jsxs(Text, { color: color, children: [healthy, "/", accounts.length, " ok"] }), weeklyFull.length > 0 && _jsx(Text, { color: "red", children: ` · ${weeklyFull.length} 7d full` }), fableHint && _jsx(Text, { color: "red", children: ` · ${fableHint}` }), grokPlan && _jsx(Text, { color: "gray", children: ` · ${grokPlan}` })] }));
|
|
1214
|
+
}
|
|
1215
|
+
const COL = {
|
|
1216
|
+
name: 22,
|
|
1217
|
+
req: 5,
|
|
1218
|
+
sess: 6,
|
|
1219
|
+
pct: 4,
|
|
1220
|
+
note: 22,
|
|
1221
|
+
reset: 8,
|
|
1222
|
+
rst: 5,
|
|
1223
|
+
};
|
|
1224
|
+
function ColumnLegend() {
|
|
1225
|
+
return (_jsx(Text, { color: "gray", children: ` ${"".padEnd(COL.name)} ${"req".padStart(COL.req)}${"s·n".padStart(COL.sess)} ${"5h".padStart(COL.pct)} ${"7d".padStart(COL.pct)} ${"note".padEnd(COL.note)} ${"↻5h".padEnd(COL.reset)} ${"↻7d".padEnd(COL.reset)} ${"rst".padStart(COL.rst)}` }));
|
|
1226
|
+
}
|
|
1227
|
+
function exhaustedModelHint(accounts) {
|
|
1228
|
+
const hits = [];
|
|
1229
|
+
for (const account of accounts) {
|
|
1230
|
+
for (const row of getAccountCapacityRows(account)) {
|
|
1231
|
+
if ((row.utilization ?? 0) < 1 || row.color !== "red")
|
|
1232
|
+
continue;
|
|
1233
|
+
hits.push(`${row.label} full`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return hits[0];
|
|
1237
|
+
}
|
|
1238
|
+
// ─── Account row (one aligned line, no bars) ─────────────────────────────────
|
|
943
1239
|
function AccountRow({ account: a, selected }) {
|
|
944
1240
|
const rl = a.rateLimits ?? EMPTY_RL;
|
|
945
1241
|
const usage = rl.usage;
|
|
946
1242
|
const globalCapacity = getGlobalCapacityView(rl);
|
|
947
|
-
const isOpenAI = a
|
|
948
|
-
const
|
|
949
|
-
const
|
|
950
|
-
const capacityRows = isOpenAI
|
|
951
|
-
? getCodexCapacityRows(a.codexRateLimits, a.globalCooldownUntilMs)
|
|
952
|
-
: getAccountCapacityRows(a);
|
|
953
|
-
const isLimited = isOpenAI ? isCodexLimited(a.codexRateLimits) : rl.status === "rate_limited";
|
|
1243
|
+
const isOpenAI = isOpenAIAccount(a);
|
|
1244
|
+
const isXai = isXaiAccount(a);
|
|
1245
|
+
const isLimited = isLimitedAccount(a);
|
|
954
1246
|
const isDisabled = a.enabled === false;
|
|
1247
|
+
const modelNote = isOpenAI || isXai ? undefined : noteModelLimit(a);
|
|
1248
|
+
const extraCap = usage?.extraUsage;
|
|
1249
|
+
const extraOff = extraCap !== undefined && extraCap.usable !== true
|
|
1250
|
+
&& (extraCap.spendLimitReached
|
|
1251
|
+
|| (modelNote !== undefined && modelNote.utilization >= 1 && extraCap.enabled === false));
|
|
1252
|
+
const gapNote = openaiQuotaGapNote(a);
|
|
1253
|
+
const grokNote = grokQuotaNote(a);
|
|
1254
|
+
const note = [
|
|
1255
|
+
modelNote
|
|
1256
|
+
? `${modelNote.label} ${Math.round(modelNote.utilization * 100)}%`
|
|
1257
|
+
: "",
|
|
1258
|
+
extraOff ? "extra off" : "",
|
|
1259
|
+
gapNote ?? "",
|
|
1260
|
+
grokNote ?? "",
|
|
1261
|
+
].filter(Boolean).join(" · ");
|
|
1262
|
+
const codexWindows = getCodexDefaultWindows(a.codexRateLimits);
|
|
1263
|
+
const sessionWindow = codexWindows.find(window => window.kind === "session");
|
|
1264
|
+
const weeklyWindow = codexWindows.find(window => window.kind === "weekly");
|
|
1265
|
+
const hasClaudeQuota = !isOpenAI && !isXai && (rl.lastUpdated > 0 || usage);
|
|
1266
|
+
const fiveHour = isOpenAI
|
|
1267
|
+
? sessionWindow
|
|
1268
|
+
: hasClaudeQuota ? globalCapacity.fiveHour : undefined;
|
|
1269
|
+
const sevenDay = isOpenAI
|
|
1270
|
+
? weeklyWindow
|
|
1271
|
+
: hasClaudeQuota ? globalCapacity.sevenDay : undefined;
|
|
1272
|
+
const creditsLabel = resetCreditsColumnLabel(a);
|
|
955
1273
|
const dot = isDisabled ? "⊘" : isLimited ? "⊘" : a.busy ? "◌" : a.healthy ? "●" : "●";
|
|
956
1274
|
const dotColor = isDisabled ? "gray" : isLimited ? "red" : a.busy ? "yellow" : a.healthy ? "green" : "red";
|
|
957
|
-
const
|
|
958
|
-
const
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
:
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
: rl.plan ? ` [${rl.plan}]` : "";
|
|
966
|
-
// User-defined caps hint
|
|
967
|
-
const s5 = a.sessionLimitPercent ?? 100;
|
|
968
|
-
const w7 = a.weeklyLimitPercent ?? 100;
|
|
969
|
-
const hasCaps = s5 < 100 || w7 < 100;
|
|
970
|
-
const capsHint = hasCaps
|
|
971
|
-
? ` cap${s5 < 100 ? ` 5h≤${s5}%` : ""}${w7 < 100 ? ` 7d≤${w7}%` : ""}`
|
|
972
|
-
: "";
|
|
973
|
-
const pointer = selected ? "▶" : " ";
|
|
974
|
-
const nameColor = isDisabled ? "gray" : undefined;
|
|
975
|
-
const creditsLabel = codex?.credits
|
|
976
|
-
? codex.credits.unlimited
|
|
977
|
-
? "∞"
|
|
978
|
-
: codex.credits.balance !== undefined
|
|
979
|
-
? codex.credits.balance
|
|
980
|
-
: codex.credits.hasCredits ? "yes" : "no"
|
|
981
|
-
: undefined;
|
|
982
|
-
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(21) }), _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 && (
|
|
983
|
-
// The account still works — its rotated token is live in memory — but a
|
|
984
|
-
// restart before the pending write lands would need a re-login.
|
|
985
|
-
_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 && codexDefaultWindows.length > 0 && (_jsxs(Box, { paddingLeft: 4, children: [codexDefaultWindows.map((window, index) => (_jsxs(React.Fragment, { children: [index > 0 && _jsx(Text, { children: " " }), _jsx(UtilBar, { label: window.label, util: window.utilization, resetTs: window.resetAt, isActive: false, cap: window.kind === "weekly" ? w7 : s5 })] }, window.label))), 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}`)))] }));
|
|
1275
|
+
const sessions = a.activeSessions ?? 0;
|
|
1276
|
+
const inflight = a.inFlightRequests ?? 0;
|
|
1277
|
+
const load = sessions <= 0 && inflight <= 0
|
|
1278
|
+
? ""
|
|
1279
|
+
: inflight > 0
|
|
1280
|
+
? `${sessions}·${inflight}`
|
|
1281
|
+
: String(sessions);
|
|
1282
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: selected ? "▶" : " " }), _jsx(Text, { color: dotColor, children: dot }), _jsx(Text, { color: selected ? "white" : isDisabled ? "gray" : undefined, dimColor: isDisabled, children: ` ${a.id.slice(0, COL.name).padEnd(COL.name)}` }), _jsx(Text, { color: "white", children: String(a.requestCount).padStart(COL.req) }), _jsx(Text, { color: "gray", children: load.padStart(COL.sess) }), _jsx(Text, { children: " " }), _jsx(QuotaCell, { util: fiveHour?.utilization }), _jsx(Text, { children: " " }), _jsx(QuotaCell, { util: sevenDay?.utilization }), _jsx(Text, { color: extraOff || gapNote ? "yellow" : modelNote?.color ?? "gray", children: ` ${note.padEnd(COL.note)}` }), _jsx(Text, { children: " " }), _jsx(ResetCell, { resetTs: fiveHour?.resetAt }), _jsx(Text, { children: " " }), _jsx(ResetCell, { resetTs: sevenDay?.resetAt }), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: creditsLabel.padStart(COL.rst) }), a.credentialsPendingWrite && _jsx(Text, { color: "yellow", children: " unsaved" })] }));
|
|
986
1283
|
}
|
|
987
|
-
|
|
988
|
-
|
|
1284
|
+
function QuotaCell({ util }) {
|
|
1285
|
+
if (util === undefined) {
|
|
1286
|
+
return _jsx(Text, { color: "gray", children: "—".padStart(COL.pct) });
|
|
1287
|
+
}
|
|
989
1288
|
const pct = Math.round(util * 100);
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
return (
|
|
1289
|
+
const color = pct >= 90 ? "red" : pct >= 70 ? "yellow" : "green";
|
|
1290
|
+
return _jsx(Text, { color: color, children: `${pct}%`.padStart(COL.pct) });
|
|
1291
|
+
}
|
|
1292
|
+
function ResetCell({ resetTs }) {
|
|
1293
|
+
if (!resetTs || resetTs <= 0) {
|
|
1294
|
+
return _jsx(Text, { color: "gray", children: "—".padEnd(COL.reset) });
|
|
1295
|
+
}
|
|
1296
|
+
return _jsx(Text, { color: "gray", children: `↻${formatResetIn(resetTs)}`.padEnd(COL.reset) });
|
|
998
1297
|
}
|
|
999
1298
|
function formatResetIn(unixSeconds) {
|
|
1000
1299
|
const diff = unixSeconds - Date.now() / 1000;
|
|
@@ -1047,28 +1346,21 @@ function LogRow({ log, selected }) {
|
|
|
1047
1346
|
}
|
|
1048
1347
|
// ─── Detail panel ─────────────────────────────────────────────────────────────
|
|
1049
1348
|
function DetailPanel({ log }) {
|
|
1050
|
-
const time = new Date(log.ts).
|
|
1051
|
-
hour12: false,
|
|
1052
|
-
year: "numeric", month: "2-digit", day: "2-digit",
|
|
1053
|
-
hour: "2-digit", minute: "2-digit", second: "2-digit",
|
|
1054
|
-
});
|
|
1349
|
+
const time = new Date(log.ts).toLocaleTimeString("en-GB", { hour12: false });
|
|
1055
1350
|
const isError = log.type === "error";
|
|
1056
1351
|
const isWarn = log.type === "warn";
|
|
1057
|
-
const statusLabel = log.statusCode === undefined ? "
|
|
1352
|
+
const statusLabel = log.statusCode === undefined ? ""
|
|
1058
1353
|
: log.statusCode === 0 ? "connection error"
|
|
1059
|
-
:
|
|
1354
|
+
: String(log.statusCode);
|
|
1060
1355
|
const statusColor = log.statusCode === undefined ? "gray"
|
|
1061
1356
|
: log.statusCode === 0 ? "red"
|
|
1062
1357
|
: log.statusCode >= 500 ? "red"
|
|
1063
1358
|
: log.statusCode >= 400 ? "yellow"
|
|
1064
1359
|
: "green";
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [
|
|
1069
|
-
}
|
|
1070
|
-
function FieldColored({ label, value, color }) {
|
|
1071
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: color, children: value })] }));
|
|
1360
|
+
const inputTok = (log.cacheReadTokens ?? 0) + (log.cacheCreationTokens ?? 0) + (log.inputTokens ?? 0);
|
|
1361
|
+
const outputTok = log.outputTokens ?? 0;
|
|
1362
|
+
const hitPct = inputTok > 0 ? Math.round(((log.cacheReadTokens ?? 0) / inputTok) * 100) : null;
|
|
1363
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : isWarn ? "yellow" : "cyan", children: " DETAILS " }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [time, " ", log.accountId] }), log.method && log.path && _jsx(Text, { color: "white", children: ` ${log.method} ${log.path}` }), statusLabel !== "" && _jsx(Text, { color: statusColor, children: ` ${statusLabel}` }), log.durationMs !== undefined && _jsx(Text, { color: "gray", children: ` ${log.durationMs}ms` }), _jsx(Text, { color: "gray", children: ` ${sourceFullLabel(log.source)}` }), log.details && _jsx(Text, { color: "gray", children: ` ${log.details}` })] }), (inputTok > 0 || outputTok > 0) && (_jsxs(Text, { color: "gray", children: [`${fmtTok(inputTok)} in · ${fmtTok(outputTok)} out`, hitPct !== null ? ` · cache ${hitPct}%` : ""] }))] }));
|
|
1072
1364
|
}
|
|
1073
1365
|
// ─── Cache health badge (aggregated) ─────────────────────────────────────────
|
|
1074
1366
|
function CacheHealthBadge({ read, created, input }) {
|
|
@@ -1080,13 +1372,6 @@ function CacheHealthBadge({ read, created, input }) {
|
|
|
1080
1372
|
const label = hitPct >= 70 ? "healthy" : hitPct >= 30 ? "fair" : "poor";
|
|
1081
1373
|
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, ")"] })] }));
|
|
1082
1374
|
}
|
|
1083
|
-
// ─── Cache breakdown (per-request detail) ────────────────────────────────────
|
|
1084
|
-
function CacheBreakdown({ read, created, input, output }) {
|
|
1085
|
-
const totalInput = read + created + input;
|
|
1086
|
-
const hitPct = totalInput > 0 ? (read / totalInput) * 100 : 0;
|
|
1087
|
-
const color = totalInput === 0 ? "gray" : hitPct >= 70 ? "green" : hitPct >= 30 ? "yellow" : "red";
|
|
1088
|
-
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" })] }));
|
|
1089
|
-
}
|
|
1090
1375
|
// ─── Token summary (aggregated totals) ──────────────────────────────────────
|
|
1091
1376
|
function TokenSummary({ cacheRead, cacheCreated, uncached, output }) {
|
|
1092
1377
|
const totalInput = cacheRead + cacheCreated + uncached;
|
|
@@ -1114,17 +1399,6 @@ function sourceFullLabel(source) {
|
|
|
1114
1399
|
return "API";
|
|
1115
1400
|
return "—";
|
|
1116
1401
|
}
|
|
1117
|
-
// ─── HTTP status text ─────────────────────────────────────────────────────────
|
|
1118
|
-
function httpStatusText(code) {
|
|
1119
|
-
const map = {
|
|
1120
|
-
200: "OK", 201: "Created", 204: "No Content",
|
|
1121
|
-
400: "Bad Request", 401: "Unauthorized", 403: "Forbidden",
|
|
1122
|
-
404: "Not Found", 429: "Too Many Requests",
|
|
1123
|
-
500: "Internal Server Error", 502: "Bad Gateway",
|
|
1124
|
-
503: "Service Unavailable", 529: "Overloaded",
|
|
1125
|
-
};
|
|
1126
|
-
return map[code] ?? "";
|
|
1127
|
-
}
|
|
1128
1402
|
// ─── Formatters ───────────────────────────────────────────────────────────────
|
|
1129
1403
|
function formatUptime(seconds) {
|
|
1130
1404
|
const h = Math.floor(seconds / 3_600);
|
|
@@ -1136,18 +1410,3 @@ function formatUptime(seconds) {
|
|
|
1136
1410
|
return `${m}m ${s}s`;
|
|
1137
1411
|
return `${s}s`;
|
|
1138
1412
|
}
|
|
1139
|
-
function formatMs(ms) {
|
|
1140
|
-
const totalMin = Math.round(ms / 60_000);
|
|
1141
|
-
if (totalMin >= 60)
|
|
1142
|
-
return `${Math.floor(totalMin / 60)}h ${totalMin % 60}m`;
|
|
1143
|
-
return `${totalMin}m`;
|
|
1144
|
-
}
|
|
1145
|
-
function formatAgo(ts) {
|
|
1146
|
-
if (!ts)
|
|
1147
|
-
return "never";
|
|
1148
|
-
const s = Math.round((Date.now() - ts) / 1_000);
|
|
1149
|
-
if (s < 60)
|
|
1150
|
-
return `${s}s ago`;
|
|
1151
|
-
const m = Math.floor(s / 60);
|
|
1152
|
-
return `${m}m ago`;
|
|
1153
|
-
}
|