@askalf/dario 5.5.29 → 5.5.30
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/health-response.d.ts +13 -0
- package/dist/health-response.js +23 -4
- package/dist/pool.d.ts +27 -0
- package/dist/pool.js +39 -12
- package/dist/proxy.js +9 -2
- package/package.json +1 -1
|
@@ -82,6 +82,19 @@ export interface HealthResponse {
|
|
|
82
82
|
export interface PoolAccountStatusLike {
|
|
83
83
|
expiresAt: number;
|
|
84
84
|
inAuthCooldown: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Why the ROUTER cannot serve from this account, or null when it can —
|
|
87
|
+
* `accountIneligibility()` in pool.ts, computed by the caller (dario#1030).
|
|
88
|
+
*
|
|
89
|
+
* Required, deliberately. This surface previously derived usability itself
|
|
90
|
+
* from `inAuthCooldown` alone, which is a SUBSET of what select() filters on:
|
|
91
|
+
* a pool with every token expired, or every account rate-limited, reported
|
|
92
|
+
* `healthy` / `authenticated: true` while every request it served failed.
|
|
93
|
+
* The field is passed in rather than computed here so this module stays free
|
|
94
|
+
* of pool machinery and unit-testable as a pure function — and required so a
|
|
95
|
+
* caller cannot silently reintroduce the subset.
|
|
96
|
+
*/
|
|
97
|
+
ineligible: string | null;
|
|
85
98
|
}
|
|
86
99
|
export interface PoolDerivedStatus {
|
|
87
100
|
authenticated: boolean;
|
package/dist/health-response.js
CHANGED
|
@@ -14,6 +14,17 @@
|
|
|
14
14
|
* The HTTP status (200 healthy / 503 degraded) is identical either way, so external
|
|
15
15
|
* uptime monitoring that keys on the status code is unaffected.
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* Reason -> operator-facing summary. Keyed by `accountIneligibility()`'s return
|
|
19
|
+
* values; declared here rather than imported so this module stays free of pool
|
|
20
|
+
* machinery (see the file header). An unrecognised reason falls through to a
|
|
21
|
+
* generic line rather than being dropped.
|
|
22
|
+
*/
|
|
23
|
+
const POOL_DEAD_SUMMARY = {
|
|
24
|
+
'rate-limited': 'all accounts rate-limited',
|
|
25
|
+
'token-expired': 'all tokens expired — run `dario login`',
|
|
26
|
+
'auth-cooldown': 'all accounts in auth-cooldown',
|
|
27
|
+
};
|
|
17
28
|
function formatMsLeft(ms) {
|
|
18
29
|
const clamped = Math.max(0, ms);
|
|
19
30
|
return `${Math.floor(clamped / 3_600_000)}h ${Math.floor((clamped % 3_600_000) / 60_000)}m`;
|
|
@@ -33,17 +44,25 @@ export function derivePoolStatus(accounts, now, adminEnabled) {
|
|
|
33
44
|
: 'no accounts yet — run `dario accounts add <alias>`',
|
|
34
45
|
};
|
|
35
46
|
}
|
|
36
|
-
|
|
47
|
+
// Ask exactly the question the router asks. Anything select() would refuse
|
|
48
|
+
// to serve from is unusable here too, whatever the reason.
|
|
49
|
+
const usable = accounts.filter((a) => a.ineligible === null);
|
|
37
50
|
if (usable.length === 0) {
|
|
38
|
-
//
|
|
39
|
-
//
|
|
51
|
+
// No account can serve the next request — the deadness /health exists to
|
|
52
|
+
// signal. Name the reason: an operator seeing 'broken' needs to know
|
|
53
|
+
// whether to run `dario login`, wait out a rate limit, or fix credentials,
|
|
54
|
+
// and those have nothing in common but the verdict.
|
|
55
|
+
const reasons = new Set(accounts.map((a) => a.ineligible).filter((r) => r !== null));
|
|
56
|
+
const summary = reasons.size === 1
|
|
57
|
+
? POOL_DEAD_SUMMARY[[...reasons][0]] ?? `all accounts unusable: ${[...reasons][0]}`
|
|
58
|
+
: `no account can serve (${[...reasons].sort().join(', ')})`;
|
|
40
59
|
return {
|
|
41
60
|
authenticated: false,
|
|
42
61
|
status: 'broken',
|
|
43
62
|
mode: 'pool',
|
|
44
63
|
accounts: accounts.length,
|
|
45
64
|
expiresAt: Math.min(...accounts.map((a) => a.expiresAt)),
|
|
46
|
-
expiresIn:
|
|
65
|
+
expiresIn: summary,
|
|
47
66
|
};
|
|
48
67
|
}
|
|
49
68
|
// Earliest expiry among USABLE accounts — the pool's background refresh
|
package/dist/pool.d.ts
CHANGED
|
@@ -92,6 +92,33 @@ export interface PoolAccount {
|
|
|
92
92
|
}
|
|
93
93
|
export declare function authCooldownMs(consecutiveFailures: number): number;
|
|
94
94
|
export declare function isInAuthCooldown(account: PoolAccount, now?: number): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* How long before a token's stated expiry the router stops trusting it. A
|
|
97
|
+
* request selected at T must still be valid when it reaches Anthropic.
|
|
98
|
+
*/
|
|
99
|
+
export declare const TOKEN_EXPIRY_MARGIN_MS = 30000;
|
|
100
|
+
/** Why the router cannot serve a request from an account. */
|
|
101
|
+
export type AccountIneligibility = 'rate-limited' | 'token-expired' | 'auth-cooldown';
|
|
102
|
+
/**
|
|
103
|
+
* The single answer to "can the router serve a request from this account, and
|
|
104
|
+
* if not, why not?" (dario#1030).
|
|
105
|
+
*
|
|
106
|
+
* This predicate was inline at four sites in this file and had been
|
|
107
|
+
* re-implemented, as a SUBSET, by a fifth reader in health-response.ts — which
|
|
108
|
+
* filtered on auth-cooldown alone. A pool whose tokens had all expired, or
|
|
109
|
+
* whose accounts were all rate-limited, therefore reported `healthy` /
|
|
110
|
+
* `authenticated: true` on /health while every request it served failed. That
|
|
111
|
+
* is the exact case /health exists to catch: a monitor watching it sees
|
|
112
|
+
* nothing, and `dario doctor` reads the same derivation.
|
|
113
|
+
*
|
|
114
|
+
* It returns the REASON rather than a boolean because the surfaces want to say
|
|
115
|
+
* why — /health's `expiresIn` line, the 503 body, and doctor's routing row all
|
|
116
|
+
* need to name the failure, and a boolean forces each of them to re-derive it
|
|
117
|
+
* and drift again.
|
|
118
|
+
*/
|
|
119
|
+
export declare function accountIneligibility(account: PoolAccount, now?: number): AccountIneligibility | null;
|
|
120
|
+
/** Boolean form of `accountIneligibility` — the router's eligibility filter. */
|
|
121
|
+
export declare function isAccountEligible(account: PoolAccount, now?: number): boolean;
|
|
95
122
|
export interface PoolStatus {
|
|
96
123
|
accounts: number;
|
|
97
124
|
healthy: number;
|
package/dist/pool.js
CHANGED
|
@@ -78,6 +78,41 @@ export function isInAuthCooldown(account, now = Date.now()) {
|
|
|
78
78
|
const cooldown = authCooldownMs(account.consecutiveAuthFailures);
|
|
79
79
|
return now - account.lastAuthFailureAt < cooldown;
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* How long before a token's stated expiry the router stops trusting it. A
|
|
83
|
+
* request selected at T must still be valid when it reaches Anthropic.
|
|
84
|
+
*/
|
|
85
|
+
export const TOKEN_EXPIRY_MARGIN_MS = 30_000;
|
|
86
|
+
/**
|
|
87
|
+
* The single answer to "can the router serve a request from this account, and
|
|
88
|
+
* if not, why not?" (dario#1030).
|
|
89
|
+
*
|
|
90
|
+
* This predicate was inline at four sites in this file and had been
|
|
91
|
+
* re-implemented, as a SUBSET, by a fifth reader in health-response.ts — which
|
|
92
|
+
* filtered on auth-cooldown alone. A pool whose tokens had all expired, or
|
|
93
|
+
* whose accounts were all rate-limited, therefore reported `healthy` /
|
|
94
|
+
* `authenticated: true` on /health while every request it served failed. That
|
|
95
|
+
* is the exact case /health exists to catch: a monitor watching it sees
|
|
96
|
+
* nothing, and `dario doctor` reads the same derivation.
|
|
97
|
+
*
|
|
98
|
+
* It returns the REASON rather than a boolean because the surfaces want to say
|
|
99
|
+
* why — /health's `expiresIn` line, the 503 body, and doctor's routing row all
|
|
100
|
+
* need to name the failure, and a boolean forces each of them to re-derive it
|
|
101
|
+
* and drift again.
|
|
102
|
+
*/
|
|
103
|
+
export function accountIneligibility(account, now = Date.now()) {
|
|
104
|
+
if (account.rateLimit.status === 'rejected')
|
|
105
|
+
return 'rate-limited';
|
|
106
|
+
if (account.expiresAt <= now + TOKEN_EXPIRY_MARGIN_MS)
|
|
107
|
+
return 'token-expired';
|
|
108
|
+
if (isInAuthCooldown(account, now))
|
|
109
|
+
return 'auth-cooldown';
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/** Boolean form of `accountIneligibility` — the router's eligibility filter. */
|
|
113
|
+
export function isAccountEligible(account, now = Date.now()) {
|
|
114
|
+
return accountIneligibility(account, now) === null;
|
|
115
|
+
}
|
|
81
116
|
/**
|
|
82
117
|
* Resolve the pool strategy from an explicit value (CLI flag / config file,
|
|
83
118
|
* already precedence-merged by the caller) with `DARIO_POOL_STRATEGY` as
|
|
@@ -313,9 +348,7 @@ export class AccountPool {
|
|
|
313
348
|
return null;
|
|
314
349
|
const now = Date.now();
|
|
315
350
|
const all = [...this.accounts.values()];
|
|
316
|
-
const eligible = all.filter(a => a
|
|
317
|
-
a.expiresAt > now + 30_000 &&
|
|
318
|
-
!isInAuthCooldown(a, now));
|
|
351
|
+
const eligible = all.filter(a => isAccountEligible(a, now));
|
|
319
352
|
if (eligible.length > 0) {
|
|
320
353
|
if (this.strategy === 'fill-first') {
|
|
321
354
|
const first = pickFillFirst(eligible, family);
|
|
@@ -366,9 +399,7 @@ export class AccountPool {
|
|
|
366
399
|
if (binding) {
|
|
367
400
|
const bound = this.accounts.get(binding.alias);
|
|
368
401
|
if (bound
|
|
369
|
-
&& bound
|
|
370
|
-
&& bound.expiresAt > now + 30_000
|
|
371
|
-
&& !isInAuthCooldown(bound, now)
|
|
402
|
+
&& isAccountEligible(bound, now)
|
|
372
403
|
&& computeHeadroom(bound.rateLimit, family) > POOL_HEADROOM_FLOOR) {
|
|
373
404
|
// Refresh the idle timer. A session that keeps taking turns must never
|
|
374
405
|
// be reaped or rebound while active — that would strand its warm prompt
|
|
@@ -446,9 +477,7 @@ export class AccountPool {
|
|
|
446
477
|
return null;
|
|
447
478
|
const now = Date.now();
|
|
448
479
|
const candidates = [...this.accounts.values()].filter(a => !excluded.has(a.alias));
|
|
449
|
-
const eligible = candidates.filter(a => a
|
|
450
|
-
a.expiresAt > now + 30_000 &&
|
|
451
|
-
!isInAuthCooldown(a, now));
|
|
480
|
+
const eligible = candidates.filter(a => isAccountEligible(a, now));
|
|
452
481
|
if (eligible.length > 0) {
|
|
453
482
|
// Fill-first failover keeps the fill order: the next account tried
|
|
454
483
|
// after a 429 is the next alias in line, not the max-headroom seat —
|
|
@@ -496,9 +525,7 @@ export class AccountPool {
|
|
|
496
525
|
status() {
|
|
497
526
|
const all = this.all();
|
|
498
527
|
const now = Date.now();
|
|
499
|
-
const healthy = all.filter(a => a
|
|
500
|
-
a.expiresAt > now + 30_000 &&
|
|
501
|
-
!isInAuthCooldown(a, now));
|
|
528
|
+
const healthy = all.filter(a => isAccountEligible(a, now));
|
|
502
529
|
// Status is a pool-wide aggregate; family-agnostic. Per-model
|
|
503
530
|
// headroom is request-context-specific and only meaningful at
|
|
504
531
|
// select() time.
|
package/dist/proxy.js
CHANGED
|
@@ -12,7 +12,7 @@ import { darioVersion } from './version.js';
|
|
|
12
12
|
import { buildCCRequest, applyCcPromptCaching, 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, reconcilePoolAccounts, resolvePoolStrategy, utilFreshness } from './pool.js';
|
|
15
|
+
import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reconcilePoolAccounts, resolvePoolStrategy, utilFreshness } from './pool.js';
|
|
16
16
|
import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS } from './analytics.js';
|
|
17
17
|
import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
|
|
18
18
|
import { notify as osNotify } from './notify.js';
|
|
@@ -1677,7 +1677,14 @@ export async function startProxy(opts = {}) {
|
|
|
1677
1677
|
// made the TUI claim the proxy was down).
|
|
1678
1678
|
async function currentStatus() {
|
|
1679
1679
|
const now = Date.now();
|
|
1680
|
-
return derivePoolStatus(
|
|
1680
|
+
return derivePoolStatus(
|
|
1681
|
+
// `ineligible` carries the ROUTER's verdict, so /health cannot answer a
|
|
1682
|
+
// narrower question than select() does (#1030).
|
|
1683
|
+
pool.all().map((a) => ({
|
|
1684
|
+
expiresAt: a.expiresAt,
|
|
1685
|
+
inAuthCooldown: isInAuthCooldown(a, now),
|
|
1686
|
+
ineligible: accountIneligibility(a, now),
|
|
1687
|
+
})), now, adminEnabled);
|
|
1681
1688
|
}
|
|
1682
1689
|
// Model catalog wiring — /v1/models serves the upstream-autodetected set,
|
|
1683
1690
|
// authenticated the same way the request path is (per-token API key when
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.30",
|
|
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": {
|