@timo972/cc-router 0.12.0 → 0.12.1-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 +51 -13
- package/dist/providers/openai/account-state.js +1 -0
- package/dist/providers/openai/codex-transport.js +26 -11
- package/dist/providers/openai/device-oauth.js +1 -1
- package/dist/providers/openai/token-pool.js +2 -0
- package/dist/providers/openai/token-refresher.js +120 -23
- package/dist/providers/openai/usage-fetch.js +14 -1
- package/dist/proxy/messages-cross-route.js +31 -9
- package/dist/proxy/openai-ingress.js +75 -22
- package/dist/proxy/pool-refresh.js +80 -0
- package/dist/proxy/responses-server.js +23 -4
- package/dist/proxy/server.js +62 -3
- package/dist/proxy/transport-diagnostics.js +32 -0
- package/dist/proxy/transport-timing.js +153 -0
- package/dist/ui/Dashboard.js +75 -22
- package/dist/ui/accountsApi.js +31 -1
- package/package.json +1 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export function createHeaderDeadline(timeoutMs, parent) {
|
|
2
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
3
|
+
return { signal: parent, dispose() { } };
|
|
4
|
+
}
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
let timer = setTimeout(() => {
|
|
7
|
+
controller.abort(new DOMException("upstream headers timed out", "TimeoutError"));
|
|
8
|
+
}, timeoutMs);
|
|
9
|
+
const onParentAbort = () => controller.abort(parent?.reason);
|
|
10
|
+
if (parent?.aborted) {
|
|
11
|
+
onParentAbort();
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
parent?.addEventListener("abort", onParentAbort, { once: true });
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
dispose() {
|
|
19
|
+
if (timer)
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
timer = undefined;
|
|
22
|
+
parent?.removeEventListener("abort", onParentAbort);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Wrap a body with a timeout for each upstream read.
|
|
28
|
+
*
|
|
29
|
+
* The zero high-water mark is deliberate. A positive high-water mark lets the
|
|
30
|
+
* Web Streams implementation prefetch the next chunk as soon as the previous
|
|
31
|
+
* one is delivered. When the HTTP response is waiting for downstream drain,
|
|
32
|
+
* that prefetched read would run the upstream idle timer and could time out an
|
|
33
|
+
* otherwise healthy stream solely because its client was slow.
|
|
34
|
+
*/
|
|
35
|
+
export function withStreamIdleTimeout(body, idleMs, parent) {
|
|
36
|
+
if (!body)
|
|
37
|
+
return null;
|
|
38
|
+
const timeoutEnabled = idleMs !== undefined && idleMs > 0;
|
|
39
|
+
if (!timeoutEnabled && !parent)
|
|
40
|
+
return body;
|
|
41
|
+
const reader = body.getReader();
|
|
42
|
+
let timer;
|
|
43
|
+
let closed = false;
|
|
44
|
+
let controllerRef;
|
|
45
|
+
const clearIdleTimer = () => {
|
|
46
|
+
if (timer)
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
timer = undefined;
|
|
49
|
+
};
|
|
50
|
+
const releaseReader = () => {
|
|
51
|
+
try {
|
|
52
|
+
reader.releaseLock();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// A cancellation can briefly leave read() pending. reader.cancel()
|
|
56
|
+
// settles it before trying this again in its finally callback.
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const finish = (release = true) => {
|
|
60
|
+
if (closed)
|
|
61
|
+
return;
|
|
62
|
+
closed = true;
|
|
63
|
+
clearIdleTimer();
|
|
64
|
+
parent?.removeEventListener("abort", onAbort);
|
|
65
|
+
if (release)
|
|
66
|
+
releaseReader();
|
|
67
|
+
};
|
|
68
|
+
const fail = (error) => {
|
|
69
|
+
if (closed)
|
|
70
|
+
return;
|
|
71
|
+
finish(false);
|
|
72
|
+
controllerRef?.error(error);
|
|
73
|
+
void reader.cancel(error).catch(() => { }).finally(releaseReader);
|
|
74
|
+
};
|
|
75
|
+
const onAbort = () => {
|
|
76
|
+
fail(parent?.reason ?? new DOMException("aborted", "AbortError"));
|
|
77
|
+
};
|
|
78
|
+
const startIdleTimer = () => {
|
|
79
|
+
if (!timeoutEnabled)
|
|
80
|
+
return;
|
|
81
|
+
clearIdleTimer();
|
|
82
|
+
timer = setTimeout(() => {
|
|
83
|
+
fail(new DOMException("upstream stream idle timeout", "TimeoutError"));
|
|
84
|
+
}, idleMs);
|
|
85
|
+
};
|
|
86
|
+
return new ReadableStream({
|
|
87
|
+
start(controller) {
|
|
88
|
+
controllerRef = controller;
|
|
89
|
+
if (parent?.aborted)
|
|
90
|
+
onAbort();
|
|
91
|
+
else
|
|
92
|
+
parent?.addEventListener("abort", onAbort, { once: true });
|
|
93
|
+
},
|
|
94
|
+
async pull(controller) {
|
|
95
|
+
if (closed)
|
|
96
|
+
return;
|
|
97
|
+
startIdleTimer();
|
|
98
|
+
try {
|
|
99
|
+
const next = await reader.read();
|
|
100
|
+
clearIdleTimer();
|
|
101
|
+
if (closed)
|
|
102
|
+
return;
|
|
103
|
+
if (next.done) {
|
|
104
|
+
finish();
|
|
105
|
+
controller.close();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
controller.enqueue(next.value);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (closed)
|
|
112
|
+
return;
|
|
113
|
+
finish();
|
|
114
|
+
controller.error(error);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
async cancel(reason) {
|
|
118
|
+
if (closed)
|
|
119
|
+
return;
|
|
120
|
+
finish(false);
|
|
121
|
+
try {
|
|
122
|
+
await reader.cancel(reason);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
releaseReader();
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
}, { highWaterMark: 0 });
|
|
129
|
+
}
|
|
130
|
+
/** Resolve only when an HTTP response can accept another write. Every losing
|
|
131
|
+
* listener is removed, including the error/close path. */
|
|
132
|
+
export function waitForWritable(res) {
|
|
133
|
+
if (res.destroyed)
|
|
134
|
+
return Promise.resolve(false);
|
|
135
|
+
return new Promise(resolve => {
|
|
136
|
+
let done = false;
|
|
137
|
+
const finish = (value) => {
|
|
138
|
+
if (done)
|
|
139
|
+
return;
|
|
140
|
+
done = true;
|
|
141
|
+
res.removeListener("drain", drain);
|
|
142
|
+
res.removeListener("close", closed);
|
|
143
|
+
res.removeListener("error", errored);
|
|
144
|
+
resolve(value);
|
|
145
|
+
};
|
|
146
|
+
const drain = () => finish(true);
|
|
147
|
+
const closed = () => finish(false);
|
|
148
|
+
const errored = () => finish(false);
|
|
149
|
+
res.once("drain", drain);
|
|
150
|
+
res.once("close", closed);
|
|
151
|
+
res.once("error", errored);
|
|
152
|
+
});
|
|
153
|
+
}
|
package/dist/ui/Dashboard.js
CHANGED
|
@@ -7,6 +7,9 @@ import { getCurrentVersion } from "../utils/self-update.js";
|
|
|
7
7
|
import { readClaudeRouting, readCodexRouting, setClaudeRouting, setCodexRouting, } from "../utils/cli-routing.js";
|
|
8
8
|
import { mergeGrokIntoHealth, loadGrokHealthSnapshotsWithSubscription } from "../providers/xai/overview.js";
|
|
9
9
|
const POLL_INTERVAL_MS = 2_000;
|
|
10
|
+
/** Progress banner for the manual reload; replaced by the result banner, so
|
|
11
|
+
* only the failure case (client gave up) ever lets it expire. */
|
|
12
|
+
const REFRESH_ALL_BANNER_MS = 120_000;
|
|
10
13
|
/** Grok's plan/code-access rarely change; refresh far slower than the 2s poll. */
|
|
11
14
|
const GROK_SUBSCRIPTION_INTERVAL_MS = 60_000;
|
|
12
15
|
/** Most activity rows the dashboard will show — the list shrinks below this
|
|
@@ -543,22 +546,22 @@ export function Dashboard({ port, baseUrl, authToken, onIntent }) {
|
|
|
543
546
|
// /v1/user round-trip every tick. Undefined until the first fetch lands, at
|
|
544
547
|
// which point `mergeGrokIntoHealth` falls back to its access-token tier.
|
|
545
548
|
const grokSnapshotsRef = useRef(undefined);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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); };
|
|
549
|
+
const refreshGrokSnapshots = useCallback(async () => {
|
|
550
|
+
try {
|
|
551
|
+
grokSnapshotsRef.current = await loadGrokHealthSnapshotsWithSubscription();
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// Keep the last known snapshots; the poll degrades to the tier fallback.
|
|
555
|
+
}
|
|
561
556
|
}, []);
|
|
557
|
+
useEffect(() => {
|
|
558
|
+
refreshGrokSnapshots();
|
|
559
|
+
const timer = setInterval(refreshGrokSnapshots, GROK_SUBSCRIPTION_INTERVAL_MS);
|
|
560
|
+
return () => { clearInterval(timer); };
|
|
561
|
+
}, [refreshGrokSnapshots]);
|
|
562
|
+
// The manual reload key re-runs the same poll immediately so the freshly
|
|
563
|
+
// fetched usage shows up without waiting for the next 2s tick.
|
|
564
|
+
const pollRef = useRef(async () => { });
|
|
562
565
|
useEffect(() => {
|
|
563
566
|
let cancelled = false;
|
|
564
567
|
const healthUrl = `${resolvedBase}/cc-router/health`;
|
|
@@ -590,24 +593,29 @@ export function Dashboard({ port, baseUrl, authToken, onIntent }) {
|
|
|
590
593
|
setRetryCount(n => n + 1);
|
|
591
594
|
}
|
|
592
595
|
};
|
|
596
|
+
pollRef.current = poll;
|
|
593
597
|
poll();
|
|
594
598
|
const timer = setInterval(poll, POLL_INTERVAL_MS);
|
|
595
599
|
return () => { cancelled = true; clearInterval(timer); };
|
|
596
600
|
}, [resolvedBase, authToken]);
|
|
601
|
+
const refreshLocalViews = useCallback(async () => {
|
|
602
|
+
await refreshGrokSnapshots();
|
|
603
|
+
await pollRef.current();
|
|
604
|
+
}, [refreshGrokSnapshots]);
|
|
597
605
|
if (connectError) {
|
|
598
606
|
return _jsx(ErrorScreen, { error: connectError, port: port, retries: retryCount });
|
|
599
607
|
}
|
|
600
608
|
if (!data) {
|
|
601
609
|
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u280B Connecting to ", resolvedBase, "..."] }) }));
|
|
602
610
|
}
|
|
603
|
-
return (_jsx(LiveDashboard, { data: data, port: port, baseUrl: resolvedBase, lastUpdate: lastUpdate, api: api, modelsApi: modelsApi, onIntent: onIntent }));
|
|
611
|
+
return (_jsx(LiveDashboard, { data: data, port: port, baseUrl: resolvedBase, lastUpdate: lastUpdate, api: api, modelsApi: modelsApi, onIntent: onIntent, onRefreshAll: refreshLocalViews }));
|
|
604
612
|
}
|
|
605
613
|
// ─── Error screen ─────────────────────────────────────────────────────────────
|
|
606
614
|
function ErrorScreen({ error, port, retries }) {
|
|
607
615
|
return (_jsxs(Box, { flexDirection: "column", marginY: 1, marginX: 2, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Is the proxy running? Start it with:" }), _jsx(Text, { color: "cyan", children: " cc-router start" })] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", children: ["Retrying every ", POLL_INTERVAL_MS / 1000, "s"] }), retries > 0 && _jsxs(Text, { color: "gray", children: [" (attempt ", retries, ")"] }), _jsx(Text, { color: "gray", children: " \u00B7 [q] quit" })] })] }));
|
|
608
616
|
}
|
|
609
617
|
// ─── Live dashboard ───────────────────────────────────────────────────────────
|
|
610
|
-
function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onIntent, }) {
|
|
618
|
+
function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onIntent, onRefreshAll, }) {
|
|
611
619
|
const [cliRouting, setCliRouting] = useState(() => ({
|
|
612
620
|
claude: readClaudeRouting(),
|
|
613
621
|
codex: readCodexRouting(),
|
|
@@ -770,14 +778,14 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
770
778
|
// setBanner can fire on an unmounted component after `n` exits Ink.
|
|
771
779
|
const [banner, setBanner] = useState(null);
|
|
772
780
|
const bannerTimerRef = useRef(null);
|
|
773
|
-
const showBanner = useCallback((text, color) => {
|
|
781
|
+
const showBanner = useCallback((text, color, durationMs = 4_000) => {
|
|
774
782
|
if (bannerTimerRef.current)
|
|
775
783
|
clearTimeout(bannerTimerRef.current);
|
|
776
784
|
setBanner({ text, color });
|
|
777
785
|
bannerTimerRef.current = setTimeout(() => {
|
|
778
786
|
setBanner(null);
|
|
779
787
|
bannerTimerRef.current = null;
|
|
780
|
-
},
|
|
788
|
+
}, durationMs);
|
|
781
789
|
}, []);
|
|
782
790
|
useEffect(() => () => {
|
|
783
791
|
if (bannerTimerRef.current)
|
|
@@ -872,6 +880,47 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
872
880
|
showBanner(`Models error: ${errMsg(err)}`, "red");
|
|
873
881
|
}
|
|
874
882
|
}, [modelsApi, showBanner]);
|
|
883
|
+
// ── Reload everything ────────────────────────────────────────────────────
|
|
884
|
+
// One key for "make it look like I just restarted the router": the server
|
|
885
|
+
// sweeps cooldowns, re-tries due/quarantined tokens and re-fetches every
|
|
886
|
+
// account's usage; the dashboard then re-reads Grok, CLI routing, the
|
|
887
|
+
// model list (if already loaded) and polls health right away.
|
|
888
|
+
const refreshAllInFlightRef = useRef(false);
|
|
889
|
+
const doRefreshAll = useCallback(async () => {
|
|
890
|
+
if (refreshAllInFlightRef.current) {
|
|
891
|
+
showBanner("Reload already running…", "gray");
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
refreshAllInFlightRef.current = true;
|
|
895
|
+
// Stays up until the result banner replaces it: a reload with several
|
|
896
|
+
// slow providers can outlive the default banner lifetime.
|
|
897
|
+
showBanner("Reloading accounts, usage and models…", "cyan", REFRESH_ALL_BANNER_MS);
|
|
898
|
+
try {
|
|
899
|
+
const result = await api.refreshAll();
|
|
900
|
+
refreshCliRouting();
|
|
901
|
+
if (modelsStatus) {
|
|
902
|
+
try {
|
|
903
|
+
const status = await modelsApi.list();
|
|
904
|
+
setModelsStatus(status);
|
|
905
|
+
}
|
|
906
|
+
catch {
|
|
907
|
+
// The account reload succeeded; a stale model list is not worth a red banner.
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
await onRefreshAll?.();
|
|
911
|
+
const problems = [
|
|
912
|
+
result.usageFailed > 0 ? `${result.usageFailed} usage fetch failed` : "",
|
|
913
|
+
result.tokenRefreshFailed > 0 ? `${result.tokenRefreshFailed} token refresh failed` : "",
|
|
914
|
+
].filter(Boolean);
|
|
915
|
+
showBanner(`Reloaded ${result.accounts} accounts — usage fresh for ${result.usageRefreshed}${problems.length ? `, ${problems.join(", ")}` : ""}`, problems.length ? "yellow" : "green");
|
|
916
|
+
}
|
|
917
|
+
catch (err) {
|
|
918
|
+
showBanner(`Reload error: ${errMsg(err)}`, "red");
|
|
919
|
+
}
|
|
920
|
+
finally {
|
|
921
|
+
refreshAllInFlightRef.current = false;
|
|
922
|
+
}
|
|
923
|
+
}, [api, modelsApi, modelsStatus, onRefreshAll, refreshCliRouting, showBanner]);
|
|
875
924
|
const doToggleCli = useCallback((target) => {
|
|
876
925
|
const current = target === "claude" ? cliRouting.claude.enabled : cliRouting.codex.enabled;
|
|
877
926
|
const result = target === "claude"
|
|
@@ -970,6 +1019,10 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
970
1019
|
exit();
|
|
971
1020
|
return;
|
|
972
1021
|
}
|
|
1022
|
+
if (input === "R") {
|
|
1023
|
+
void doRefreshAll();
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
973
1026
|
if (key.tab) {
|
|
974
1027
|
// Compact view hides the activity list, so skip "logs" in the cycle.
|
|
975
1028
|
if (compact) {
|
|
@@ -1119,10 +1172,10 @@ function LiveDashboard({ data, port, baseUrl, lastUpdate, api, modelsApi, onInte
|
|
|
1119
1172
|
_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
|
|
1120
1173
|
? _jsx(Text, { color: "gray", children: " No activity yet" })
|
|
1121
1174
|
: 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]"
|
|
1175
|
+
? " [Tab] [e] toggle [a]/[o]/[g] provider [n] add [d] delete [w] 7d [s] 5h [R] reload [z] compact [q]"
|
|
1123
1176
|
: focus === "models"
|
|
1124
|
-
? " [Tab] [m/r] refresh [c]/[o] default [Esc] logs [z] compact [q]"
|
|
1125
|
-
: " [Tab] [m] models [z] compact [q] quit" }) })] }) }));
|
|
1177
|
+
? " [Tab] [m/r] refresh [c]/[o] default [R] reload all [Esc] logs [z] compact [q]"
|
|
1178
|
+
: " [Tab] [m] models [R] reload [z] compact [q] quit" }) })] }) }));
|
|
1126
1179
|
}
|
|
1127
1180
|
function OperationsPanel({ operational, baseUrl, focus, cliRouting }) {
|
|
1128
1181
|
const authLabel = operational.auth.required ? "protected" : "open";
|
package/dist/ui/accountsApi.js
CHANGED
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
* flow is not here because it runs inquirer.
|
|
7
7
|
*/
|
|
8
8
|
const REQUEST_TIMEOUT_MS = 3_000;
|
|
9
|
+
// Worst case is every account timing out: OpenAI token refreshes are
|
|
10
|
+
// sequential with a 15s deadline each, usage fetches run two at a time with
|
|
11
|
+
// 10s each. The server single-flights the pass, so a client that does give
|
|
12
|
+
// up and presses again joins the running one rather than stacking another.
|
|
13
|
+
const REFRESH_ALL_TIMEOUT_MS = 120_000;
|
|
9
14
|
const MAX_PUBLIC_ROWS = 12;
|
|
10
15
|
export function createAccountsApi(baseUrl, authToken) {
|
|
11
|
-
const
|
|
16
|
+
const root = baseUrl.replace(/\/+$/, "");
|
|
17
|
+
const base = root + "/cc-router/accounts";
|
|
12
18
|
const authHeaders = authToken ? { authorization: `Bearer ${authToken}` } : {};
|
|
13
19
|
async function send(method, path, body) {
|
|
14
20
|
const res = await fetch(base + path, {
|
|
@@ -35,13 +41,37 @@ export function createAccountsApi(baseUrl, authToken) {
|
|
|
35
41
|
const payload = await res.json();
|
|
36
42
|
return Array.isArray(payload.accounts) ? payload.accounts.flatMap(publicAccountSafeView) : [];
|
|
37
43
|
}
|
|
44
|
+
async function refreshAll() {
|
|
45
|
+
// Usage fetches for every account run behind this call, so it gets its
|
|
46
|
+
// own, longer budget than the account mutations above.
|
|
47
|
+
const res = await fetch(root + "/cc-router/refresh", {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: authHeaders,
|
|
50
|
+
signal: AbortSignal.timeout(REFRESH_ALL_TIMEOUT_MS),
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok)
|
|
53
|
+
throw new Error(`HTTP ${res.status}`);
|
|
54
|
+
const payload = await res.json();
|
|
55
|
+
return publicRefreshResult(payload.refresh);
|
|
56
|
+
}
|
|
38
57
|
return {
|
|
39
58
|
list,
|
|
59
|
+
refreshAll,
|
|
40
60
|
patch(id, patch) { return send("PATCH", `/${encodeURIComponent(id)}`, patch); },
|
|
41
61
|
setProviderEnabled(provider, enabled) { return send("PATCH", `/providers/${encodeURIComponent(provider)}`, { enabled }); },
|
|
42
62
|
remove(id) { return send("DELETE", `/${encodeURIComponent(id)}`); },
|
|
43
63
|
};
|
|
44
64
|
}
|
|
65
|
+
function publicRefreshResult(value) {
|
|
66
|
+
const record = isRecord(value) ? value : {};
|
|
67
|
+
return {
|
|
68
|
+
accounts: publicInteger(record.accounts),
|
|
69
|
+
usageRefreshed: publicInteger(record.usageRefreshed),
|
|
70
|
+
usageFailed: publicInteger(record.usageFailed),
|
|
71
|
+
tokenRefreshFailed: publicInteger(record.tokenRefreshFailed),
|
|
72
|
+
durationMs: publicInteger(record.durationMs),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
45
75
|
function isRecord(value) {
|
|
46
76
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
47
77
|
}
|
package/package.json
CHANGED