@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +33 -4
  3. package/dist/cli/cmd-accounts.js +91 -13
  4. package/dist/cli/cmd-cli-targets.js +177 -0
  5. package/dist/cli/cmd-client.js +3 -2
  6. package/dist/cli/cmd-configure.js +18 -4
  7. package/dist/cli/cmd-status.js +4 -1
  8. package/dist/cli/cmd-stop.js +18 -0
  9. package/dist/cli/index.js +10 -3
  10. package/dist/config/manager.js +19 -3
  11. package/dist/config/paths.js +1 -0
  12. package/dist/protocol/anthropic-to-openai.js +31 -19
  13. package/dist/protocol/model-ref.js +1 -1
  14. package/dist/protocol/openai-function-call.js +16 -0
  15. package/dist/protocol/openai-response-to-anthropic.js +34 -6
  16. package/dist/protocol/openai-stream-to-anthropic.js +116 -16
  17. package/dist/protocol/openai-to-anthropic.js +54 -29
  18. package/dist/providers/openai/account-state.js +4 -1
  19. package/dist/providers/openai/usage.js +14 -1
  20. package/dist/providers/xai/account-record.js +42 -0
  21. package/dist/providers/xai/device-oauth.js +116 -0
  22. package/dist/providers/xai/import-auth.js +70 -0
  23. package/dist/providers/xai/overview.js +279 -0
  24. package/dist/providers/xai/subscription-fetch.js +56 -0
  25. package/dist/proxy/allowance.js +142 -0
  26. package/dist/proxy/logger.js +4 -3
  27. package/dist/proxy/messages-cross-route.js +113 -8
  28. package/dist/proxy/models-server.js +30 -2
  29. package/dist/proxy/openai-ingress.js +7 -1
  30. package/dist/proxy/openai-routing.js +12 -7
  31. package/dist/proxy/server.js +110 -6
  32. package/dist/ui/Dashboard.js +383 -124
  33. package/dist/ui/accountsApi.js +3 -1
  34. package/dist/utils/cli-routing.js +80 -0
  35. package/dist/utils/codex-config.js +337 -15
  36. package/package.json +3 -2
