mixdog 0.7.17 → 0.7.18

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 (40) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/UNINSTALL.md +7 -4
  4. package/bin/statusline-lib.mjs +29 -6
  5. package/bin/statusline-route.mjs +273 -0
  6. package/bin/statusline.mjs +31 -8
  7. package/commands/model.md +61 -0
  8. package/hooks/session-start.cjs +15 -1
  9. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  10. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  11. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  12. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  13. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  14. package/package.json +1 -1
  15. package/rules/lead/01-general.md +3 -0
  16. package/scripts/gateway-model.mjs +596 -0
  17. package/scripts/lib/gateway-inventory.mjs +178 -0
  18. package/scripts/lib/gateway-settings.mjs +78 -0
  19. package/scripts/run-mcp.mjs +26 -1
  20. package/scripts/statusline-launcher-smoke.mjs +155 -2
  21. package/scripts/uninstall.mjs +44 -7
  22. package/server-main.mjs +252 -11
  23. package/src/agent/index.mjs +96 -5
  24. package/src/agent/orchestrator/bridge-trace.mjs +18 -0
  25. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +4 -1
  26. package/src/agent/orchestrator/providers/anthropic.mjs +6 -2
  27. package/src/agent/orchestrator/session/loop.mjs +145 -4
  28. package/src/agent/orchestrator/session/trim.mjs +1 -1
  29. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +62 -6
  30. package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
  31. package/src/agent/orchestrator/tools/patch-manifest.json +7 -7
  32. package/src/agent/tool-defs.mjs +10 -3
  33. package/src/channels/lib/runtime-paths.mjs +39 -4
  34. package/src/gateway/claude-current.mjs +255 -0
  35. package/src/gateway/oauth-usage.mjs +598 -0
  36. package/src/gateway/route-meta.mjs +629 -0
  37. package/src/gateway/server.mjs +713 -0
  38. package/src/shared/llm/cost.mjs +1 -1
  39. package/src/shared/seed.mjs +25 -0
  40. package/tools.json +21 -3
