maxpool 1.5.21 → 1.5.23
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/package.json +2 -2
- package/src/oauth.js +83 -5
- package/src/server.js +93 -11
- package/src/tui.js +45 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maxpool",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.23",
|
|
4
4
|
"description": "Multi-account Claude Code proxy with adaptive, rate-aware load balancing across Claude accounts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"url": "https://github.com/2solarmax/maxpool/issues"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
40
|
+
"node": ">=20.3.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/oauth.js
CHANGED
|
@@ -268,14 +268,92 @@ export function classifyZaiLimit(l, now = Date.now()) {
|
|
|
268
268
|
return { bucket, utilization, resetAt };
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
// Kimi Coding Plan usage — the coding key (sk-kimi-…) reads its own quota at
|
|
272
|
+
// <base>/v1/usages (the community `kimi-code-usage` tool's endpoint). NOTE the
|
|
273
|
+
// User-Agent: the endpoint is served by the Kimi Code platform, not the Open
|
|
274
|
+
// Platform (whose /users/me/balance 401s a coding key). Best-effort, non-load-
|
|
275
|
+
// bearing UA — the failure path degrades to last-known + the staleness marker.
|
|
276
|
+
const KIMI_USAGE_UA = 'KimiCLI/1.6';
|
|
277
|
+
|
|
278
|
+
/** ms of one Kimi limit `window`, normalized from its timeUnit so the SHORTEST
|
|
279
|
+
* window (the 5h session) is picked regardless of unit. null when unknown. */
|
|
280
|
+
export function kimiWindowMs(window) {
|
|
281
|
+
const dur = Number(window?.duration);
|
|
282
|
+
if (!Number.isFinite(dur) || dur <= 0) return null;
|
|
283
|
+
const unit = String(window?.timeUnit || window?.time_unit || '').toUpperCase();
|
|
284
|
+
const mult = unit.includes('SECOND') ? 1e3
|
|
285
|
+
: unit.includes('MINUTE') ? 60e3
|
|
286
|
+
: unit.includes('HOUR') ? 3600e3
|
|
287
|
+
: unit.includes('DAY') ? 86400e3
|
|
288
|
+
: unit.includes('WEEK') ? 7 * 86400e3
|
|
289
|
+
: unit.includes('MONTH') ? 30 * 86400e3
|
|
290
|
+
: null;
|
|
291
|
+
return mult == null ? null : dur * mult;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Normalize a Kimi usage row {limit,used,remaining,resetTime} (values are STRINGS)
|
|
295
|
+
* to { utilization, resetAt }. CRITICAL: utilization is `null` — never a computed
|
|
296
|
+
* NaN — when the inputs are absent or limit<=0, because clamp01(NaN)=0 downstream
|
|
297
|
+
* would render a blind account as "0% used = fully available" and route real
|
|
298
|
+
* traffic onto quota we can't see. */
|
|
299
|
+
export function parseKimiRow(d) {
|
|
300
|
+
if (!d || typeof d !== 'object') return null;
|
|
301
|
+
const limit = Number(d.limit ?? d.limit_amount);
|
|
302
|
+
let used = d.used != null ? Number(d.used ?? d.used_amount) : null;
|
|
303
|
+
const remaining = d.remaining != null ? Number(d.remaining) : null;
|
|
304
|
+
if (used == null && remaining != null && Number.isFinite(limit)) used = limit - remaining;
|
|
305
|
+
const utilization = (Number.isFinite(limit) && limit > 0 && used != null && Number.isFinite(used))
|
|
306
|
+
? Math.max(0, Math.min(1, used / limit))
|
|
307
|
+
: null;
|
|
308
|
+
const resetRaw = d.resetTime || d.reset_at || d.reset_time;
|
|
309
|
+
const parsed = resetRaw ? Date.parse(resetRaw) : NaN;
|
|
310
|
+
return { utilization, resetAt: Number.isFinite(parsed) ? parsed : null };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Read the Kimi Coding Plan quota → { ses, wk, source:'kimi' } | { error, status? }.
|
|
314
|
+
* Weekly = the top-level `usage` rolling window; Ses (5h) = the shortest `limits[]`
|
|
315
|
+
* window. A transient/partial response returns {error} or null buckets so
|
|
316
|
+
* applyProviderUsage keeps the last-known values (never a phantom 0%). */
|
|
317
|
+
export async function fetchKimiUsage(account) {
|
|
318
|
+
const token = account?.credential;
|
|
319
|
+
if (!token) return { error: 'unsupported', source: null };
|
|
320
|
+
const base = String(account.upstream || 'https://api.kimi.com/coding').replace(/\/+$/, '');
|
|
321
|
+
const headers = { 'Authorization': `Bearer ${token}`, 'User-Agent': KIMI_USAGE_UA, 'Accept': 'application/json' };
|
|
322
|
+
try {
|
|
323
|
+
let res = await fetch(`${base}/v1/usages`, { headers, signal: AbortSignal.timeout(10_000) });
|
|
324
|
+
if (res.status === 404) res = await fetch(`${base}/v1/usage`, { headers, signal: AbortSignal.timeout(10_000) });
|
|
325
|
+
if (!res.ok) return { error: `HTTP ${res.status}`, status: res.status };
|
|
326
|
+
const data = await res.json();
|
|
327
|
+
// Weekly = top-level `usage` (ALWAYS present in a valid coding-plan response).
|
|
328
|
+
// If it's missing, the response is partial/malformed → return an error so
|
|
329
|
+
// applyProviderUsage keeps the last-known weekly instead of the falsy-wk branch
|
|
330
|
+
// BLANKING it (unlike z.ai, where a missing weekly genuinely means "no weekly").
|
|
331
|
+
const wkRow = parseKimiRow(data?.usage);
|
|
332
|
+
if (!wkRow || wkRow.utilization == null) return { error: 'partial_response', source: 'kimi' };
|
|
333
|
+
const wk = wkRow;
|
|
334
|
+
// Ses = the shortest limits[] window (the 5h). Fall back to first if durations unknown.
|
|
335
|
+
let ses = null, sesMs = Infinity;
|
|
336
|
+
for (const l of (Array.isArray(data?.limits) ? data.limits : [])) {
|
|
337
|
+
const detail = l && typeof l.detail === 'object' ? l.detail : l;
|
|
338
|
+
const row = parseKimiRow(detail);
|
|
339
|
+
if (!row || row.utilization == null) continue;
|
|
340
|
+
const ms = kimiWindowMs(l?.window);
|
|
341
|
+
if (ms != null && ms < sesMs) { sesMs = ms; ses = row; }
|
|
342
|
+
else if (ses == null && ms == null) ses = row;
|
|
343
|
+
}
|
|
344
|
+
return { ses, wk, source: 'kimi', level: data?.user?.membership?.level || null };
|
|
345
|
+
} catch (err) {
|
|
346
|
+
return { error: err.message || String(err), status: null };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Read a provider account's quota. z.ai and Kimi both have a pollable coding-plan
|
|
351
|
+
* usage endpoint mapped to Ses/Wk windows. Returns { ses, wk, source } |
|
|
352
|
+
* { error, status?, source? }. */
|
|
275
353
|
export async function fetchProviderUsage(account) {
|
|
276
354
|
const provider = account?.provider;
|
|
277
355
|
const token = account?.credential;
|
|
278
|
-
if (provider === 'kimi') return
|
|
356
|
+
if (provider === 'kimi') return fetchKimiUsage(account);
|
|
279
357
|
if (provider !== 'zai' || !token) return { error: 'unsupported', source: null };
|
|
280
358
|
try {
|
|
281
359
|
const res = await fetch(ZAI_QUOTA_URL, {
|
package/src/server.js
CHANGED
|
@@ -15,6 +15,16 @@ const DEFAULT_RETRY = {
|
|
|
15
15
|
maxRetryBufferBytes: 10 * 1024 * 1024,
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// Upstream-hang guards. A half-open upstream (headers then silence, never closes)
|
|
19
|
+
// would block the request forever and LEAK its in-flight lease — the account climbs
|
|
20
|
+
// to safetyMaxActivePerAccount and is falsely "full", breaking routing (the "106
|
|
21
|
+
// phantom active" incident). Env-overridable with conservative defaults: this path
|
|
22
|
+
// serves EVERY provider (Anthropic/GLM/Kimi), so the idle gap is generous enough
|
|
23
|
+
// that a legitimately-slow-but-alive stream is never cut (each chunk resets it).
|
|
24
|
+
const UPSTREAM_TTFB_MS = Math.max(5_000, Number(process.env.MAXPOOL_TTFB_MS) || 120_000); // headers must arrive within this
|
|
25
|
+
const STREAM_IDLE_MS = Math.max(30_000, Number(process.env.MAXPOOL_STREAM_IDLE_MS) || 300_000); // max gap BETWEEN streamed chunks (reset per chunk)
|
|
26
|
+
const UPSTREAM_BODY_MS = Math.max(30_000, Number(process.env.MAXPOOL_BODY_MS) || 300_000); // non-streaming body read
|
|
27
|
+
|
|
18
28
|
const DEFAULT_QUEUE = {
|
|
19
29
|
enabled: true,
|
|
20
30
|
maxWaitMs: 24 * 60 * 60 * 1000,
|
|
@@ -429,14 +439,31 @@ async function forwardRequest(
|
|
|
429
439
|
}
|
|
430
440
|
}
|
|
431
441
|
|
|
442
|
+
// TTFB guard: an upstream that accepts the connection but never sends response
|
|
443
|
+
// headers would hang `await fetch` forever (signal only covers client-disconnect)
|
|
444
|
+
// and leak the lease. Abort via a DEDICATED controller (so the caller can tell a
|
|
445
|
+
// TTFB timeout apart from a client-gone abort) and CLEAR it the moment headers
|
|
446
|
+
// arrive — the streaming body is then governed by the per-chunk idle guard, never
|
|
447
|
+
// this fixed clock, so a long healthy stream is never cut.
|
|
448
|
+
const ttfbController = new AbortController();
|
|
449
|
+
const ttfbTimer = setTimeout(
|
|
450
|
+
() => ttfbController.abort(Object.assign(new Error('upstream TTFB timeout'), { code: 'UPSTREAM_TTFB' })),
|
|
451
|
+
UPSTREAM_TTFB_MS,
|
|
452
|
+
);
|
|
453
|
+
ttfbTimer.unref?.();
|
|
432
454
|
try {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
455
|
+
let upstreamRes;
|
|
456
|
+
try {
|
|
457
|
+
upstreamRes = await fetch(upstreamUrl, {
|
|
458
|
+
method,
|
|
459
|
+
headers,
|
|
460
|
+
body: ['GET', 'HEAD'].includes(method) ? undefined : upstreamBody,
|
|
461
|
+
redirect: 'manual',
|
|
462
|
+
signal: AbortSignal.any([clientGone.signal, ttfbController.signal]),
|
|
463
|
+
});
|
|
464
|
+
} finally {
|
|
465
|
+
clearTimeout(ttfbTimer);
|
|
466
|
+
}
|
|
440
467
|
// Response arrived — the pre-response leak window is over. Stop guarding for
|
|
441
468
|
// client-disconnect via abort (streamResponse handles mid-stream disconnects).
|
|
442
469
|
res.off('close', onClientClose);
|
|
@@ -840,7 +867,26 @@ async function forwardRequest(
|
|
|
840
867
|
writeRequestLog(logDir, reqId, logSections);
|
|
841
868
|
}
|
|
842
869
|
} else {
|
|
843
|
-
|
|
870
|
+
// Bound the non-streaming body read so a mid-body upstream stall can't hang
|
|
871
|
+
// the request forever and leak the lease (same class as the streaming idle
|
|
872
|
+
// guard). On timeout → UPSTREAM_BODY → the caller frees the lease.
|
|
873
|
+
const bodyP = upstreamRes.arrayBuffer();
|
|
874
|
+
bodyP.catch(() => {});
|
|
875
|
+
let bodyTimer;
|
|
876
|
+
const bodyTimeout = new Promise((_, reject) => {
|
|
877
|
+
bodyTimer = setTimeout(
|
|
878
|
+
() => reject(Object.assign(new Error('upstream body timeout'), { code: 'UPSTREAM_BODY' })),
|
|
879
|
+
UPSTREAM_BODY_MS,
|
|
880
|
+
);
|
|
881
|
+
bodyTimer.unref?.();
|
|
882
|
+
});
|
|
883
|
+
let arr;
|
|
884
|
+
try {
|
|
885
|
+
arr = await Promise.race([bodyP, bodyTimeout]);
|
|
886
|
+
} finally {
|
|
887
|
+
clearTimeout(bodyTimer);
|
|
888
|
+
}
|
|
889
|
+
const buf = Buffer.from(arr);
|
|
844
890
|
extractUsageFromBody(buf, account.index, accountManager);
|
|
845
891
|
markThinkingFromResponse(buf, accountManager, requestInfo);
|
|
846
892
|
accountManager.releaseAccount(lease, { success: upstreamRes.status < 500, status: upstreamRes.status });
|
|
@@ -896,6 +942,11 @@ async function forwardRequest(
|
|
|
896
942
|
(err.message.includes('fetch failed') ||
|
|
897
943
|
err.code === 'ECONNRESET' || err.code === 'ECONNREFUSED' ||
|
|
898
944
|
err.code === 'ETIMEDOUT' || err.code === 'UND_ERR_CONNECT_TIMEOUT' ||
|
|
945
|
+
// Upstream-hang guards: a stalled connection is network-class, not the
|
|
946
|
+
// account's fault → 5s cooldown + release + (pre-headers) retry elsewhere;
|
|
947
|
+
// once committed, isTransient falls through to sendErrorResponse which ends
|
|
948
|
+
// the client's SSE cleanly (frees the lease AND unhangs the client).
|
|
949
|
+
err.code === 'UPSTREAM_TTFB' || err.code === 'UPSTREAM_IDLE' || err.code === 'UPSTREAM_BODY' ||
|
|
899
950
|
err.message.includes('terminated'));
|
|
900
951
|
|
|
901
952
|
if (isTransient) {
|
|
@@ -1045,7 +1096,7 @@ function unavailableMessage(accountManager, requestInfo = {}, retryAfter, willRe
|
|
|
1045
1096
|
return `All ${n} accounts exhausted. Retry in ${retryAfter}s.`;
|
|
1046
1097
|
}
|
|
1047
1098
|
|
|
1048
|
-
export const __serverTest = { unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody };
|
|
1099
|
+
export const __serverTest = { unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody, streamResponse };
|
|
1049
1100
|
|
|
1050
1101
|
async function readErrorBody(upstreamRes, limitBytes = 64 * 1024) {
|
|
1051
1102
|
if (!upstreamRes.body) return '';
|
|
@@ -1696,7 +1747,7 @@ function mappedModel(originalModel, account) {
|
|
|
1696
1747
|
/**
|
|
1697
1748
|
* Stream an SSE response to the client, parsing usage data along the way.
|
|
1698
1749
|
*/
|
|
1699
|
-
async function streamResponse(webStream, res, status, responseHeaders, accountIndex, accountManager, streamLog, requestInfo = {}) {
|
|
1750
|
+
async function streamResponse(webStream, res, status, responseHeaders, accountIndex, accountManager, streamLog, requestInfo = {}, idleMs = STREAM_IDLE_MS) {
|
|
1700
1751
|
const reader = webStream.getReader();
|
|
1701
1752
|
const decoder = new TextDecoder();
|
|
1702
1753
|
let sseBuffer = '';
|
|
@@ -1711,9 +1762,39 @@ async function streamResponse(webStream, res, status, responseHeaders, accountIn
|
|
|
1711
1762
|
// pre-byte failover can still re-hold the session via queueAndRetry.
|
|
1712
1763
|
clearQueueHeartbeat(requestInfo);
|
|
1713
1764
|
|
|
1765
|
+
// Client left mid-stream: the caller drops its disconnect-abort once headers
|
|
1766
|
+
// arrive, and the loop below only checks res.destroyed AFTER a read resolves — so
|
|
1767
|
+
// a BLOCKED read() would never notice. Cancel the reader on 'close' to unblock it
|
|
1768
|
+
// (resolves the pending read as done) → the loop breaks → the lease frees instead
|
|
1769
|
+
// of leaking until the (maybe never) upstream close.
|
|
1770
|
+
const onClose = () => { reader.cancel().catch(() => {}); };
|
|
1771
|
+
res.once('close', onClose);
|
|
1772
|
+
|
|
1714
1773
|
try {
|
|
1715
1774
|
while (true) {
|
|
1716
|
-
|
|
1775
|
+
// Idle guard: a half-open upstream (headers, then silence, never closes) would
|
|
1776
|
+
// block reader.read() forever → the whole request handler hangs → neither the
|
|
1777
|
+
// lease nor onRequestEnd is released (the "106 phantom active" leak). Bound each
|
|
1778
|
+
// read by a per-chunk idle timeout, RESET every chunk so a healthy slow stream
|
|
1779
|
+
// is never cut; on a real stall, UPSTREAM_IDLE → the caller frees the lease and
|
|
1780
|
+
// ends the client SSE cleanly (isTransient → sendErrorResponse).
|
|
1781
|
+
const readP = reader.read();
|
|
1782
|
+
readP.catch(() => {}); // race-loser must not surface as an unhandled rejection
|
|
1783
|
+
let idleTimer;
|
|
1784
|
+
const idle = new Promise((_, reject) => {
|
|
1785
|
+
idleTimer = setTimeout(
|
|
1786
|
+
() => reject(Object.assign(new Error('upstream idle timeout'), { code: 'UPSTREAM_IDLE' })),
|
|
1787
|
+
idleMs,
|
|
1788
|
+
);
|
|
1789
|
+
idleTimer.unref?.();
|
|
1790
|
+
});
|
|
1791
|
+
let result;
|
|
1792
|
+
try {
|
|
1793
|
+
result = await Promise.race([readP, idle]);
|
|
1794
|
+
} finally {
|
|
1795
|
+
clearTimeout(idleTimer); // single live timer at a time — no per-chunk timer accumulation
|
|
1796
|
+
}
|
|
1797
|
+
const { done, value } = result;
|
|
1717
1798
|
if (done) break;
|
|
1718
1799
|
|
|
1719
1800
|
// Client disconnected — stop reading from upstream
|
|
@@ -1760,6 +1841,7 @@ async function streamResponse(webStream, res, status, responseHeaders, accountIn
|
|
|
1760
1841
|
readFailed = true;
|
|
1761
1842
|
throw err;
|
|
1762
1843
|
} finally {
|
|
1844
|
+
res.off('close', onClose);
|
|
1763
1845
|
// Cancel upstream reader to stop consuming data nobody needs
|
|
1764
1846
|
reader.cancel().catch(() => {});
|
|
1765
1847
|
if (!readFailed) {
|
package/src/tui.js
CHANGED
|
@@ -183,7 +183,18 @@ function bar(ratio, w = 10, resetTs) {
|
|
|
183
183
|
return out;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
// A gray placeholder the EXACT visible width of a bar(), for a quota that's absent
|
|
187
|
+
// or unpollable (label "—" / "n/a" / "probing"). Mirrors bar()'s no-data branch so
|
|
188
|
+
// provider rows stay column-aligned with OAuth rows.
|
|
189
|
+
function emptyBar(label, w = 10) {
|
|
190
|
+
const text = String(label).slice(0, w);
|
|
191
|
+
const pad = Math.max(0, w - text.length);
|
|
192
|
+
const lp = Math.floor(pad / 2);
|
|
193
|
+
const rp = pad - lp;
|
|
194
|
+
return `${ESC}100m${' '.repeat(lp)}${text}${' '.repeat(rp)}${RESET}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const __tuiTest = { formatReset, quotaLabel, bar, emptyBar, strip, loadText, countdown };
|
|
187
198
|
|
|
188
199
|
function timestamp() {
|
|
189
200
|
return new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
@@ -940,8 +951,10 @@ export class TUI {
|
|
|
940
951
|
const rawName = a.name.slice(0, 12).padEnd(12);
|
|
941
952
|
const name = isSel ? bold(rawName) : rawName;
|
|
942
953
|
|
|
943
|
-
// Type
|
|
944
|
-
|
|
954
|
+
// Type — pad to 8 so "provider" (8 chars) doesn't overflow a 7-wide column and
|
|
955
|
+
// shift the whole provider row (incl. its quota bars) 1 char out of alignment
|
|
956
|
+
// with the "oauth"/"apikey" rows.
|
|
957
|
+
const type = gray(a.type.padEnd(8));
|
|
945
958
|
|
|
946
959
|
// Status
|
|
947
960
|
let status;
|
|
@@ -1039,35 +1052,44 @@ export class TUI {
|
|
|
1039
1052
|
}
|
|
1040
1053
|
|
|
1041
1054
|
_renderProviderAcct(sel, cur, name, type, status, a, bw = 11, showBoth = true) {
|
|
1042
|
-
const completed = a.completedRequests || 0;
|
|
1043
|
-
const failed = a.failedRequests || 0;
|
|
1044
|
-
const active = a.inFlight || 0;
|
|
1045
|
-
const last = a.lastStatus ? `${statusColor(a.lastStatus)} ${formatMs(a.lastResponseMs)}` : '-';
|
|
1046
1055
|
const q = a.quota || {};
|
|
1047
1056
|
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
|
|
1057
|
+
// Ses/Wk bars in the SAME columns as OAuth rows (right after status) so the
|
|
1058
|
+
// whole table's bars line up. z.ai exposes a 5-hour token window (Ses) and —
|
|
1059
|
+
// when its monitor endpoint includes it — a weekly (Wk, intermittently absent →
|
|
1060
|
+
// "—"). Kimi's coding key has NO pollable quota (web console only) → "n/a" both
|
|
1061
|
+
// + a "console-only" note; not-yet-probed → "probing". Fields are the SEPARATE
|
|
1062
|
+
// provider* set, so a provider reading never leaks into an OAuth bar.
|
|
1063
|
+
let sesCell, wkCell, note = '';
|
|
1053
1064
|
if (q.providerSes != null || q.providerWk != null) {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (this.am._quotaProbeStale?.(a))
|
|
1065
|
+
sesCell = q.providerSes != null ? bar(q.providerSes, bw, q.providerSesReset) : emptyBar('—', bw);
|
|
1066
|
+
wkCell = q.providerWk != null ? bar(q.providerWk, bw, q.providerWkReset) : emptyBar('—', bw);
|
|
1067
|
+
if (this.am._quotaProbeStale?.(a)) note = ` ${dim('stale')}`;
|
|
1057
1068
|
} else if (q.providerQuotaSource === 'console-only') {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1069
|
+
sesCell = emptyBar('n/a', bw);
|
|
1070
|
+
wkCell = emptyBar('n/a', bw);
|
|
1071
|
+
note = ` ${dim('console-only')}`;
|
|
1072
|
+
} else {
|
|
1073
|
+
sesCell = emptyBar('probing', bw);
|
|
1074
|
+
wkCell = emptyBar('probing', bw);
|
|
1061
1075
|
}
|
|
1062
1076
|
|
|
1063
|
-
let
|
|
1077
|
+
let line = ` ${sel}${cur} ${name} ${type} ${status} Ses ${sesCell}`;
|
|
1078
|
+
if (showBoth) line += ` Wk ${wkCell}`;
|
|
1079
|
+
line += note;
|
|
1080
|
+
// Same recent-load columns as OAuth (providers track inFlight + load events),
|
|
1081
|
+
// plus a compact Last <status> <ms> — the one signal that matters for a
|
|
1082
|
+
// rarely-hit fallback (loadText reads 0r when idle).
|
|
1083
|
+
line += ` ${dim(loadText(this._accountLoad(a)))}`;
|
|
1084
|
+
if (a.lastStatus) line += ` ${dim('Last')} ${statusColor(a.lastStatus)} ${dim(formatMs(a.lastResponseMs))}`;
|
|
1085
|
+
// Header-derived generic rate-limit (distinct from the monitor-endpoint quota),
|
|
1086
|
+
// if the provider upstream returns x-ratelimit-* headers — only when present.
|
|
1064
1087
|
if (q.genericLimit != null && q.genericRemaining != null) {
|
|
1065
|
-
const used = q.genericLimit - q.genericRemaining;
|
|
1066
1088
|
const reset = formatReset(q.genericReset);
|
|
1067
|
-
|
|
1089
|
+
line += ` ${dim(`Lim ${q.genericLimit - q.genericRemaining}/${q.genericLimit}${reset ? ' ' + reset : ''}`)}`;
|
|
1068
1090
|
}
|
|
1069
|
-
|
|
1070
|
-
return
|
|
1091
|
+
if (a.lastError) line += ` ${red('Err ' + String(a.lastError).slice(0, 18))}`;
|
|
1092
|
+
return line;
|
|
1071
1093
|
}
|
|
1072
1094
|
|
|
1073
1095
|
_accountLoad(account) {
|