@@ -0,0 +1,116 @@
1
+ import { createXaiAccountRecord, XAI_DEFAULT_SCOPES } from "./account-record.js";
2
+ const DEFAULT_ISSUER = "https://auth.x.ai";
3
+ const DEFAULT_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
4
+ const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
5
+ function issuerOf(opts) {
6
+ return (opts.issuer ?? DEFAULT_ISSUER).replace(/\/+$/, "");
7
+ }
8
+ function clientIdOf(opts) {
9
+ return opts.clientId ?? DEFAULT_CLIENT_ID;
10
+ }
11
+ function fetchOf(opts) {
12
+ return opts.fetchImpl ?? fetch;
13
+ }
14
+ async function readError(res) {
15
+ try {
16
+ return await res.text();
17
+ }
18
+ catch {
19
+ return "";
20
+ }
21
+ }
22
+ function parseAccessTokenExpiry(accessToken, expiresIn, now = Date.now()) {
23
+ const parts = accessToken.split(".");
24
+ if (parts[1]) {
25
+ try {
26
+ const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
27
+ if (typeof claims.exp === "number" && Number.isFinite(claims.exp) && claims.exp > 0) {
28
+ return claims.exp * 1000;
29
+ }
30
+ }
31
+ catch { /* fall through */ }
32
+ }
33
+ if (typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0) {
34
+ return now + expiresIn * 1000;
35
+ }
36
+ throw new Error("xAI access token has no usable expiry");
37
+ }
38
+ export async function requestXaiDeviceCode(opts = {}) {
39
+ const issuer = issuerOf(opts);
40
+ const form = new URLSearchParams({
41
+ client_id: clientIdOf(opts),
42
+ scope: XAI_DEFAULT_SCOPES.join(" "),
43
+ });
44
+ const res = await fetchOf(opts)(`${issuer}/oauth2/device/code`, {
45
+ method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/x-www-form-urlencoded",
48
+ Accept: "application/json",
49
+ },
50
+ body: form.toString(),
51
+ });
52
+ if (!res.ok) {
53
+ throw new Error(`xAI device code request failed (${res.status}): ${await readError(res)}`);
54
+ }
55
+ const body = await res.json();
56
+ if (!body.device_code || !body.user_code || !body.verification_uri) {
57
+ throw new Error("xAI device code response is missing device_code, user_code, or verification_uri");
58
+ }
59
+ return {
60
+ verificationUrl: body.verification_uri_complete ?? body.verification_uri,
61
+ userCode: body.user_code,
62
+ deviceCode: body.device_code,
63
+ intervalSeconds: Number(body.interval ?? 5),
64
+ };
65
+ }
66
+ export async function exchangeXaiDeviceCodeForTokens(opts) {
67
+ const issuer = issuerOf(opts);
68
+ const sleep = opts.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
69
+ const now = opts.now ?? Date.now;
70
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
71
+ const started = now();
72
+ while (now() - started <= timeoutMs) {
73
+ const form = new URLSearchParams({
74
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
75
+ device_code: opts.deviceCode.deviceCode,
76
+ client_id: clientIdOf(opts),
77
+ });
78
+ const res = await fetchOf(opts)(`${issuer}/oauth2/token`, {
79
+ method: "POST",
80
+ headers: {
81
+ "Content-Type": "application/x-www-form-urlencoded",
82
+ Accept: "application/json",
83
+ },
84
+ body: form.toString(),
85
+ });
86
+ const body = await res.json().catch(() => ({}));
87
+ if (res.ok && body.access_token && body.refresh_token) {
88
+ return {
89
+ accessToken: body.access_token,
90
+ refreshToken: body.refresh_token,
91
+ expiresAt: parseAccessTokenExpiry(body.access_token, body.expires_in, now()),
92
+ };
93
+ }
94
+ const error = body.error ?? "";
95
+ if (error === "authorization_pending" || error === "slow_down" || res.status === 400 && !error) {
96
+ const wait = error === "slow_down"
97
+ ? Math.max(1, opts.deviceCode.intervalSeconds) + 5
98
+ : Math.max(1, opts.deviceCode.intervalSeconds);
99
+ await sleep(wait * 1000);
100
+ continue;
101
+ }
102
+ throw new Error(`xAI device authorization failed (${res.status}): ${body.error_description ?? (error || await readError(res))}`);
103
+ }
104
+ throw new Error("xAI device authorization timed out");
105
+ }
106
+ export async function loginXaiWithDeviceCode(opts) {
107
+ const deviceCode = await requestXaiDeviceCode(opts);
108
+ opts.onDeviceCode?.(deviceCode);
109
+ const tokens = await exchangeXaiDeviceCodeForTokens({ ...opts, deviceCode });
110
+ return createXaiAccountRecord({
111
+ id: opts.accountId,
112
+ accessToken: tokens.accessToken,
113
+ refreshToken: tokens.refreshToken,
114
+ expiresAt: tokens.expiresAt,
115
+ });
116
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import path from "path";
3
+ import { grokHomeDir } from "./overview.js";
4
+ import { createXaiAccountRecord } from "./account-record.js";
5
+ /**
6
+ * Copy the Grok CLI OIDC login (~/.grok/auth.json) into an accounts.json record.
7
+ * Does not write the file itself — the caller persists via upsert.
8
+ */
9
+ export function importGrokCliAuth(opts = {}) {
10
+ const grokHome = opts.grokHome ?? grokHomeDir();
11
+ const fileExists = opts.fileExists ?? existsSync;
12
+ const readFile = opts.readFile ?? ((filePath) => readFileSync(filePath, "utf-8"));
13
+ const authPath = path.join(grokHome, "auth.json");
14
+ if (!fileExists(authPath)) {
15
+ throw new Error(`No Grok CLI login at ${authPath}. Run: grok login`);
16
+ }
17
+ let raw;
18
+ try {
19
+ raw = JSON.parse(readFile(authPath));
20
+ }
21
+ catch {
22
+ throw new Error(`Could not parse ${authPath}`);
23
+ }
24
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
25
+ throw new Error(`${authPath} is not a Grok auth map`);
26
+ }
27
+ const entry = firstOidcEntry(raw);
28
+ if (!entry) {
29
+ throw new Error("No OIDC Grok login found in auth.json. Run: grok login");
30
+ }
31
+ const accessToken = typeof entry.key === "string" ? entry.key.trim() : "";
32
+ const refreshToken = typeof entry.refresh_token === "string" ? entry.refresh_token.trim() : "";
33
+ const expiresAt = parseExpiresAt(entry.expires_at, accessToken);
34
+ const id = opts.accountId?.trim() || "grok";
35
+ return createXaiAccountRecord({
36
+ id,
37
+ accessToken,
38
+ refreshToken,
39
+ expiresAt,
40
+ });
41
+ }
42
+ function firstOidcEntry(raw) {
43
+ for (const value of Object.values(raw)) {
44
+ if (value === null || typeof value !== "object" || Array.isArray(value))
45
+ continue;
46
+ const entry = value;
47
+ if (entry.auth_mode === "oidc" && typeof entry.key === "string" && typeof entry.refresh_token === "string") {
48
+ return entry;
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+ function parseExpiresAt(value, accessToken) {
54
+ if (typeof value === "string" && value.trim()) {
55
+ const parsed = Date.parse(value);
56
+ if (Number.isFinite(parsed) && parsed > 0)
57
+ return parsed;
58
+ }
59
+ const parts = accessToken.split(".");
60
+ if (parts[1]) {
61
+ try {
62
+ const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
63
+ if (typeof claims.exp === "number" && Number.isFinite(claims.exp) && claims.exp > 0) {
64
+ return claims.exp * 1000;
65
+ }
66
+ }
67
+ catch { /* fall through */ }
68
+ }
69
+ throw new Error("Grok login is missing a usable expiry");
70
+ }
@@ -0,0 +1,279 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { loadXaiAccounts } from "../../config/manager.js";
5
+ import { fetchGrokSubscription } from "./subscription-fetch.js";
6
+ function loadConfiguredXaiAccounts() {
7
+ // Read the override at call time. This keeps the overview aligned with the
8
+ // manager's documented ACCOUNTS_PATH override even when the environment is
9
+ // installed after this module has been imported (as in embedded callers and
10
+ // tests); without it the paths module's import-time snapshot can hide stored
11
+ // xAI accounts from the dashboard.
12
+ return loadXaiAccounts(process.env["ACCOUNTS_PATH"] || undefined);
13
+ }
14
+ export function grokHomeDir(homeDir = os.homedir()) {
15
+ const fromEnv = process.env["GROK_HOME"]?.trim();
16
+ if (fromEnv)
17
+ return fromEnv;
18
+ return path.join(homeDir, ".grok");
19
+ }
20
+ export function loadGrokAccountSnapshots(opts = {}) {
21
+ const grokHome = opts.grokHome ?? grokHomeDir();
22
+ const now = opts.now ?? Date.now;
23
+ const isProcessAlive = opts.isProcessAlive ?? defaultIsProcessAlive;
24
+ const readFile = opts.readFile ?? ((filePath) => readFileSync(filePath, "utf-8"));
25
+ const fileExists = opts.fileExists ?? existsSync;
26
+ const authPath = path.join(grokHome, "auth.json");
27
+ if (!fileExists(authPath))
28
+ return [];
29
+ let raw;
30
+ try {
31
+ raw = JSON.parse(readFile(authPath));
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
37
+ return [];
38
+ const liveSessions = readLiveSessions(path.join(grokHome, "active_sessions.json"), { readFile, fileExists, isProcessAlive });
39
+ const nowMs = now();
40
+ const usedIds = new Set();
41
+ const snapshots = [];
42
+ for (const entry of Object.values(raw)) {
43
+ const snapshot = snapshotFromAuthEntry(entry, liveSessions.length, nowMs, usedIds);
44
+ if (snapshot)
45
+ snapshots.push(snapshot);
46
+ }
47
+ return snapshots;
48
+ }
49
+ /** Stored xAI accounts if any, otherwise the live Grok CLI login. */
50
+ export function loadGrokHealthSnapshots(opts = {}) {
51
+ const overlay = loadGrokAccountSnapshots(opts);
52
+ const liveSessions = overlay.reduce((sum, account) => Math.max(sum, account.activeSessions), 0);
53
+ const stored = loadConfiguredXaiAccounts();
54
+ if (stored.length === 0)
55
+ return overlay;
56
+ const now = (opts.now ?? Date.now)();
57
+ return stored.map(account => {
58
+ const tier = grokTierFromAccessToken(account.accessToken);
59
+ return {
60
+ id: account.id,
61
+ provider: "xai_subscription",
62
+ enabled: true,
63
+ healthy: account.enabled !== false && account.expiresAt > now,
64
+ busy: liveSessions > 0,
65
+ inFlightRequests: 0,
66
+ activeSessions: liveSessions,
67
+ requestCount: 0,
68
+ errorCount: 0,
69
+ expiresInMs: account.expiresAt - now,
70
+ lastUsedMs: 0,
71
+ lastRefreshMs: 0,
72
+ ...(tier !== undefined ? { tier } : {}),
73
+ };
74
+ });
75
+ }
76
+ /**
77
+ * Sync snapshots enriched with the live plan name + code-access flag. Each
78
+ * stored xAI account gets one `/v1/user` lookup (matched by id); a failed or
79
+ * missing lookup leaves the snapshot on its access-token `tier` fallback, so an
80
+ * offline dashboard degrades to the coarse tier instead of dropping the row.
81
+ */
82
+ export async function loadGrokHealthSnapshotsWithSubscription(opts = {}) {
83
+ const base = loadGrokHealthSnapshots(opts);
84
+ if (base.length === 0)
85
+ return base;
86
+ const accounts = opts.accounts ?? loadConfiguredXaiAccounts();
87
+ const fetchSubscription = opts.fetchSubscription
88
+ ?? ((accessToken) => fetchGrokSubscription({ accessToken }));
89
+ return Promise.all(base.map(async (snapshot) => {
90
+ const account = accounts.find(candidate => candidate.id === snapshot.id);
91
+ if (!account)
92
+ return snapshot;
93
+ const result = await fetchSubscription(account.accessToken);
94
+ if (!result.ok)
95
+ return snapshot;
96
+ return {
97
+ ...snapshot,
98
+ ...(result.subscriptionTier !== undefined ? { subscriptionTier: result.subscriptionTier } : {}),
99
+ ...(result.hasCodeAccess !== undefined ? { hasCodeAccess: result.hasCodeAccess } : {}),
100
+ };
101
+ }));
102
+ }
103
+ export function grokSnapshotAsHealthAccount(snapshot) {
104
+ const xai = {
105
+ ...(snapshot.tier !== undefined ? { tier: snapshot.tier } : {}),
106
+ ...(snapshot.subscriptionTier !== undefined ? { subscriptionTier: snapshot.subscriptionTier } : {}),
107
+ ...(snapshot.hasCodeAccess !== undefined ? { hasCodeAccess: snapshot.hasCodeAccess } : {}),
108
+ };
109
+ return {
110
+ id: snapshot.id,
111
+ provider: "xai_subscription",
112
+ enabled: true,
113
+ healthy: snapshot.healthy,
114
+ busy: snapshot.busy,
115
+ inFlightRequests: 0,
116
+ activeSessions: snapshot.activeSessions,
117
+ requestCount: snapshot.requestCount,
118
+ errorCount: snapshot.errorCount,
119
+ expiresInMs: snapshot.expiresInMs,
120
+ lastUsedMs: snapshot.lastUsedMs,
121
+ lastRefreshMs: snapshot.lastRefreshMs,
122
+ ...(Object.keys(xai).length > 0 ? { xai } : {}),
123
+ };
124
+ }
125
+ export function mergeGrokIntoHealth(health, snapshots = loadGrokHealthSnapshots()) {
126
+ // The proxy daemon already emits the Grok row from the sync (network-free)
127
+ // path, so it carries only the coarse `tier`. Rather than skip enrichment,
128
+ // overlay the live plan name / code-access flag from the async snapshots the
129
+ // caller polled — otherwise the daemon-served row would stay stuck on "tier N".
130
+ if (health.accounts.some(account => account.provider === "xai_subscription")) {
131
+ return enrichExistingGrokAccounts(health, snapshots);
132
+ }
133
+ const grokAccounts = snapshots.map(grokSnapshotAsHealthAccount);
134
+ if (grokAccounts.length === 0)
135
+ return health;
136
+ const healthy = grokAccounts.filter(account => account.healthy).length;
137
+ const xai = {
138
+ configured: true,
139
+ accounts: grokAccounts.length,
140
+ healthy,
141
+ enabled: grokAccounts.length,
142
+ };
143
+ return {
144
+ ...health,
145
+ accounts: [...health.accounts, ...grokAccounts],
146
+ ...(health.operational
147
+ ? { operational: { ...health.operational, providers: { ...health.operational.providers, xai } } }
148
+ : {}),
149
+ };
150
+ }
151
+ /**
152
+ * Overlay the live plan name / code-access flag from freshly-polled snapshots
153
+ * onto the tier-only Grok rows the proxy daemon serves. Matches by account id,
154
+ * with a single-account fallback (the common one-Grok-login case). Returns the
155
+ * same object untouched when nothing changed, so the fast poll path stays cheap.
156
+ */
157
+ function enrichExistingGrokAccounts(health, snapshots) {
158
+ if (snapshots.length === 0)
159
+ return health;
160
+ const byId = new Map(snapshots.map(snapshot => [snapshot.id, snapshot]));
161
+ let changed = false;
162
+ const accounts = health.accounts.map(account => {
163
+ if (account.provider !== "xai_subscription")
164
+ return account;
165
+ const snapshot = (account.id !== undefined ? byId.get(account.id) : undefined)
166
+ ?? (snapshots.length === 1 ? snapshots[0] : undefined);
167
+ if (!snapshot)
168
+ return account;
169
+ const enrichment = {
170
+ ...(snapshot.tier !== undefined ? { tier: snapshot.tier } : {}),
171
+ ...(snapshot.subscriptionTier !== undefined ? { subscriptionTier: snapshot.subscriptionTier } : {}),
172
+ ...(snapshot.hasCodeAccess !== undefined ? { hasCodeAccess: snapshot.hasCodeAccess } : {}),
173
+ };
174
+ if (Object.keys(enrichment).length === 0)
175
+ return account;
176
+ changed = true;
177
+ return { ...account, xai: { ...account.xai, ...enrichment } };
178
+ });
179
+ return changed ? { ...health, accounts } : health;
180
+ }
181
+ function snapshotFromAuthEntry(value, liveSessionCount, nowMs, usedIds) {
182
+ if (value === null || typeof value !== "object" || Array.isArray(value))
183
+ return undefined;
184
+ const entry = value;
185
+ if (entry.auth_mode !== "oidc" && typeof entry.key !== "string")
186
+ return undefined;
187
+ const claims = typeof entry.key === "string" ? decodeJwtPayload(entry.key) : null;
188
+ const id = uniqueAccountId("grok", usedIds);
189
+ const expiresAt = parseExpiresAt(entry.expires_at, claims);
190
+ const expiresInMs = expiresAt > 0 ? expiresAt - nowMs : 0;
191
+ const healthy = expiresInMs > 0;
192
+ const tier = numberClaim(claims, "tier");
193
+ return {
194
+ id,
195
+ provider: "xai_subscription",
196
+ enabled: true,
197
+ healthy,
198
+ busy: liveSessionCount > 0,
199
+ inFlightRequests: 0,
200
+ activeSessions: liveSessionCount,
201
+ requestCount: 0,
202
+ errorCount: 0,
203
+ expiresInMs,
204
+ lastUsedMs: 0,
205
+ lastRefreshMs: 0,
206
+ ...(tier !== undefined ? { tier } : {}),
207
+ };
208
+ }
209
+ function readLiveSessions(sessionsPath, opts) {
210
+ if (!opts.fileExists(sessionsPath))
211
+ return [];
212
+ let raw;
213
+ try {
214
+ raw = JSON.parse(opts.readFile(sessionsPath));
215
+ }
216
+ catch {
217
+ return [];
218
+ }
219
+ if (!Array.isArray(raw))
220
+ return [];
221
+ return raw.filter((row) => {
222
+ if (row === null || typeof row !== "object")
223
+ return false;
224
+ const pid = row.pid;
225
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && opts.isProcessAlive(pid);
226
+ });
227
+ }
228
+ function uniqueAccountId(base, used) {
229
+ if (!used.has(base)) {
230
+ used.add(base);
231
+ return base;
232
+ }
233
+ let n = 2;
234
+ while (used.has(`${base}-${n}`))
235
+ n += 1;
236
+ const id = `${base}-${n}`;
237
+ used.add(id);
238
+ return id;
239
+ }
240
+ function parseExpiresAt(value, claims) {
241
+ if (typeof value === "string" && value.trim()) {
242
+ const parsed = Date.parse(value);
243
+ if (Number.isFinite(parsed) && parsed > 0)
244
+ return parsed;
245
+ }
246
+ const exp = numberClaim(claims, "exp");
247
+ return exp !== undefined && exp > 0 ? exp * 1000 : 0;
248
+ }
249
+ export function grokTierFromAccessToken(accessToken) {
250
+ return numberClaim(decodeJwtPayload(accessToken), "tier");
251
+ }
252
+ function decodeJwtPayload(token) {
253
+ const parts = token.split(".");
254
+ if (parts.length < 2 || !parts[1])
255
+ return null;
256
+ try {
257
+ const json = Buffer.from(parts[1], "base64url").toString("utf-8");
258
+ const parsed = JSON.parse(json);
259
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
260
+ return null;
261
+ return parsed;
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ }
267
+ function numberClaim(claims, key) {
268
+ const value = claims?.[key];
269
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
270
+ }
271
+ function defaultIsProcessAlive(pid) {
272
+ try {
273
+ process.kill(pid, 0);
274
+ return true;
275
+ }
276
+ catch {
277
+ return false;
278
+ }
279
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Live subscription lookup for the Grok dashboard row.
3
+ *
4
+ * The Grok CLI's own backend is `https://cli-chat-proxy.grok.com/v1`. A
5
+ * bearer-only GET on `/v1/user?include=subscription` answers with the account
6
+ * identity plus `subscriptionTier` ("GrokPro", …) and `hasGrokCodeAccess` — the
7
+ * only quota-relevant signals xAI exposes to the OIDC CLI token. The
8
+ * `?include=subscription` query is REQUIRED: the bare `/v1/user` omits
9
+ * `subscriptionTier` entirely (verified live 2026-08-21).
10
+ *
11
+ * Probed 2026-08-21 against the live endpoint: there is NO usage/limit/reset
12
+ * data here. `/v1/usage`, `/v1/rate-limits`, `/v1/quota` and friends all 404;
13
+ * no `x-ratelimit-*` response headers. xAI's weekly usage pool lives only in the
14
+ * web app's Settings → Usage surface behind a different auth context, so the
15
+ * dashboard shows the plan, not a percentage. This is why the Grok row has no
16
+ * Claude-style 5h/7d windows — they do not exist.
17
+ */
18
+ export const GROK_USER_ENDPOINT = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
19
+ export async function fetchGrokSubscription(account, options = {}) {
20
+ const fetchImpl = options.fetch ?? globalThis.fetch;
21
+ let response;
22
+ try {
23
+ response = await fetchImpl(GROK_USER_ENDPOINT, {
24
+ headers: { authorization: `Bearer ${account.accessToken}`, accept: "application/json" },
25
+ signal: AbortSignal.timeout(10_000),
26
+ });
27
+ }
28
+ catch {
29
+ return { ok: false, reason: "network" };
30
+ }
31
+ if (response.status === 401 || response.status === 403)
32
+ return { ok: false, reason: "auth" };
33
+ if (!response.ok)
34
+ return { ok: false, reason: "http" };
35
+ let body;
36
+ try {
37
+ body = await response.json();
38
+ }
39
+ catch {
40
+ return { ok: false, reason: "malformed" };
41
+ }
42
+ return parseGrokUserPayload(body);
43
+ }
44
+ export function parseGrokUserPayload(body) {
45
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
46
+ return { ok: false, reason: "malformed" };
47
+ }
48
+ const record = body;
49
+ const tier = record["subscriptionTier"];
50
+ const codeAccess = record["hasGrokCodeAccess"];
51
+ return {
52
+ ok: true,
53
+ ...(typeof tier === "string" && tier.trim() ? { subscriptionTier: tier.trim() } : {}),
54
+ ...(typeof codeAccess === "boolean" ? { hasCodeAccess: codeAccess } : {}),
55
+ };
56
+ }
@@ -0,0 +1,142 @@
1
+ // The 7-day (weekly) window is the headline constraint for both providers —
2
+ // it is the one that actually forces a provider switch. The 5-hour window
3
+ // resets hourly-scale and is only a secondary, tie-breaking signal. This
4
+ // module is a PURE function over the already-in-memory HealthAccountView[]
5
+ // (no upstream fetch, no I/O) so polling it can never itself rate-limit an
6
+ // account.
7
+ const CONSTRAINED_THRESHOLD = 0.85;
8
+ const NO_WINDOWS = { sevenDayUtil: null, sevenDayResetMs: null, fiveHourUtil: null };
9
+ export function createAllowanceView(views, now) {
10
+ return {
11
+ ts: now,
12
+ providers: {
13
+ anthropic: computeProviderAllowance(views.filter(v => v.provider === "anthropic_subscription"), now, anthropicWindows, isAnthropicRateLimited),
14
+ openai: computeProviderAllowance(views.filter(v => v.provider === "openai_subscription"), now, openAIWindows, isOpenAIRateLimited),
15
+ },
16
+ };
17
+ }
18
+ function isCandidate(view) {
19
+ return view.enabled !== false;
20
+ }
21
+ function isCooling(view, now) {
22
+ return Math.max(view.cooldownUntilMs ?? 0, view.globalCooldownUntilMs ?? 0) > now;
23
+ }
24
+ function isAnthropicRateLimited(view) {
25
+ return view.rateLimits?.status === "rate_limited";
26
+ }
27
+ function isOpenAIRateLimited(view) {
28
+ return view.codexRateLimits?.status === "rate_limited";
29
+ }
30
+ function anthropicWindows(view) {
31
+ const rl = view.rateLimits;
32
+ if (!rl)
33
+ return NO_WINDOWS;
34
+ return {
35
+ sevenDayUtil: typeof rl.sevenDayUtil === "number" ? rl.sevenDayUtil : null,
36
+ sevenDayResetMs: rl.sevenDayReset > 0 ? rl.sevenDayReset * 1000 : null,
37
+ fiveHourUtil: typeof rl.fiveHourUtil === "number" ? rl.fiveHourUtil : null,
38
+ };
39
+ }
40
+ // The default Codex bucket carries two windows: a short one (primary,
41
+ // typically the 5h window) and a long one (secondary, typically the 7d /
42
+ // weekly window). We identify the weekly window by whichever has the LARGER
43
+ // windowMinutes rather than by name, since primary/secondary are positional,
44
+ // not semantic. If only one window is present it is treated as the weekly
45
+ // (7d-primary) signal and there is no secondary 5h reading.
46
+ function openAIWindows(view) {
47
+ const bucket = view.codexRateLimits?.buckets?.[0];
48
+ if (!bucket)
49
+ return NO_WINDOWS;
50
+ const windows = [bucket.primary, bucket.secondary]
51
+ .filter((w) => w != null);
52
+ if (windows.length === 0)
53
+ return NO_WINDOWS;
54
+ const weekly = windows.reduce((best, w) => (w.windowMinutes > best.windowMinutes ? w : best));
55
+ const short = windows.length > 1 ? windows.find(w => w !== weekly) ?? null : null;
56
+ return {
57
+ sevenDayUtil: weekly.utilization,
58
+ sevenDayResetMs: weekly.resetAt > 0 ? weekly.resetAt * 1000 : null,
59
+ fiveHourUtil: short ? short.utilization : null,
60
+ };
61
+ }
62
+ function computeProviderAllowance(views, now, windowsOf, isRateLimited) {
63
+ const candidates = views.filter(isCandidate);
64
+ const available = candidates.filter(v => !isCooling(v, now) && !isRateLimited(v));
65
+ const availableWindows = available.map(windowsOf);
66
+ const sevenDayUtils = availableWindows
67
+ .map(w => w.sevenDayUtil)
68
+ .filter((u) => u != null);
69
+ const fiveHourUtils = availableWindows
70
+ .map(w => w.fiveHourUtil)
71
+ .filter((u) => u != null);
72
+ const worstSevenDayUtil = sevenDayUtils.length > 0 ? Math.max(...sevenDayUtils) : null;
73
+ const bestSevenDayUtil = sevenDayUtils.length > 0 ? Math.min(...sevenDayUtils) : null;
74
+ const sevenDayHeadroom = bestSevenDayUtil != null ? 1 - bestSevenDayUtil : null;
75
+ const sevenDaySpread = worstSevenDayUtil != null && bestSevenDayUtil != null
76
+ ? worstSevenDayUtil - bestSevenDayUtil
77
+ : null;
78
+ const fiveHourUtil = fiveHourUtils.length > 0 ? Math.min(...fiveHourUtils) : null;
79
+ const accountsTotal = candidates.length;
80
+ const accountsAvailable = available.length;
81
+ let status;
82
+ if (accountsAvailable === 0) {
83
+ status = "exhausted";
84
+ }
85
+ else if ((bestSevenDayUtil != null && bestSevenDayUtil >= CONSTRAINED_THRESHOLD)
86
+ || (fiveHourUtil != null && fiveHourUtil >= CONSTRAINED_THRESHOLD)) {
87
+ status = "constrained";
88
+ }
89
+ else {
90
+ status = "ok";
91
+ }
92
+ const earliestResetAt = computeEarliestResetAt({
93
+ status,
94
+ accountsAvailable,
95
+ available,
96
+ candidates,
97
+ windowsOf,
98
+ now,
99
+ });
100
+ const coolingModelFamilies = Array.from(new Set(candidates.flatMap(v => (v.modelCooldowns ?? [])
101
+ .filter(c => c.untilMs > now)
102
+ .map(c => c.modelFamily)))).sort();
103
+ return {
104
+ status,
105
+ accountsTotal,
106
+ accountsAvailable,
107
+ sevenDayUtil: worstSevenDayUtil,
108
+ sevenDayHeadroom,
109
+ sevenDaySpread,
110
+ fiveHourUtil,
111
+ earliestResetAt,
112
+ coolingModelFamilies,
113
+ };
114
+ }
115
+ function computeEarliestResetAt(args) {
116
+ const { status, accountsAvailable, available, candidates, windowsOf, now } = args;
117
+ if (accountsAvailable > 0) {
118
+ if (status !== "constrained")
119
+ return null;
120
+ const resets = available
121
+ .map(windowsOf)
122
+ .map(w => w.sevenDayResetMs)
123
+ .filter((r) => r != null);
124
+ return resets.length > 0 ? Math.min(...resets) : null;
125
+ }
126
+ // Exhausted: the earliest moment capacity could plausibly return, per
127
+ // candidate — whichever cooldown (account or global) is currently
128
+ // enforced, or otherwise its weekly reset if that lies in the future.
129
+ const candidateResets = [];
130
+ for (const view of candidates) {
131
+ const cooldownUntil = Math.max(view.cooldownUntilMs ?? 0, view.globalCooldownUntilMs ?? 0);
132
+ if (cooldownUntil > now) {
133
+ candidateResets.push(cooldownUntil);
134
+ continue;
135
+ }
136
+ const sevenDayResetMs = windowsOf(view).sevenDayResetMs;
137
+ if (sevenDayResetMs != null && sevenDayResetMs > now) {
138
+ candidateResets.push(sevenDayResetMs);
139
+ }
140
+ }
141
+ return candidateResets.length > 0 ? Math.min(...candidateResets) : null;
142
+ }
@@ -1,12 +1,13 @@
1
1
  import chalk from "chalk";
2
2
  function ts() {
3
- return new Date().toISOString().slice(11, 19); // HH:MM:SS
3
+ return new Date().toISOString().slice(0, 19).replace("T", " "); // YYYY-MM-DD HH:MM:SS
4
4
  }
5
- export function logRoute(accountId, requestCount, expiresInMin) {
5
+ export function logRoute(accountId, requestCount, expiresInMin, reason) {
6
6
  console.log(chalk.gray(`[${ts()}]`) +
7
7
  chalk.green(` → ${accountId}`) +
8
8
  chalk.gray(` req#${requestCount}`) +
9
- chalk.yellow(` exp=${expiresInMin}min`));
9
+ chalk.yellow(` exp=${expiresInMin}min`) +
10
+ (reason ? chalk.cyan(` ${reason}`) : ""));
10
11
  }
11
12
  export function logRefresh(accountId, ok, expiresInMin) {
12
13
  if (ok) {