@@ -0,0 +1,598 @@
1
+ import {
2
+ closeSync,
3
+ existsSync,
4
+ openSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ readSync,
8
+ statSync,
9
+ } from 'fs';
10
+ import { homedir } from 'os';
11
+ import { join } from 'path';
12
+ import { updateJsonAtomicSync } from '../shared/atomic-file.mjs';
13
+ import { resolvePluginData } from '../shared/plugin-paths.mjs';
14
+ import { getLlmDispatcher } from '../shared/llm/http-agent.mjs';
15
+
16
+ const CACHE_FILE = 'gateway-oauth-usage-cache.json';
17
+ const LIVE_CACHE_TTL_MS = 60_000;
18
+ const DISK_CACHE_TTL_MS = 10 * 60_000;
19
+ const NEGATIVE_CACHE_TTL_MS = 5 * 60_000;
20
+ const FETCH_TIMEOUT_MS = 4500;
21
+ const WARN_TTL_MS = 5 * 60_000;
22
+ const MAX_ROLLOUT_FILES = 8;
23
+ const MAX_ROLLOUT_SCAN_FILES = 2500;
24
+ const MAX_TAIL_BYTES = 2 * 1024 * 1024;
25
+
26
+ const memoryCache = new Map();
27
+ const inflight = new Map();
28
+ const lastWarnAt = new Map();
29
+
30
+ function num(value, fallback = null) {
31
+ if (value === null || value === undefined || value === '') return fallback;
32
+ const n = Number(value);
33
+ return Number.isFinite(n) ? n : fallback;
34
+ }
35
+
36
+ function round(value, digits = 4) {
37
+ const n = Number(value);
38
+ if (!Number.isFinite(n)) return null;
39
+ const scale = 10 ** digits;
40
+ return Math.round(n * scale) / scale;
41
+ }
42
+
43
+ function cleanString(value) {
44
+ const s = typeof value === 'string' ? value.trim() : '';
45
+ return s || null;
46
+ }
47
+
48
+ function providerKey(routeInfo = {}) {
49
+ return String(routeInfo?.provider || '').toLowerCase();
50
+ }
51
+
52
+ function routeKey(routeInfo = {}) {
53
+ return `${providerKey(routeInfo)}\u0001${String(routeInfo?.model || '')}`;
54
+ }
55
+
56
+ function cachePath() {
57
+ return join(resolvePluginData(), CACHE_FILE);
58
+ }
59
+
60
+ function readJsonFile(file) {
61
+ try {
62
+ if (!existsSync(file)) return null;
63
+ return JSON.parse(readFileSync(file, 'utf8'));
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ function writeSnapshotCache(key, snapshot) {
70
+ if (!key || !snapshot) return;
71
+ try {
72
+ updateJsonAtomicSync(cachePath(), (curRaw) => {
73
+ const cur = curRaw && typeof curRaw === 'object' ? curRaw : {};
74
+ const routes = cur.routes && typeof cur.routes === 'object' ? cur.routes : {};
75
+ return {
76
+ version: 1,
77
+ updatedAt: Date.now(),
78
+ routes: {
79
+ ...routes,
80
+ [key]: snapshot,
81
+ },
82
+ };
83
+ }, { compact: true, fsyncDir: true });
84
+ } catch {
85
+ // Usage display must never affect the gateway request path.
86
+ }
87
+ }
88
+
89
+ function isContentfulSnapshot(snapshot) {
90
+ return !!snapshot
91
+ && typeof snapshot === 'object'
92
+ && (
93
+ Array.isArray(snapshot.quotaWindows) && snapshot.quotaWindows.length > 0
94
+ || snapshot.balance && typeof snapshot.balance === 'object'
95
+ );
96
+ }
97
+
98
+ function hasFutureWindow(snapshot) {
99
+ const windows = Array.isArray(snapshot?.quotaWindows) ? snapshot.quotaWindows : [];
100
+ if (!windows.length) return true;
101
+ const now = Date.now();
102
+ return windows.some(w => !w?.resetAt || num(w.resetAt, 0) > now);
103
+ }
104
+
105
+ function freshSnapshot(snapshot, ttlMs) {
106
+ if (!isContentfulSnapshot(snapshot)) return null;
107
+ const at = num(snapshot.cachedAt, 0);
108
+ if (!at || Date.now() - at > ttlMs) return null;
109
+ if (!hasFutureWindow(snapshot)) return null;
110
+ return snapshot;
111
+ }
112
+
113
+ export function readCachedOAuthUsageSnapshot(routeInfo) {
114
+ const key = routeKey(routeInfo);
115
+ const providerOnlyKey = providerKey(routeInfo);
116
+ const mem = freshSnapshot(memoryCache.get(key), LIVE_CACHE_TTL_MS)
117
+ || freshSnapshot(memoryCache.get(providerOnlyKey), LIVE_CACHE_TTL_MS);
118
+ if (mem) return mem;
119
+
120
+ const raw = readJsonFile(cachePath());
121
+ const routes = raw?.routes && typeof raw.routes === 'object' ? raw.routes : {};
122
+ return freshSnapshot(routes[key], DISK_CACHE_TTL_MS)
123
+ || freshSnapshot(routes[providerOnlyKey], DISK_CACHE_TTL_MS)
124
+ || null;
125
+ }
126
+
127
+ function cacheNegative(key, source = 'none') {
128
+ if (!key) return;
129
+ memoryCache.set(key, { empty: true, source, cachedAt: Date.now() });
130
+ }
131
+
132
+ function negativeFresh(key) {
133
+ const cur = memoryCache.get(key);
134
+ return !!cur?.empty && Date.now() - num(cur.cachedAt, 0) < NEGATIVE_CACHE_TTL_MS;
135
+ }
136
+
137
+ function warnThrottled(log, key, message) {
138
+ const now = Date.now();
139
+ const last = num(lastWarnAt.get(key), 0);
140
+ if (last && now - last < WARN_TTL_MS) return;
141
+ lastWarnAt.set(key, now);
142
+ try { log(message); } catch {}
143
+ }
144
+
145
+ function fetchOptions(headers, timeoutMs = FETCH_TIMEOUT_MS) {
146
+ let dispatcher = null;
147
+ try { dispatcher = getLlmDispatcher(); } catch {}
148
+ return {
149
+ method: 'GET',
150
+ headers,
151
+ signal: AbortSignal.timeout(timeoutMs),
152
+ redirect: 'error',
153
+ ...(dispatcher ? { dispatcher } : {}),
154
+ };
155
+ }
156
+
157
+ function resetAtMs(value, fallbackSeconds = null) {
158
+ if (typeof value === 'string') {
159
+ const iso = Date.parse(value);
160
+ if (Number.isFinite(iso)) return iso;
161
+ }
162
+ const n = num(value, 0);
163
+ if (n > 0) return n < 10_000_000_000 ? n * 1000 : n;
164
+ const secs = num(fallbackSeconds, 0);
165
+ return secs > 0 ? Date.now() + secs * 1000 : null;
166
+ }
167
+
168
+ function labelForDuration(seconds, fallback) {
169
+ const s = num(seconds, 0);
170
+ if (s > 0) {
171
+ if (Math.abs(s - 5 * 60 * 60) <= 120) return '5H';
172
+ if (Math.abs(s - 7 * 24 * 60 * 60) <= 3600) return '7D';
173
+ if (Math.abs(s - 24 * 60 * 60) <= 600) return '24H';
174
+ if (s >= 28 * 24 * 60 * 60 && s <= 31 * 24 * 60 * 60) return 'M';
175
+ }
176
+ return fallback;
177
+ }
178
+
179
+ function windowFromPercent(label, value, source) {
180
+ const entry = value && typeof value === 'object' ? value : {};
181
+ const windowSeconds = num(entry.limit_window_seconds ?? entry.window_seconds ?? entry.windowSeconds, 0);
182
+ const windowMinutes = num(entry.window_minutes ?? entry.windowMinutes, 0);
183
+ const usedPct = num(
184
+ entry.usedPct
185
+ ?? entry.used_percent
186
+ ?? entry.used_percentage
187
+ ?? entry.utilization
188
+ ?? entry.percent,
189
+ null,
190
+ );
191
+ const limitUsd = num(entry.limitUsd ?? entry.limit_usd ?? entry.limit_dollars ?? entry.monthly_limit, null);
192
+ const usedUsd = num(entry.usedUsd ?? entry.used_usd ?? entry.used_dollars ?? entry.used_credits, null);
193
+ const remainingUsd = num(entry.remainingUsd ?? entry.remaining_usd ?? entry.remaining_dollars, null);
194
+ const resetAt = resetAtMs(
195
+ entry.resetAt ?? entry.resetsAt ?? entry.reset_at ?? entry.resets_at,
196
+ entry.reset_after_seconds,
197
+ );
198
+ const out = {
199
+ label: labelForDuration(windowSeconds || windowMinutes * 60, label),
200
+ source,
201
+ };
202
+ if (usedPct !== null) out.usedPct = round(usedPct, 2);
203
+ if (limitUsd !== null) out.limitUsd = round(limitUsd, 4);
204
+ if (usedUsd !== null) out.usedUsd = round(usedUsd, 4);
205
+ if (remainingUsd !== null) out.remainingUsd = round(remainingUsd, 4);
206
+ if (resetAt) out.resetAt = resetAt;
207
+ return Object.keys(out).length > 2 ? out : null;
208
+ }
209
+
210
+ function creditWindowFromBilling(config) {
211
+ if (!config || typeof config !== 'object') return null;
212
+ const limit = num(config.monthlyLimit?.val ?? config.monthlyLimit ?? config.includedLimit?.val, null);
213
+ const used = num(config.used?.val ?? config.includedUsed?.val ?? config.used, null);
214
+ if (limit === null || used === null || !(limit > 0)) return null;
215
+ const resetAt = resetAtMs(config.billingPeriodEnd ?? config.billing_period_end);
216
+ return {
217
+ label: 'M',
218
+ source: 'grok-build-billing',
219
+ usedPct: round(Math.min(100, used * 100 / limit), 2),
220
+ usedCredits: round(used, 2),
221
+ limitCredits: round(limit, 2),
222
+ remainingCredits: round(Math.max(0, limit - used), 2),
223
+ ...(resetAt ? { resetAt } : {}),
224
+ };
225
+ }
226
+
227
+ function balanceFromGrokBilling(config) {
228
+ if (!config || typeof config !== 'object') return null;
229
+ const cap = num(config.onDemandCap?.val ?? config.onDemandCap, null);
230
+ const used = num(config.onDemandUsed?.val ?? config.onDemandUsed, 0);
231
+ if (cap === null || !(cap > 0)) return null;
232
+ return {
233
+ source: 'grok-build-on-demand',
234
+ period: 'monthly',
235
+ budgetCredits: round(cap, 2),
236
+ spentCredits: round(used, 2),
237
+ remainingCredits: round(Math.max(0, cap - used), 2),
238
+ };
239
+ }
240
+
241
+ function balanceFromCredits(credits, source) {
242
+ if (!credits || typeof credits !== 'object') return null;
243
+ const balance = num(credits.balance ?? credits.remaining ?? credits.remaining_dollars, null);
244
+ if (balance === null) return null;
245
+ if (credits.has_credits === false && balance <= 0) return null;
246
+ return {
247
+ source,
248
+ remainingUsd: round(balance, 4),
249
+ currency: cleanString(credits.currency) || 'USD',
250
+ };
251
+ }
252
+
253
+ function balanceFromExtraUsage(extra) {
254
+ if (!extra || typeof extra !== 'object' || extra.is_enabled !== true) return null;
255
+ const limit = num(extra.monthly_limit, null);
256
+ const used = num(extra.used_credits, 0);
257
+ if (limit === null) return null;
258
+ return {
259
+ source: 'anthropic-oauth-extra',
260
+ period: 'monthly',
261
+ budgetUsd: round(limit, 4),
262
+ spentUsd: round(used, 4),
263
+ remainingUsd: round(Math.max(0, limit - used), 4),
264
+ currency: cleanString(extra.currency) || 'USD',
265
+ };
266
+ }
267
+
268
+ function normalizeOpenAIWhamUsage(data) {
269
+ const rate = data?.rate_limit && typeof data.rate_limit === 'object' ? data.rate_limit : null;
270
+ if (!rate) return null;
271
+ const windows = [
272
+ windowFromPercent('5H', rate.primary_window || rate.primary, 'openai-codex-oauth'),
273
+ windowFromPercent('7D', rate.secondary_window || rate.secondary, 'openai-codex-oauth'),
274
+ ].filter(Boolean);
275
+ if (!windows.length) return null;
276
+ return {
277
+ provider: 'openai-oauth',
278
+ source: 'openai-codex-wham',
279
+ planType: cleanString(data.plan_type),
280
+ quotaWindows: windows,
281
+ balance: balanceFromCredits(data.credits, 'openai-codex-credits'),
282
+ rawKeys: Object.keys(data || {}).sort(),
283
+ };
284
+ }
285
+
286
+ function normalizeCodexRateLimits(rateLimits, source = 'openai-codex-local') {
287
+ if (!rateLimits || typeof rateLimits !== 'object') return null;
288
+ const windows = [
289
+ windowFromPercent('5H', rateLimits.primary || rateLimits.five_hour, source),
290
+ windowFromPercent('7D', rateLimits.secondary || rateLimits.seven_day, source),
291
+ ].filter(Boolean);
292
+ if (!windows.length) return null;
293
+ return {
294
+ provider: 'openai-oauth',
295
+ source,
296
+ planType: cleanString(rateLimits.plan_type),
297
+ quotaWindows: windows,
298
+ balance: balanceFromCredits(rateLimits.credits, `${source}-credits`),
299
+ };
300
+ }
301
+
302
+ function normalizeAnthropicUsage(data, source = 'anthropic-oauth') {
303
+ if (!data || typeof data !== 'object') return null;
304
+ let windows = [
305
+ windowFromPercent('5H', data.five_hour, source),
306
+ windowFromPercent('7D', data.seven_day, source),
307
+ ].filter(Boolean);
308
+
309
+ if (!windows.length && Array.isArray(data.limits)) {
310
+ windows = data.limits
311
+ .filter(x => x && x.is_active !== false)
312
+ .map((x) => {
313
+ const label = x.kind === 'session' || x.group === 'session'
314
+ ? '5H'
315
+ : x.kind === 'weekly_all' || x.group === 'weekly'
316
+ ? '7D'
317
+ : String(x.kind || x.group || 'USE').toUpperCase();
318
+ return windowFromPercent(label, {
319
+ percent: x.percent,
320
+ resets_at: x.resets_at,
321
+ }, source);
322
+ })
323
+ .filter(Boolean);
324
+ }
325
+
326
+ const extra = data.extra_usage && data.extra_usage.is_enabled === true
327
+ ? windowFromPercent('EXTRA', {
328
+ utilization: data.extra_usage.utilization,
329
+ limit_dollars: data.extra_usage.monthly_limit,
330
+ used_dollars: data.extra_usage.used_credits,
331
+ remaining_dollars: Math.max(0, num(data.extra_usage.monthly_limit, 0) - num(data.extra_usage.used_credits, 0)),
332
+ }, 'anthropic-oauth-extra')
333
+ : null;
334
+ if (extra) windows.push(extra);
335
+
336
+ if (!windows.length && !data.extra_usage) return null;
337
+ return {
338
+ provider: 'anthropic-oauth',
339
+ source,
340
+ quotaWindows: windows,
341
+ balance: balanceFromExtraUsage(data.extra_usage),
342
+ rawKeys: Object.keys(data || {}).sort(),
343
+ };
344
+ }
345
+
346
+ function parseStatuslineRateLimits(raw, source) {
347
+ if (!raw || typeof raw !== 'object') return null;
348
+ const rateLimits = raw.rate_limits || raw.rateLimits;
349
+ if (!rateLimits || typeof rateLimits !== 'object') return null;
350
+ const windows = [
351
+ windowFromPercent('5H', rateLimits.five_hour || rateLimits.primary, source),
352
+ windowFromPercent('7D', rateLimits.seven_day || rateLimits.secondary, source),
353
+ ].filter(Boolean);
354
+ if (!windows.length) return null;
355
+ return { quotaWindows: windows, source };
356
+ }
357
+
358
+ function tailFileText(file, maxBytes = MAX_TAIL_BYTES) {
359
+ let fd = null;
360
+ try {
361
+ const st = statSync(file);
362
+ const size = st.size || 0;
363
+ const start = Math.max(0, size - maxBytes);
364
+ const len = size - start;
365
+ const buf = Buffer.alloc(len);
366
+ fd = openSync(file, 'r');
367
+ readSync(fd, buf, 0, len, start);
368
+ return buf.toString('utf8');
369
+ } catch {
370
+ return '';
371
+ } finally {
372
+ if (fd != null) {
373
+ try { closeSync(fd); } catch {}
374
+ }
375
+ }
376
+ }
377
+
378
+ function newestRolloutFiles(root) {
379
+ const out = [];
380
+ const stack = [root];
381
+ let seen = 0;
382
+ while (stack.length && seen < MAX_ROLLOUT_SCAN_FILES) {
383
+ const dir = stack.pop();
384
+ let entries = [];
385
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
386
+ for (const ent of entries) {
387
+ if (seen++ >= MAX_ROLLOUT_SCAN_FILES) break;
388
+ const full = join(dir, ent.name);
389
+ if (ent.isDirectory()) {
390
+ stack.push(full);
391
+ } else if (ent.isFile() && /^rollout-.*\.jsonl$/i.test(ent.name)) {
392
+ try {
393
+ const st = statSync(full);
394
+ out.push({ file: full, mtimeMs: st.mtimeMs || 0 });
395
+ } catch {}
396
+ }
397
+ }
398
+ }
399
+ return out
400
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
401
+ .slice(0, MAX_ROLLOUT_FILES)
402
+ .map(x => x.file);
403
+ }
404
+
405
+ function latestCodexRateLimitsFromRollout() {
406
+ const root = join(homedir(), '.codex', 'sessions');
407
+ if (!existsSync(root)) return null;
408
+ for (const file of newestRolloutFiles(root)) {
409
+ const lines = tailFileText(file).split(/\r?\n/).filter(Boolean);
410
+ for (let i = lines.length - 1; i >= 0; i--) {
411
+ try {
412
+ const row = JSON.parse(lines[i]);
413
+ const rateLimits = row?.payload?.rate_limits || row?.rate_limits;
414
+ const snap = normalizeCodexRateLimits(rateLimits, 'openai-codex-rollout');
415
+ if (snap && hasFutureWindow(snap)) return snap;
416
+ } catch {}
417
+ }
418
+ }
419
+ return null;
420
+ }
421
+
422
+ function latestClaudeStatuslineUsage() {
423
+ const file = join(homedir(), '.claude', 'cc-statusline-last.json');
424
+ const raw = readJsonFile(file);
425
+ const parsed = parseStatuslineRateLimits(raw, 'anthropic-oauth-statusline');
426
+ if (!parsed || !hasFutureWindow(parsed)) return null;
427
+ return {
428
+ provider: 'anthropic-oauth',
429
+ source: parsed.source,
430
+ quotaWindows: parsed.quotaWindows,
431
+ };
432
+ }
433
+
434
+ function grokCliIdentity(accessToken = '') {
435
+ const raw = readJsonFile(join(homedir(), '.grok', 'auth.json'));
436
+ if (!raw || typeof raw !== 'object') return {};
437
+ const entries = Object.values(raw).filter(v => v && typeof v === 'object');
438
+ const exact = accessToken ? entries.find(v => v.key === accessToken || v.access_token === accessToken) : null;
439
+ const entry = exact || entries.find(v => v.key || v.access_token) || null;
440
+ if (!entry) return {};
441
+ return {
442
+ userId: entry.user_id || entry.userId || entry.principal_id || entry.principalId || '',
443
+ token: entry.key || entry.access_token || entry.accessToken || '',
444
+ };
445
+ }
446
+
447
+ async function fetchOpenAICodexUsage(providerObj) {
448
+ const auth = await providerObj?.ensureAuth?.({ reason: 'usage' });
449
+ const token = auth?.access_token || auth?.accessToken;
450
+ if (!token) return latestCodexRateLimitsFromRollout();
451
+ const res = await fetch('https://chatgpt.com/backend-api/wham/usage', fetchOptions({
452
+ Authorization: `Bearer ${token}`,
453
+ originator: 'codex_cli_rs',
454
+ 'chatgpt-account-id': auth.account_id || auth.accountId || '',
455
+ 'OpenAI-Beta': 'responses=experimental',
456
+ Accept: 'application/json',
457
+ }));
458
+ if (!res.ok) throw new Error(`codex usage ${res.status}`);
459
+ const data = await res.json();
460
+ return normalizeOpenAIWhamUsage(data) || latestCodexRateLimitsFromRollout();
461
+ }
462
+
463
+ async function fetchAnthropicUsage(providerObj) {
464
+ const auth = await providerObj?.ensureAuth?.({ reason: 'usage' });
465
+ const token = auth?.accessToken || auth?.access_token;
466
+ if (!token) return latestClaudeStatuslineUsage();
467
+ const res = await fetch('https://api.anthropic.com/api/oauth/usage', fetchOptions({
468
+ Authorization: `Bearer ${token}`,
469
+ 'anthropic-beta': 'oauth-2025-04-20',
470
+ 'User-Agent': 'claude-code/2.0.0',
471
+ 'Content-Type': 'application/json',
472
+ Accept: 'application/json',
473
+ }));
474
+ if (!res.ok) throw new Error(`anthropic oauth usage ${res.status}`);
475
+ const data = await res.json();
476
+ return normalizeAnthropicUsage(data) || latestClaudeStatuslineUsage();
477
+ }
478
+
479
+ async function fetchGrokUsage(providerObj, routeInfo) {
480
+ const auth = await providerObj?.ensureAuth?.({ reason: 'usage' });
481
+ const token = auth?.access_token || auth?.accessToken || auth?.key;
482
+ if (!token) return null;
483
+ const cliIdentity = grokCliIdentity(token);
484
+ const userId = auth?.user_id || auth?.userId || auth?.principal_id || auth?.principalId || cliIdentity.userId || '';
485
+ const cliHeaders = {
486
+ Authorization: `Bearer ${token}`,
487
+ 'X-XAI-Token-Auth': 'xai-grok-cli',
488
+ 'x-userid': userId,
489
+ Accept: 'application/json',
490
+ 'User-Agent': 'xai-grok-build/0.2.16',
491
+ };
492
+
493
+ try {
494
+ const res = await fetch('https://cli-chat-proxy.grok.com/v1/billing', fetchOptions(cliHeaders));
495
+ if (res.ok) {
496
+ const data = await res.json();
497
+ const config = data?.config && typeof data.config === 'object' ? data.config : data;
498
+ const monthly = creditWindowFromBilling(config);
499
+ if (monthly) {
500
+ return {
501
+ provider: routeInfo?.provider || 'grok-oauth',
502
+ model: routeInfo?.model || null,
503
+ source: 'grok-build-billing',
504
+ quotaWindows: [monthly],
505
+ balance: balanceFromGrokBilling(config),
506
+ rawKeys: Object.keys(data || {}).sort(),
507
+ };
508
+ }
509
+ }
510
+ } catch {
511
+ // Fall through to generic probes below.
512
+ }
513
+
514
+ // xAI documents per-request cost tracking and console rate-limit pages, but
515
+ // the stable Grok Build quota is currently on the CLI proxy /billing route.
516
+ // Probe conservative generic candidates too so a future API addition starts
517
+ // working without changing the statusline contract.
518
+ const headers = {
519
+ Authorization: `Bearer ${token}`,
520
+ 'X-XAI-Token-Auth': 'xai-grok-cli',
521
+ 'x-userid': userId,
522
+ Accept: 'application/json',
523
+ 'User-Agent': 'xai-grok-build/mixdog',
524
+ };
525
+ const urls = [
526
+ 'https://cli-chat-proxy.grok.com/v1/billing',
527
+ 'https://api.x.ai/v1/usage',
528
+ 'https://api.x.ai/v1/rate_limits',
529
+ 'https://api.x.ai/v1/rate-limits',
530
+ 'https://cli-chat-proxy.grok.com/v1/usage',
531
+ 'https://cli-chat-proxy.grok.com/v1/rate_limits',
532
+ ];
533
+ for (const url of urls) {
534
+ try {
535
+ const res = await fetch(url, fetchOptions(headers, 2500));
536
+ if (!res.ok) continue;
537
+ const data = await res.json();
538
+ const parsed = normalizeCodexRateLimits(data?.rate_limits || data?.rateLimits, 'grok-oauth')
539
+ || normalizeAnthropicUsage(data, 'grok-oauth')
540
+ || null;
541
+ if (parsed) return { ...parsed, provider: routeInfo?.provider || 'grok-oauth' };
542
+ } catch {}
543
+ }
544
+ return null;
545
+ }
546
+
547
+ export async function fetchOAuthUsageSnapshot(routeInfo, providerObj, log = () => {}) {
548
+ const provider = providerKey(routeInfo);
549
+ if (!provider.includes('oauth')) return null;
550
+ const key = routeKey(routeInfo);
551
+ const providerOnly = providerKey(routeInfo);
552
+ const cached = freshSnapshot(memoryCache.get(key), LIVE_CACHE_TTL_MS)
553
+ || freshSnapshot(memoryCache.get(providerOnly), LIVE_CACHE_TTL_MS);
554
+ if (cached) return cached;
555
+ if (negativeFresh(key) || negativeFresh(providerOnly)) return null;
556
+ if (inflight.has(key)) return inflight.get(key);
557
+
558
+ const task = (async () => {
559
+ let snapshot = null;
560
+ try {
561
+ if (provider === 'openai-oauth') {
562
+ snapshot = await fetchOpenAICodexUsage(providerObj);
563
+ } else if (provider === 'anthropic-oauth') {
564
+ snapshot = await fetchAnthropicUsage(providerObj);
565
+ } else if (provider === 'grok-oauth') {
566
+ snapshot = await fetchGrokUsage(providerObj, routeInfo);
567
+ }
568
+ } catch (err) {
569
+ if (provider === 'openai-oauth') snapshot = latestCodexRateLimitsFromRollout();
570
+ if (provider === 'anthropic-oauth') snapshot = latestClaudeStatuslineUsage();
571
+ if (!snapshot) {
572
+ warnThrottled(log, `oauth-usage:${provider}`, `gateway ${provider} usage fetch unavailable: ${err?.message || err}`);
573
+ }
574
+ }
575
+
576
+ if (!isContentfulSnapshot(snapshot)) {
577
+ cacheNegative(key, 'empty');
578
+ cacheNegative(providerOnly, 'empty');
579
+ return null;
580
+ }
581
+
582
+ const normalized = {
583
+ ...snapshot,
584
+ provider: routeInfo?.provider || snapshot.provider || provider,
585
+ model: routeInfo?.model || snapshot.model || null,
586
+ cachedAt: Date.now(),
587
+ };
588
+ memoryCache.set(key, normalized);
589
+ memoryCache.set(providerOnly, normalized);
590
+ writeSnapshotCache(key, normalized);
591
+ writeSnapshotCache(providerOnly, normalized);
592
+ return normalized;
593
+ })().finally(() => {
594
+ inflight.delete(key);
595
+ });
596
+ inflight.set(key, task);
597
+ return task;
598
+ }