@askalf/dario 6.0.32 → 6.0.33
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/dist/admin-api.d.ts +19 -3
- package/dist/admin-api.js +9 -0
- package/dist/pool.d.ts +46 -1
- package/dist/pool.js +70 -7
- package/dist/proxy.js +20 -2
- package/dist/tui/tabs/accounts.d.ts +2 -0
- package/dist/tui/tabs/accounts.js +15 -2
- package/docs/admin-api.md +14 -6
- package/docs/multi-account-pool.md +13 -0
- package/package.json +1 -1
package/dist/admin-api.d.ts
CHANGED
|
@@ -40,9 +40,10 @@
|
|
|
40
40
|
* writes behind one HTTP request.
|
|
41
41
|
*
|
|
42
42
|
* `GET /admin/accounts` reports each account's persisted metadata — alias,
|
|
43
|
-
* scopes, token expiry — plus its live pool status (5h/7d utilization
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* scopes, token expiry — plus its live pool status (5h/7d utilization and
|
|
44
|
+
* how old that reading is, when its window resets, representative-claim,
|
|
45
|
+
* routing status, request and rejection counts, consecutive auth failures)
|
|
46
|
+
* when the proxy supplies a `poolStatus` snapshot, which it does
|
|
46
47
|
* whenever pool mode is active. It's the headless, admin-token-gated
|
|
47
48
|
* equivalent of the `GET /accounts` pool view.
|
|
48
49
|
*
|
|
@@ -90,9 +91,24 @@ export interface AdminAccountLive {
|
|
|
90
91
|
lastObservedAt: number | null;
|
|
91
92
|
/** Age of that reading in ms, or `null` when never observed. */
|
|
92
93
|
utilAgeMs: number | null;
|
|
94
|
+
/**
|
|
95
|
+
* When the window that reading was measured against rolls over — epoch ms
|
|
96
|
+
* and ms-from-now — or `null` when no response has stated one. For a
|
|
97
|
+
* `rejected` seat this is when the rejection lifts (dario#1244).
|
|
98
|
+
*/
|
|
99
|
+
resetAt: number | null;
|
|
100
|
+
resetInMs: number | null;
|
|
93
101
|
claim: string;
|
|
94
102
|
status: string;
|
|
95
103
|
requestCount: number;
|
|
104
|
+
/**
|
|
105
|
+
* Upstream 429s this account answered. `requestCount` counts requests it
|
|
106
|
+
* served and a 429 served nothing, so a seat parked on its first attempt
|
|
107
|
+
* read `request_count: 0` next to `status: rejected` (dario#1244).
|
|
108
|
+
*/
|
|
109
|
+
rejectedCount: number;
|
|
110
|
+
/** Epoch ms of the most recent 429 on this account, or `null` if never. */
|
|
111
|
+
lastRejectedAt: number | null;
|
|
96
112
|
/**
|
|
97
113
|
* Consecutive auth failures on this account (dario#234's cool-down
|
|
98
114
|
* counter). `status: 'auth-cooldown'` alone doesn't distinguish a single
|
package/dist/admin-api.js
CHANGED
|
@@ -359,9 +359,18 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
359
359
|
...(l ? {
|
|
360
360
|
util5h: l.util5h,
|
|
361
361
|
util7d: l.util7d,
|
|
362
|
+
// The reading's age and its window's reset, so `rejected` says
|
|
363
|
+
// since when and until when — the same fields GET /accounts has
|
|
364
|
+
// carried since #1032 and #1232; this surface dropped them.
|
|
365
|
+
last_observed_at: l.lastObservedAt ?? null,
|
|
366
|
+
util_age_ms: l.utilAgeMs ?? null,
|
|
367
|
+
reset_at: l.resetAt ?? null,
|
|
368
|
+
reset_in_ms: l.resetInMs ?? null,
|
|
362
369
|
claim: l.claim,
|
|
363
370
|
status: l.status,
|
|
364
371
|
request_count: l.requestCount,
|
|
372
|
+
rejected_count: l.rejectedCount ?? 0,
|
|
373
|
+
last_rejected_at: l.lastRejectedAt ?? null,
|
|
365
374
|
consecutive_auth_failures: l.consecutiveAuthFailures,
|
|
366
375
|
} : {}),
|
|
367
376
|
};
|
package/dist/pool.d.ts
CHANGED
|
@@ -65,6 +65,34 @@ export interface UtilFreshness {
|
|
|
65
65
|
* null rather than as an age of ~56 years since epoch.
|
|
66
66
|
*/
|
|
67
67
|
export declare function utilFreshness(rl: RateLimitSnapshot, now: number): UtilFreshness;
|
|
68
|
+
/** When an account's rate-limit window rolls over — see `rateLimitWindow`. */
|
|
69
|
+
export interface RateLimitWindow {
|
|
70
|
+
/**
|
|
71
|
+
* Epoch ms the window resets at, from `anthropic-ratelimit-unified-reset`;
|
|
72
|
+
* null when no response on this account has stated one.
|
|
73
|
+
*/
|
|
74
|
+
resetAt: number | null;
|
|
75
|
+
/** Ms until that reset, floored at 0 once it has passed; null when unknown. */
|
|
76
|
+
resetInMs: number | null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The reset moment of the window an account's last reading was measured
|
|
80
|
+
* against (dario#1244). The snapshot has carried `reset` since the header was
|
|
81
|
+
* first parsed and routing has expired rejections on it since #1232, but no
|
|
82
|
+
* operator surface showed it: a seat read `status: rejected` with nothing
|
|
83
|
+
* saying until when, and `requestCount: 0` beside it (a 429 serves nothing,
|
|
84
|
+
* so the attempt was never counted) made the rejection look like one dario
|
|
85
|
+
* had made up. For a `rejected` seat this is when the rejection lifts; for
|
|
86
|
+
* an `allowed` one, when its representative window rolls. The header is
|
|
87
|
+
* epoch SECONDS; both fields here are milliseconds, like `expiresInMs` and
|
|
88
|
+
* `utilAgeMs`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function rateLimitWindow(rl: RateLimitSnapshot, now: number): RateLimitWindow;
|
|
91
|
+
/**
|
|
92
|
+
* One line for a log or a doctor row:
|
|
93
|
+
* `5h 104%, 7d 25%, claim five_hour, resets in 37m`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function describeRateLimitSnapshot(rl: RateLimitSnapshot, now?: number): string;
|
|
68
96
|
export interface PoolAccount {
|
|
69
97
|
alias: string;
|
|
70
98
|
accessToken: string;
|
|
@@ -73,6 +101,16 @@ export interface PoolAccount {
|
|
|
73
101
|
identity: AccountIdentity;
|
|
74
102
|
rateLimit: RateLimitSnapshot;
|
|
75
103
|
requestCount: number;
|
|
104
|
+
/**
|
|
105
|
+
* Upstream 429s this account has answered. `requestCount` counts requests
|
|
106
|
+
* the account SERVED, and a 429 served nothing — so a seat parked on its
|
|
107
|
+
* first attempt read `requestCount: 0` next to `status: rejected`, as if
|
|
108
|
+
* dario had rejected a seat it never called (dario#1244). This is the field
|
|
109
|
+
* that says it was tried.
|
|
110
|
+
*/
|
|
111
|
+
rejectedCount: number;
|
|
112
|
+
/** Epoch ms of the most recent 429 on this account; undefined if never. */
|
|
113
|
+
lastRejectedAt?: number;
|
|
76
114
|
/** Epoch ms of the OAuth grant (refresh-grant.ts); undefined when unknown. */
|
|
77
115
|
grantedAt?: number;
|
|
78
116
|
/**
|
|
@@ -304,7 +342,14 @@ export declare class AccountPool {
|
|
|
304
342
|
/** Select the next-best account, excluding the given set of aliases. */
|
|
305
343
|
selectExcluding(excluded: Set<string>, family?: string | null): PoolAccount | null;
|
|
306
344
|
updateRateLimits(alias: string, snapshot: RateLimitSnapshot): void;
|
|
307
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Park `alias` on an upstream 429. Returns true when this takes a seat OUT
|
|
347
|
+
* of rotation — the first 429 of a window — and false when the seat was
|
|
348
|
+
* already parked inside a live window: the all-exhausted fallback in
|
|
349
|
+
* `select()` re-probes parked seats, so a pool with nothing left can 429
|
|
350
|
+
* the same seat many times, and only the transition is worth a log line.
|
|
351
|
+
*/
|
|
352
|
+
markRejected(alias: string, snapshot: RateLimitSnapshot): boolean;
|
|
308
353
|
updateTokens(alias: string, accessToken: string, refreshToken: string, expiresAt: number): void;
|
|
309
354
|
get(alias: string): PoolAccount | undefined;
|
|
310
355
|
all(): PoolAccount[];
|
package/dist/pool.js
CHANGED
|
@@ -58,6 +58,42 @@ export function utilFreshness(rl, now) {
|
|
|
58
58
|
utilAgeMs: lastObservedAt === null ? null : Math.max(0, now - lastObservedAt),
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The reset moment of the window an account's last reading was measured
|
|
63
|
+
* against (dario#1244). The snapshot has carried `reset` since the header was
|
|
64
|
+
* first parsed and routing has expired rejections on it since #1232, but no
|
|
65
|
+
* operator surface showed it: a seat read `status: rejected` with nothing
|
|
66
|
+
* saying until when, and `requestCount: 0` beside it (a 429 serves nothing,
|
|
67
|
+
* so the attempt was never counted) made the rejection look like one dario
|
|
68
|
+
* had made up. For a `rejected` seat this is when the rejection lifts; for
|
|
69
|
+
* an `allowed` one, when its representative window rolls. The header is
|
|
70
|
+
* epoch SECONDS; both fields here are milliseconds, like `expiresInMs` and
|
|
71
|
+
* `utilAgeMs`.
|
|
72
|
+
*/
|
|
73
|
+
export function rateLimitWindow(rl, now) {
|
|
74
|
+
if (!(rl.reset > 0))
|
|
75
|
+
return { resetAt: null, resetInMs: null };
|
|
76
|
+
const resetAt = rl.reset * 1000;
|
|
77
|
+
return { resetAt, resetInMs: Math.max(0, resetAt - now) };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* One line for a log or a doctor row:
|
|
81
|
+
* `5h 104%, 7d 25%, claim five_hour, resets in 37m`.
|
|
82
|
+
*/
|
|
83
|
+
export function describeRateLimitSnapshot(rl, now = Date.now()) {
|
|
84
|
+
const pct = (n) => `${Math.round(n * 100)}%`;
|
|
85
|
+
const { resetInMs } = rateLimitWindow(rl, now);
|
|
86
|
+
const reset = resetInMs === null ? 'no reset stated'
|
|
87
|
+
: resetInMs === 0 ? 'window already rolled'
|
|
88
|
+
: `resets in ${formatDurationMs(resetInMs)}`;
|
|
89
|
+
return `5h ${pct(rl.util5h)}, 7d ${pct(rl.util7d)}, claim ${rl.claim}, ${reset}`;
|
|
90
|
+
}
|
|
91
|
+
function formatDurationMs(ms) {
|
|
92
|
+
const totalMins = Math.max(1, Math.round(ms / 60_000));
|
|
93
|
+
const h = Math.floor(totalMins / 60);
|
|
94
|
+
const m = totalMins % 60;
|
|
95
|
+
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
|
96
|
+
}
|
|
61
97
|
/**
|
|
62
98
|
* Cool-down schedule after auth failures. First failure: 60s. Each
|
|
63
99
|
* consecutive failure doubles the window up to 30 minutes. Cleared
|
|
@@ -318,21 +354,36 @@ export class AccountPool {
|
|
|
318
354
|
}
|
|
319
355
|
add(alias, opts) {
|
|
320
356
|
const existing = this.accounts.get(alias);
|
|
357
|
+
// A record whose grantedAt differs from the live entry's is a NEW grant
|
|
358
|
+
// under this alias — a re-login, possibly on a different organization
|
|
359
|
+
// with its own windows. The live state describes the old credential (its
|
|
360
|
+
// rejection and reading, its auth streak, its identity), so it starts
|
|
361
|
+
// fresh (dario#1244): before this, a seat re-granted to clear
|
|
362
|
+
// `auth-cooldown` stayed cooling until the old streak's timer ran out,
|
|
363
|
+
// and one re-granted on another organization stayed parked on the old
|
|
364
|
+
// organization's window. A reconcile carrying the same grant — a token
|
|
365
|
+
// refresh, an admin change to another seat, a peer instance's rotation in
|
|
366
|
+
// HA — keeps the live state as before. So does a record with no grantedAt
|
|
367
|
+
// at all: it cannot be told apart from the same grant.
|
|
368
|
+
const regranted = existing !== undefined && opts.grantedAt !== undefined && opts.grantedAt !== existing.grantedAt;
|
|
369
|
+
const keep = regranted ? undefined : existing;
|
|
321
370
|
this.accounts.set(alias, {
|
|
322
371
|
alias,
|
|
323
372
|
accessToken: opts.accessToken,
|
|
324
373
|
refreshToken: opts.refreshToken,
|
|
325
374
|
expiresAt: opts.expiresAt,
|
|
326
|
-
grantedAt: opts.grantedAt ??
|
|
327
|
-
identity:
|
|
375
|
+
grantedAt: opts.grantedAt ?? keep?.grantedAt,
|
|
376
|
+
identity: keep?.identity ?? {
|
|
328
377
|
deviceId: opts.deviceId,
|
|
329
378
|
accountUuid: opts.accountUuid,
|
|
330
379
|
sessionId: randomUUID(),
|
|
331
380
|
},
|
|
332
|
-
rateLimit:
|
|
333
|
-
requestCount:
|
|
334
|
-
|
|
335
|
-
|
|
381
|
+
rateLimit: keep?.rateLimit ?? { ...EMPTY_SNAPSHOT },
|
|
382
|
+
requestCount: keep?.requestCount ?? 0,
|
|
383
|
+
rejectedCount: keep?.rejectedCount ?? 0,
|
|
384
|
+
lastRejectedAt: keep?.lastRejectedAt,
|
|
385
|
+
lastAuthFailureAt: keep?.lastAuthFailureAt,
|
|
386
|
+
consecutiveAuthFailures: keep?.consecutiveAuthFailures ?? 0,
|
|
336
387
|
});
|
|
337
388
|
}
|
|
338
389
|
remove(alias) {
|
|
@@ -549,11 +600,23 @@ export class AccountPool {
|
|
|
549
600
|
account.rateLimit = snapshot;
|
|
550
601
|
account.requestCount++;
|
|
551
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Park `alias` on an upstream 429. Returns true when this takes a seat OUT
|
|
605
|
+
* of rotation — the first 429 of a window — and false when the seat was
|
|
606
|
+
* already parked inside a live window: the all-exhausted fallback in
|
|
607
|
+
* `select()` re-probes parked seats, so a pool with nothing left can 429
|
|
608
|
+
* the same seat many times, and only the transition is worth a log line.
|
|
609
|
+
*/
|
|
552
610
|
markRejected(alias, snapshot) {
|
|
553
611
|
const account = this.accounts.get(alias);
|
|
554
612
|
if (!account)
|
|
555
|
-
return;
|
|
613
|
+
return false;
|
|
614
|
+
const now = snapshot.updatedAt || Date.now();
|
|
615
|
+
const wasParked = account.rateLimit.status === 'rejected' && !rateLimitWindowPassed(account.rateLimit, now);
|
|
556
616
|
account.rateLimit = { ...snapshot, status: 'rejected' };
|
|
617
|
+
account.rejectedCount++;
|
|
618
|
+
account.lastRejectedAt = now;
|
|
619
|
+
return !wasParked;
|
|
557
620
|
}
|
|
558
621
|
updateTokens(alias, accessToken, refreshToken, expiresAt) {
|
|
559
622
|
const account = this.accounts.get(alias);
|
package/dist/proxy.js
CHANGED
|
@@ -12,7 +12,7 @@ import { darioVersion } from './version.js';
|
|
|
12
12
|
import { buildCCRequest, applyCcPromptCaching, isGenuineCCClient, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, overlayTemplateHeaderValues, forwardClientCCIdentityHeaders, isMcpToolName, CC_TEMPLATE, effectiveCacheControl, withForced1hBeta } from './cc-template.js';
|
|
13
13
|
import { stampCch, hasCchSeed } from './cch.js';
|
|
14
14
|
import { describeTemplate, detectDrift, checkCCCompat, probeInstalledCCVersion } from './live-fingerprint.js';
|
|
15
|
-
import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, reconcilePoolAccounts, resolvePoolStrategy, utilFreshness } from './pool.js';
|
|
15
|
+
import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, reconcilePoolAccounts, resolvePoolStrategy, utilFreshness, rateLimitWindow, describeRateLimitSnapshot } from './pool.js';
|
|
16
16
|
import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS, CODEX_CLAIM } from './analytics.js';
|
|
17
17
|
import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
|
|
18
18
|
import { notify as osNotify } from './notify.js';
|
|
@@ -2102,9 +2102,12 @@ export async function startProxy(opts = {}) {
|
|
|
2102
2102
|
// surface documents itself as reporting the same snapshot, so it
|
|
2103
2103
|
// must not be the one place a stale reading still looks current.
|
|
2104
2104
|
...utilFreshness(a.rateLimit, snapNow),
|
|
2105
|
+
...rateLimitWindow(a.rateLimit, snapNow),
|
|
2105
2106
|
claim: a.rateLimit.claim,
|
|
2106
2107
|
status: reportedAccountStatus(a, snapNow),
|
|
2107
2108
|
requestCount: a.requestCount,
|
|
2109
|
+
rejectedCount: a.rejectedCount,
|
|
2110
|
+
lastRejectedAt: a.lastRejectedAt ?? null,
|
|
2108
2111
|
// Raw streak, not just the cooldown boolean: a single 401 also
|
|
2109
2112
|
// shows `auth-cooldown` for 60s, indistinguishable from a
|
|
2110
2113
|
// genuinely dead refresh token by that field alone. The magnitude
|
|
@@ -2201,9 +2204,16 @@ export async function startProxy(opts = {}) {
|
|
|
2201
2204
|
util5h: a.rateLimit.util5h,
|
|
2202
2205
|
util7d: a.rateLimit.util7d,
|
|
2203
2206
|
...utilFreshness(a.rateLimit, now),
|
|
2207
|
+
// When that window rolls (dario#1244): for a rejected seat, when
|
|
2208
|
+
// the rejection lifts. Milliseconds, like expiresInMs.
|
|
2209
|
+
...rateLimitWindow(a.rateLimit, now),
|
|
2204
2210
|
claim: a.rateLimit.claim,
|
|
2205
2211
|
status: reportedAccountStatus(a, now),
|
|
2206
2212
|
requestCount: a.requestCount,
|
|
2213
|
+
// 429s answered — the attempts requestCount does not count, so a
|
|
2214
|
+
// parked seat no longer reads as one that was never called.
|
|
2215
|
+
rejectedCount: a.rejectedCount,
|
|
2216
|
+
lastRejectedAt: a.lastRejectedAt ?? null,
|
|
2207
2217
|
expiresInMs: Math.max(0, a.expiresAt - now),
|
|
2208
2218
|
// Refresh-token grant age (refresh-grant.ts): the wall a token
|
|
2209
2219
|
// refresh cannot move. null fields = grant date unknown.
|
|
@@ -3723,7 +3733,15 @@ export async function startProxy(opts = {}) {
|
|
|
3723
3733
|
if (poolAccount) {
|
|
3724
3734
|
const snapshot = parseRateLimits(upstream.headers);
|
|
3725
3735
|
if (upstream.status === 429) {
|
|
3726
|
-
|
|
3736
|
+
// Say so the moment a seat leaves rotation. With a peer to fail
|
|
3737
|
+
// over to the client sees 200, and nothing else named the seat,
|
|
3738
|
+
// the reading, or when it comes back (dario#1244). Once per
|
|
3739
|
+
// parking: the all-exhausted fallback re-probes parked seats, and
|
|
3740
|
+
// those repeats are verbose-only.
|
|
3741
|
+
const parked = pool.markRejected(poolAccount.alias, snapshot);
|
|
3742
|
+
if (parked || verbose) {
|
|
3743
|
+
console.error(`[dario] #${requestCount} rate limited (429) on account "${poolAccount.alias}": ${describeRateLimitSnapshot(snapshot)} — parked until the window rolls`);
|
|
3744
|
+
}
|
|
3727
3745
|
}
|
|
3728
3746
|
else {
|
|
3729
3747
|
pool.updateRateLimits(poolAccount.alias, snapshot);
|
|
@@ -34,6 +34,8 @@ export interface AccountsState {
|
|
|
34
34
|
util5h?: number;
|
|
35
35
|
util7d?: number;
|
|
36
36
|
status?: string;
|
|
37
|
+
/** Ms until the seat's rate-limit window rolls; null/absent when unknown. */
|
|
38
|
+
resetInMs?: number | null;
|
|
37
39
|
}>;
|
|
38
40
|
error: string | null;
|
|
39
41
|
/** Where the list came from: the running proxy's pool, the proxy's
|
|
@@ -98,8 +98,11 @@ export const AccountsTab = {
|
|
|
98
98
|
const expiresCol = pad(formatExpiry(acc.expiresAt), 14);
|
|
99
99
|
const u5 = pad(acc.util5h !== undefined ? `${Math.round(acc.util5h * 100)}%` : '—', 9);
|
|
100
100
|
const u7 = pad(acc.util7d !== undefined ? `${Math.round(acc.util7d * 100)}%` : '—', 9);
|
|
101
|
-
|
|
102
|
-
const
|
|
101
|
+
// A parked seat says for how long (dario#1244): "rejected 37m".
|
|
102
|
+
const statusCol = acc.status === 'rejected' && typeof acc.resetInMs === 'number'
|
|
103
|
+
? `rejected ${formatCountdown(acc.resetInMs)}`
|
|
104
|
+
: (acc.status ?? '—');
|
|
105
|
+
const statusFg = statusCol === 'auth-cooldown' || acc.status === 'rejected' ? fg('yellow', statusCol) : dim(statusCol);
|
|
103
106
|
push(' ' + aliasCol + expiresCol + u5 + u7 + statusFg);
|
|
104
107
|
}
|
|
105
108
|
else {
|
|
@@ -143,6 +146,7 @@ export async function refreshAccounts(ctx) {
|
|
|
143
146
|
util5h: a.util5h,
|
|
144
147
|
util7d: a.util7d,
|
|
145
148
|
status: a.status,
|
|
149
|
+
resetInMs: a.resetInMs,
|
|
146
150
|
})),
|
|
147
151
|
error: null,
|
|
148
152
|
};
|
|
@@ -177,6 +181,15 @@ async function diskFallback() {
|
|
|
177
181
|
return { loading: false, accounts: [], error: e.message, source: 'disk' };
|
|
178
182
|
}
|
|
179
183
|
}
|
|
184
|
+
/** `37m` / `4h59m` / `now` — how long until a parked seat's window rolls. */
|
|
185
|
+
function formatCountdown(ms) {
|
|
186
|
+
if (ms <= 0)
|
|
187
|
+
return 'now';
|
|
188
|
+
const totalMins = Math.max(1, Math.round(ms / 60_000));
|
|
189
|
+
const h = Math.floor(totalMins / 60);
|
|
190
|
+
const m = totalMins % 60;
|
|
191
|
+
return h > 0 ? `${h}h${m}m` : `${m}m`;
|
|
192
|
+
}
|
|
180
193
|
function formatExpiry(expiresAt) {
|
|
181
194
|
if (expiresAt === 0)
|
|
182
195
|
return dim('—');
|
package/docs/admin-api.md
CHANGED
|
@@ -95,12 +95,20 @@ All endpoints accept the token as `authorization: Bearer <token>` or
|
|
|
95
95
|
| `DELETE /admin/accounts/<alias>` | — | `{ alias, removed }` (`404` if no such alias) |
|
|
96
96
|
|
|
97
97
|
`GET /admin/accounts` is the monitoring surface: each entry carries the
|
|
98
|
-
persisted metadata (`alias`, `scopes`, `expires_in_ms
|
|
99
|
-
status whenever pool mode is active** — `util5h` / `util7d`
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
persisted metadata (`alias`, `scopes`, `expires_in_ms`, the grant-age fields)
|
|
99
|
+
**plus live pool status whenever pool mode is active** — `util5h` / `util7d`
|
|
100
|
+
utilization with `last_observed_at` / `util_age_ms` (how old that reading is;
|
|
101
|
+
it does not tick while a seat is parked), `reset_at` / `reset_in_ms` (when
|
|
102
|
+
the window it was measured against rolls — for a `rejected` seat, when the
|
|
103
|
+
rejection lifts), representative `claim` (e.g. `five_hour`), routing
|
|
104
|
+
`status`, `request_count` (requests served), `rejected_count` /
|
|
105
|
+
`last_rejected_at` (429s answered — a 429 serves nothing, so it is not a
|
|
106
|
+
request), and `consecutive_auth_failures`. What each `status` means and what
|
|
107
|
+
to do about it: [Reading a seat's `status`](./multi-account-pool.md#reading-a-seats-status).
|
|
108
|
+
It's the admin-token-gated equivalent of the proxy-key-gated `GET /accounts`
|
|
109
|
+
pool view; a headless operator needs only the admin token to watch headroom.
|
|
110
|
+
Completing a login for an alias that is already in the pool re-grants it: the
|
|
111
|
+
seat starts fresh — no carried-over rejection, cool-down or identity.
|
|
104
112
|
|
|
105
113
|
## Pinning a request to one seat
|
|
106
114
|
|
|
@@ -91,6 +91,19 @@ curl http://localhost:3456/accounts # per-account utilization, claim, sticky
|
|
|
91
91
|
curl http://localhost:3456/analytics # per-account / per-model stats, burn rate, exhaustion predictions
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
## Reading a seat's `status`
|
|
95
|
+
|
|
96
|
+
`GET /accounts` (and the admin API's `GET /admin/accounts`, in snake_case) report one `status` per seat. It is the routing verdict, and every value comes with the fields that explain it.
|
|
97
|
+
|
|
98
|
+
| `status` | What it means | What to do |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `allowed` | The seat's last response was a 200 with headroom. `util5h` / `util7d` are that response's reading — a ratio against 1.0, so `0.42` is 42% — `lastObservedAt` / `utilAgeMs` say how old it is, `resetAt` / `resetInMs` when its representative window rolls. | Nothing. |
|
|
101
|
+
| `rejected` | The seat's last response was a 429: the organization behind its token is over the window named by `claim` (`five_hour`, `seven_day`, …). `util5h: 1.04` is 104% of the five-hour window, not 1%. `rejectedCount` / `lastRejectedAt` say the seat was tried — a 429 serves nothing, so `requestCount` does not move — and `resetInMs` says how long it stays parked. Requests route around it; it returns on its own when the window rolls. | Nothing — the window clears itself. If the reading surprises you (your usage page for that account says 0%), the token belongs to a different organization than the page you are looking at, or to the same organization as another seat: the reading is Anthropic's own, taken on that token. `dario accounts check <alias>` asks the seat directly. |
|
|
102
|
+
| `unknown` | No current observation: a seat that has served nothing yet, or a rejection whose window has rolled (`resetInMs: 0`) and that nothing has measured since. | Nothing; the next request measures it. |
|
|
103
|
+
| `auth-cooldown` | Upstream answered 401/403 or `invalid_grant`. `consecutiveAuthFailures` tells a blip (1) from a dead refresh token (a streak); the cool-down doubles with the streak, from 1 minute to 30. | A streak means re-grant the seat — `dario accounts remove` + `add`, or the admin login flow under the same alias. A new grant starts the seat fresh: no carried-over cool-down, rejection or identity. See [Refresh-token grant age](#refresh-token-grant-age) for the 28-day wall behind most streaks. |
|
|
104
|
+
|
|
105
|
+
The proxy logs every parking as it happens, once per window: `rate limited (429) on account "spare": 5h 104%, 7d 25%, claim five_hour, resets in 37m — parked until the window rolls`. The re-probes the all-exhausted fallback makes of an already-parked seat are logged only under `-v`.
|
|
106
|
+
|
|
94
107
|
Every request carries a `billingBucket` field (`subscription` / `subscription_fallback` / `extra_usage` / `api` / `unknown`) so you can see which bucket each request billed against and a `subscriptionPercent` headline number tells you at a glance whether dario is actually routing through your subscription or silently falling to API overage.
|
|
95
108
|
|
|
96
109
|
## Refresh-token grant age
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.33",
|
|
4
4
|
"description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|