@askalf/dario 6.12.7 → 6.12.9
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/cli.js +6 -4
- package/dist/doctor-core.js +1 -1
- package/dist/pool.d.ts +7 -4
- package/dist/pool.js +97 -25
- package/dist/proxy.d.ts +2 -1
- package/dist/proxy.js +5 -4
- package/docs/pool-keys-and-analytics.md +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -536,8 +536,9 @@ async function proxy() {
|
|
|
536
536
|
?? fileCfg.pool?.strategy;
|
|
537
537
|
// --pool-headroom-floor=<ratio|percent> — the headroom at/below which a seat
|
|
538
538
|
// counts as drained: sticky sessions rebind off it and new conversations
|
|
539
|
-
// skip it.
|
|
540
|
-
//
|
|
539
|
+
// skip it. Setting the floor makes it a hard stop; the implicit 2% default
|
|
540
|
+
// stays a routing preference. dario#1333 asked to leave a seat alone at 95%
|
|
541
|
+
// used rather than ride it into the 429.
|
|
541
542
|
const poolHeadroomFloorFromFlag = args.find((a) => a.startsWith('--pool-headroom-floor='))?.split('=')[1];
|
|
542
543
|
if (poolHeadroomFloorFromFlag !== undefined && parsePoolHeadroomFloor(poolHeadroomFloorFromFlag) === null) {
|
|
543
544
|
console.error(`[dario] Invalid --pool-headroom-floor "${poolHeadroomFloorFromFlag}". Use a ratio or percent between 2% and 50% (e.g. 0.05 or 5%).`);
|
|
@@ -2203,8 +2204,9 @@ async function help() {
|
|
|
2203
2204
|
--pool-headroom-floor=<ratio|percent>
|
|
2204
2205
|
Headroom at or below which a seat counts as
|
|
2205
2206
|
drained: a sticky session rebinds off it and
|
|
2206
|
-
new conversations skip it.
|
|
2207
|
-
|
|
2207
|
+
new conversations skip it. Setting this makes
|
|
2208
|
+
the floor a hard stop. 0.05 or 5%; implicit
|
|
2209
|
+
default 2% is a preference, max 50%. Use it when your seats
|
|
2208
2210
|
answer with API errors in the last percent
|
|
2209
2211
|
of a window (dario#1333).
|
|
2210
2212
|
Env: DARIO_POOL_HEADROOM_FLOOR;
|
package/dist/doctor-core.js
CHANGED
|
@@ -1139,7 +1139,7 @@ export async function runChecks(opts = {}) {
|
|
|
1139
1139
|
// default: `new AccountPool()` here always predicted a max-headroom
|
|
1140
1140
|
// pick, which is the wrong seat on a fill-first pool.
|
|
1141
1141
|
const routing = configuredPoolRouting(loadConfig().config.pool);
|
|
1142
|
-
const pool = new AccountPool(routing.strategy, routing.headroomFloor);
|
|
1142
|
+
const pool = new AccountPool(routing.strategy, routing.headroomFloorExplicit ? routing.headroomFloor : undefined);
|
|
1143
1143
|
for (const acc of loaded) {
|
|
1144
1144
|
pool.add(acc.alias, {
|
|
1145
1145
|
accessToken: acc.accessToken,
|
package/dist/pool.d.ts
CHANGED
|
@@ -437,6 +437,7 @@ export declare function configuredPoolRouting(file: {
|
|
|
437
437
|
} | undefined, env?: NodeJS.ProcessEnv): {
|
|
438
438
|
strategy: PoolStrategy;
|
|
439
439
|
headroomFloor: number;
|
|
440
|
+
headroomFloorExplicit: boolean;
|
|
440
441
|
};
|
|
441
442
|
/**
|
|
442
443
|
* Which wire buckets bind which model family, when the wire does not say it
|
|
@@ -602,8 +603,6 @@ export declare function resolvePoolHeadroomFloor(explicit?: string | number | nu
|
|
|
602
603
|
export declare function seatExpiry(account: PoolAccount, now?: number): number;
|
|
603
604
|
export declare class AccountPool {
|
|
604
605
|
readonly strategy: PoolStrategy;
|
|
605
|
-
/** Headroom at/below which a seat counts as drained — see DEFAULT_POOL_HEADROOM_FLOOR. */
|
|
606
|
-
readonly headroomFloor: number;
|
|
607
606
|
private accounts;
|
|
608
607
|
private queue;
|
|
609
608
|
private queueMaxSize;
|
|
@@ -611,9 +610,11 @@ export declare class AccountPool {
|
|
|
611
610
|
private drainTimer;
|
|
612
611
|
private sticky;
|
|
613
612
|
private lastStickyCleanup;
|
|
614
|
-
constructor(strategy?: PoolStrategy,
|
|
615
613
|
/** Headroom at/below which a seat counts as drained — see DEFAULT_POOL_HEADROOM_FLOOR. */
|
|
616
|
-
headroomFloor
|
|
614
|
+
readonly headroomFloor: number;
|
|
615
|
+
/** Only an explicitly configured floor is a hard stop; the implicit 2% remains a preference. */
|
|
616
|
+
readonly hardHeadroomFloor: boolean;
|
|
617
|
+
constructor(strategy?: PoolStrategy, headroomFloor?: number);
|
|
617
618
|
add(alias: string, opts: {
|
|
618
619
|
accessToken: string;
|
|
619
620
|
refreshToken: string;
|
|
@@ -729,6 +730,8 @@ export declare class AccountPool {
|
|
|
729
730
|
adoptSnapshot(alias: string, snapshot: RateLimitSnapshot, rejected: boolean, from: string): boolean;
|
|
730
731
|
updateTokens(alias: string, accessToken: string, refreshToken: string, expiresAt: number): void;
|
|
731
732
|
get(alias: string): PoolAccount | undefined;
|
|
733
|
+
/** Whether a preferred (but not pinned) seat may bypass normal pool ordering. */
|
|
734
|
+
canPrefer(account: PoolAccount, now?: number, family?: string | null): boolean;
|
|
732
735
|
all(): PoolAccount[];
|
|
733
736
|
status(): PoolStatus;
|
|
734
737
|
/**
|
package/dist/pool.js
CHANGED
|
@@ -453,9 +453,11 @@ export function describePoolStrategy(strategy, floor = DEFAULT_POOL_HEADROOM_FLO
|
|
|
453
453
|
* CLI flag, which only the running process knows (GET /status reports it).
|
|
454
454
|
*/
|
|
455
455
|
export function configuredPoolRouting(file, env = process.env) {
|
|
456
|
+
const floorCandidate = env.DARIO_POOL_HEADROOM_FLOOR ?? file?.headroomFloor;
|
|
456
457
|
return {
|
|
457
458
|
strategy: resolvePoolStrategy(env.DARIO_POOL_STRATEGY ?? file?.strategy, env),
|
|
458
|
-
headroomFloor: resolvePoolHeadroomFloor(
|
|
459
|
+
headroomFloor: resolvePoolHeadroomFloor(floorCandidate, env),
|
|
460
|
+
headroomFloorExplicit: parsePoolHeadroomFloor(floorCandidate) !== null,
|
|
459
461
|
};
|
|
460
462
|
}
|
|
461
463
|
/**
|
|
@@ -834,11 +836,51 @@ function pickMaxHeadroom(accounts, family) {
|
|
|
834
836
|
}
|
|
835
837
|
return best;
|
|
836
838
|
}
|
|
839
|
+
function isAboveHeadroomFloor(headroom, floor) {
|
|
840
|
+
return headroom - floor > Number.EPSILON;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* When a below-floor reading can retire without an upstream probe, return when
|
|
844
|
+
* it does, in epoch ms: the latest future reset among the buckets keeping the
|
|
845
|
+
* seat at or below the floor. Each of those buckets holds the seat there until
|
|
846
|
+
* its own reset, so the seat is parked until the last of them, and a bucket
|
|
847
|
+
* whose reset is unknown does not shorten that. Null only when no limiting
|
|
848
|
+
* bucket has a future reset: then one request must refresh the reading. A seat
|
|
849
|
+
* at 96% of its 5h window and 97% of its 7d window, whose representative claim
|
|
850
|
+
* is the 7d one, carries no 5h reset; it stays parked until the 7d reset
|
|
851
|
+
* rather than taking traffic, and is probed after that if the 5h reading
|
|
852
|
+
* still holds it below the floor.
|
|
853
|
+
*/
|
|
854
|
+
function floorParkedUntil(snapshot, family, floor, now) {
|
|
855
|
+
const rl = expireElapsedWindow(snapshot, now);
|
|
856
|
+
const limiting = (util) => !isAboveHeadroomFloor(1 - util, floor);
|
|
857
|
+
const reset7d = () => ((rl.reset7d ?? 0) * 1000 > now
|
|
858
|
+
? (rl.reset7d ?? 0) * 1000
|
|
859
|
+
: rl.claim.startsWith('seven_day') ? rl.reset * 1000 : 0);
|
|
860
|
+
const resets = [];
|
|
861
|
+
if (limiting(rl.util5h))
|
|
862
|
+
resets.push(rl.claim === 'five_hour' ? rl.reset * 1000 : 0);
|
|
863
|
+
if (limiting(rl.util7d))
|
|
864
|
+
resets.push(reset7d());
|
|
865
|
+
if (family) {
|
|
866
|
+
for (const bucket of bucketsBindingFamily(rl, family)) {
|
|
867
|
+
const util = rl.perModel7d[bucket];
|
|
868
|
+
if (util !== undefined && limiting(util))
|
|
869
|
+
resets.push(reset7d());
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const known = resets.filter((reset) => reset > now);
|
|
873
|
+
return known.length > 0 ? Math.max(...known) : null;
|
|
874
|
+
}
|
|
875
|
+
function pickFloorProbe(accounts, family, floor, now) {
|
|
876
|
+
const stale = accounts.filter(a => floorParkedUntil(a.rateLimit, family, floor, now) === null);
|
|
877
|
+
return stale.length > 0 ? pickMaxHeadroom(stale, family) : null;
|
|
878
|
+
}
|
|
837
879
|
// Fill-first pick: lexicographically-first eligible account still above the
|
|
838
880
|
// headroom floor. Alias order (not insertion order) — accounts load from a
|
|
839
881
|
// readdir whose order the OS doesn't guarantee, and the operator can control
|
|
840
882
|
// alias names but not readdir. Returns null when every candidate is at/below
|
|
841
|
-
// the floor
|
|
883
|
+
// the floor.
|
|
842
884
|
/**
|
|
843
885
|
* The 7-day reset `expiring-first` orders by, in epoch ms: the seat's own
|
|
844
886
|
* reading while it is still ahead of `now`, else +Infinity (never read, or the
|
|
@@ -851,7 +893,7 @@ export function seatExpiry(account, now = Date.now()) {
|
|
|
851
893
|
function pickExpiringFirst(accounts, family, floor = DEFAULT_POOL_HEADROOM_FLOOR, now = Date.now()) {
|
|
852
894
|
const order = [...accounts].sort((a, b) => (seatExpiry(a, now) - seatExpiry(b, now)) || (a.alias < b.alias ? -1 : a.alias > b.alias ? 1 : 0));
|
|
853
895
|
for (const a of order) {
|
|
854
|
-
if (computeHeadroom(a.rateLimit, family)
|
|
896
|
+
if (isAboveHeadroomFloor(computeHeadroom(a.rateLimit, family), floor))
|
|
855
897
|
return a;
|
|
856
898
|
}
|
|
857
899
|
return null;
|
|
@@ -861,14 +903,13 @@ function pickFillFirst(accounts, family, floor = DEFAULT_POOL_HEADROOM_FLOOR) {
|
|
|
861
903
|
for (const a of accounts) {
|
|
862
904
|
if (best !== null && a.alias >= best.alias)
|
|
863
905
|
continue;
|
|
864
|
-
if (computeHeadroom(a.rateLimit, family)
|
|
906
|
+
if (isAboveHeadroomFloor(computeHeadroom(a.rateLimit, family), floor))
|
|
865
907
|
best = a;
|
|
866
908
|
}
|
|
867
909
|
return best;
|
|
868
910
|
}
|
|
869
911
|
export class AccountPool {
|
|
870
912
|
strategy;
|
|
871
|
-
headroomFloor;
|
|
872
913
|
accounts = new Map();
|
|
873
914
|
queue = [];
|
|
874
915
|
queueMaxSize = 50;
|
|
@@ -877,11 +918,14 @@ export class AccountPool {
|
|
|
877
918
|
sticky = new Map();
|
|
878
919
|
// Amortize the O(n) sticky TTL/orphan sweep — timestamp of the last run.
|
|
879
920
|
lastStickyCleanup = 0;
|
|
880
|
-
constructor(strategy = 'headroom',
|
|
881
921
|
/** Headroom at/below which a seat counts as drained — see DEFAULT_POOL_HEADROOM_FLOOR. */
|
|
882
|
-
headroomFloor
|
|
922
|
+
headroomFloor;
|
|
923
|
+
/** Only an explicitly configured floor is a hard stop; the implicit 2% remains a preference. */
|
|
924
|
+
hardHeadroomFloor;
|
|
925
|
+
constructor(strategy = 'headroom', headroomFloor) {
|
|
883
926
|
this.strategy = strategy;
|
|
884
|
-
this.headroomFloor = headroomFloor;
|
|
927
|
+
this.headroomFloor = headroomFloor ?? DEFAULT_POOL_HEADROOM_FLOOR;
|
|
928
|
+
this.hardHeadroomFloor = headroomFloor !== undefined;
|
|
885
929
|
}
|
|
886
930
|
add(alias, opts) {
|
|
887
931
|
const existing = this.accounts.get(alias);
|
|
@@ -997,11 +1041,13 @@ export class AccountPool {
|
|
|
997
1041
|
: pickExpiringFirst(eligible, family, this.headroomFloor, now);
|
|
998
1042
|
if (first)
|
|
999
1043
|
return first;
|
|
1000
|
-
// Every eligible account is at/below the floor — the terminal state
|
|
1001
|
-
// both strategies share. Fall through to max-headroom so the caller
|
|
1002
|
-
// still gets the least-drained account instead of null.
|
|
1003
1044
|
}
|
|
1004
|
-
|
|
1045
|
+
if (!this.hardHeadroomFloor)
|
|
1046
|
+
return pickMaxHeadroom(eligible, family);
|
|
1047
|
+
const aboveFloor = eligible.filter(a => isAboveHeadroomFloor(computeHeadroom(a.rateLimit, family, now), this.headroomFloor));
|
|
1048
|
+
return aboveFloor.length > 0
|
|
1049
|
+
? pickMaxHeadroom(aboveFloor, family)
|
|
1050
|
+
: pickFloorProbe(eligible, family, this.headroomFloor, now);
|
|
1005
1051
|
}
|
|
1006
1052
|
// No seat is eligible. A seat parked inside a live window is not
|
|
1007
1053
|
// re-probed: its 429 named the reset, the clock has not reached it, and a
|
|
@@ -1042,11 +1088,20 @@ export class AccountPool {
|
|
|
1042
1088
|
// 429 again (dario#1264 review). An auth cool-down or an expired token
|
|
1043
1089
|
// still does NOT count — those are not rate limits and must not be
|
|
1044
1090
|
// reported, or cooled, as if they were.
|
|
1045
|
-
|
|
1091
|
+
const until = all.map((a) => {
|
|
1092
|
+
if (isParkedInLiveWindow(a, now, family)) {
|
|
1093
|
+
return isBucketScopedRejection(a.rateLimit)
|
|
1094
|
+
? (a.rateLimit.parkedBucketsUntil ?? a.rateLimit.reset * 1000)
|
|
1095
|
+
: a.rateLimit.reset * 1000;
|
|
1096
|
+
}
|
|
1097
|
+
if (isCoolingAfterRejection(a, now))
|
|
1098
|
+
return a.rateLimit.cooldownUntil ?? now;
|
|
1099
|
+
if (this.hardHeadroomFloor && isAccountEligible(a, now, family)) {
|
|
1100
|
+
return floorParkedUntil(a.rateLimit, family, this.headroomFloor, now);
|
|
1101
|
+
}
|
|
1046
1102
|
return null;
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
: a.rateLimit.cooldownUntil ?? now));
|
|
1103
|
+
});
|
|
1104
|
+
return until.every((reset) => reset !== null) ? Math.min(...until) : null;
|
|
1050
1105
|
}
|
|
1051
1106
|
/** Seats a rate limit is currently keeping out of rotation (dario#1244, #1264). */
|
|
1052
1107
|
parkedCount(now = Date.now()) {
|
|
@@ -1077,7 +1132,7 @@ export class AccountPool {
|
|
|
1077
1132
|
const bound = this.accounts.get(binding.alias);
|
|
1078
1133
|
if (bound
|
|
1079
1134
|
&& isAccountEligible(bound, now, family)
|
|
1080
|
-
&& computeHeadroom(bound.rateLimit, family)
|
|
1135
|
+
&& isAboveHeadroomFloor(computeHeadroom(bound.rateLimit, family), this.headroomFloor)) {
|
|
1081
1136
|
// Refresh the idle timer. A session that keeps taking turns must never
|
|
1082
1137
|
// be reaped or rebound while active — that would strand its warm prompt
|
|
1083
1138
|
// cache — so the TTL is re-based to now on every hit.
|
|
@@ -1167,7 +1222,12 @@ export class AccountPool {
|
|
|
1167
1222
|
if (first)
|
|
1168
1223
|
return first;
|
|
1169
1224
|
}
|
|
1170
|
-
|
|
1225
|
+
if (!this.hardHeadroomFloor)
|
|
1226
|
+
return pickMaxHeadroom(eligible, family);
|
|
1227
|
+
const aboveFloor = eligible.filter(a => isAboveHeadroomFloor(computeHeadroom(a.rateLimit, family, now), this.headroomFloor));
|
|
1228
|
+
return aboveFloor.length > 0
|
|
1229
|
+
? pickMaxHeadroom(aboveFloor, family)
|
|
1230
|
+
: pickFloorProbe(eligible, family, this.headroomFloor, now);
|
|
1171
1231
|
}
|
|
1172
1232
|
// Mid-flight: the seats a 429 could still hand this request to. A seat
|
|
1173
1233
|
// parked inside a live window is not one of them — on the dario#1244
|
|
@@ -1280,6 +1340,18 @@ export class AccountPool {
|
|
|
1280
1340
|
get(alias) {
|
|
1281
1341
|
return this.accounts.get(alias);
|
|
1282
1342
|
}
|
|
1343
|
+
/** Whether a preferred (but not pinned) seat may bypass normal pool ordering. */
|
|
1344
|
+
canPrefer(account, now = Date.now(), family) {
|
|
1345
|
+
if (!isAccountEligible(account, now, family))
|
|
1346
|
+
return false;
|
|
1347
|
+
if (!this.hardHeadroomFloor)
|
|
1348
|
+
return true;
|
|
1349
|
+
if (isAboveHeadroomFloor(computeHeadroom(account.rateLimit, family, now), this.headroomFloor))
|
|
1350
|
+
return true;
|
|
1351
|
+
// A stale below-floor reading needs one probe to refresh it; a reading
|
|
1352
|
+
// with a known future reset remains hard-parked until that reset.
|
|
1353
|
+
return floorParkedUntil(account.rateLimit, family, this.headroomFloor, now) === null;
|
|
1354
|
+
}
|
|
1283
1355
|
all() {
|
|
1284
1356
|
return [...this.accounts.values()];
|
|
1285
1357
|
}
|
|
@@ -1311,11 +1383,9 @@ export class AccountPool {
|
|
|
1311
1383
|
*/
|
|
1312
1384
|
async waitForAccount() {
|
|
1313
1385
|
const immediate = this.select();
|
|
1314
|
-
if (immediate
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
return immediate;
|
|
1318
|
-
}
|
|
1386
|
+
if (immediate && (this.hardHeadroomFloor
|
|
1387
|
+
|| isAboveHeadroomFloor(computeHeadroom(immediate.rateLimit), this.headroomFloor)))
|
|
1388
|
+
return immediate;
|
|
1319
1389
|
if (this.queue.length >= this.queueMaxSize) {
|
|
1320
1390
|
throw new Error('Queue full — all accounts exhausted');
|
|
1321
1391
|
}
|
|
@@ -1355,8 +1425,10 @@ export class AccountPool {
|
|
|
1355
1425
|
const account = this.select();
|
|
1356
1426
|
if (!account)
|
|
1357
1427
|
break;
|
|
1358
|
-
|
|
1359
|
-
|
|
1428
|
+
// The implicit floor keeps its historical queueing behavior. In hard
|
|
1429
|
+
// mode select() has already applied the floor, including stale probes.
|
|
1430
|
+
if (!this.hardHeadroomFloor
|
|
1431
|
+
&& !isAboveHeadroomFloor(computeHeadroom(account.rateLimit), this.headroomFloor))
|
|
1360
1432
|
break;
|
|
1361
1433
|
const entry = this.queue.shift();
|
|
1362
1434
|
if (entry)
|
package/dist/proxy.d.ts
CHANGED
|
@@ -436,7 +436,8 @@ interface ProxyOptions {
|
|
|
436
436
|
/**
|
|
437
437
|
* Headroom at/below which a seat counts as drained (dario#1333): a sticky
|
|
438
438
|
* session rebinds off it and new conversations skip it. Ratio (`0.05`) or
|
|
439
|
-
* percent (`5%`)
|
|
439
|
+
* percent (`5%`). The implicit 2% default is a routing preference; setting
|
|
440
|
+
* this option explicitly makes the floor a hard stop. Sourced from `--pool-headroom-floor` /
|
|
440
441
|
* `DARIO_POOL_HEADROOM_FLOOR` / config `pool.headroomFloor`.
|
|
441
442
|
*/
|
|
442
443
|
poolHeadroomFloor?: string | number;
|
package/dist/proxy.js
CHANGED
|
@@ -13,7 +13,7 @@ import { CC_TOOL_DEFINITIONS_UNADVERTISABLE, CC_TEMPLATE_PROMPT_BYTES, resolveMa
|
|
|
13
13
|
import { stampCch, hasCchSeed } from './cch.js';
|
|
14
14
|
import { foldTiming, timingHeaders, timingLogFields } from './timing.js';
|
|
15
15
|
import { describeTemplate, detectDrift, checkCCCompat, probeInstalledCCVersion } from './live-fingerprint.js';
|
|
16
|
-
import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, activeParkedBuckets, reconcilePoolAccounts, resolvePoolStrategy, resolvePoolHeadroomFloor,
|
|
16
|
+
import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, activeParkedBuckets, reconcilePoolAccounts, resolvePoolStrategy, resolvePoolHeadroomFloor, parsePoolHeadroomFloor, describePoolStrategy, utilFreshness, rateLimitWindow, accountAction, accountPeers, distinctAccounts, describeRejection, maskEmail } from './pool.js';
|
|
17
17
|
import { backfillIdentity } from './accounts.js';
|
|
18
18
|
import { PoolSync, DEFAULT_POOL_SYNC_INTERVAL_MS } from './pool-sync.js';
|
|
19
19
|
import { Analytics, billingBucketFromClaim, costOfTokensFailClosed, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, CODEX_CLAIM } from './analytics.js';
|
|
@@ -1514,8 +1514,9 @@ export async function startProxy(opts = {}) {
|
|
|
1514
1514
|
const accountsList = await loadAllAccounts();
|
|
1515
1515
|
const poolStrategy = resolvePoolStrategy(opts.poolStrategy);
|
|
1516
1516
|
const poolHeadroomFloor = resolvePoolHeadroomFloor(opts.poolHeadroomFloor);
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1517
|
+
const poolHeadroomFloorExplicit = parsePoolHeadroomFloor(opts.poolHeadroomFloor) !== null;
|
|
1518
|
+
const pool = new AccountPool(poolStrategy, poolHeadroomFloorExplicit ? poolHeadroomFloor : undefined);
|
|
1519
|
+
if (poolHeadroomFloorExplicit) {
|
|
1519
1520
|
console.log(`[dario] Pool headroom floor: ${Math.round(poolHeadroomFloor * 100)}% — a seat at or below it is left alone: sticky sessions rebind off it, new conversations skip it`);
|
|
1520
1521
|
}
|
|
1521
1522
|
// Two aliases that are one account (same OAuth account uuid) are one
|
|
@@ -3391,7 +3392,7 @@ export async function startProxy(opts = {}) {
|
|
|
3391
3392
|
// eligible right now, else the pool picks as usual. Failover
|
|
3392
3393
|
// mid-request is unchanged either way — a preference, not a pin.
|
|
3393
3394
|
const preferredSeat = requestAuth.key?.seat ? (pool.get(requestAuth.key.seat) ?? null) : null;
|
|
3394
|
-
keySeatTaken = preferredSeat !== null &&
|
|
3395
|
+
keySeatTaken = preferredSeat !== null && pool.canPrefer(preferredSeat, Date.now(), requestFamily);
|
|
3395
3396
|
poolAccount = keySeatTaken ? preferredSeat : pool.select(requestFamily);
|
|
3396
3397
|
if (poolAccount)
|
|
3397
3398
|
poolParkedAnnounced = false;
|
|
@@ -35,7 +35,7 @@ ChatGPT plans added with `dario codex add <alias>` pool the same way. The Codex
|
|
|
35
35
|
|
|
36
36
|
Earlier releases placed ChatGPT conversations fill-first in alias order with cool-down eviction only, on the belief that the backend reported nothing before a 429, so a second seat served nothing until the first one declined.
|
|
37
37
|
|
|
38
|
-
`--pool-strategy=fill-first` concentrates new conversations on one seat until it drains, for primary/backup setups; `--pool-strategy=expiring-first` fills in order of each seat's 7-day reset, soonest first, so capacity that expires soonest is spent first. `--pool-headroom-floor=5%` (env `DARIO_POOL_HEADROOM_FLOOR`, config `pool.headroomFloor
|
|
38
|
+
`--pool-strategy=fill-first` concentrates new conversations on one seat until it drains, for primary/backup setups; `--pool-strategy=expiring-first` fills in order of each seat's 7-day reset, soonest first, so capacity that expires soonest is spent first. `--pool-headroom-floor=5%` (env `DARIO_POOL_HEADROOM_FLOOR`, config `pool.headroomFloor`) moves the line at which a seat counts as drained: a sticky session rebinds off it and new conversations skip it once its headroom is at or below the floor, so a seat that answers with API errors in its last percent is left alone before the 429, not at it. Setting the floor explicitly makes it a hard stop; the implicit 2% default remains a routing preference and still serves the least-drained seat when it is the only choice. A hard-parked pool answers 429 locally with `retry-after` at the earliest known reset; a reading with no reset stays probeable so stale utilization cannot park a seat forever. Refresh tokens expire about 28 days after the original grant regardless of rotation, so every seat's grant age is tracked and surfaced in `dario accounts list`, `dario doctor` and `GET /accounts` before it becomes a silent outage. Provision over HTTP with the headless [admin API](admin-api.md); pin one request to one seat with `dario accounts check <alias>` (admin API required: `DARIO_ADMIN=1` and a `DARIO_ADMIN_TOKEN`). Internals and the live `/accounts` + `/analytics` endpoints: [multi-account-pool.md](multi-account-pool.md); covered end-to-end by [`test/pool-e2e.mjs`](../test/pool-e2e.mjs).
|
|
39
39
|
|
|
40
40
|
## One key per developer
|
|
41
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.12.
|
|
3
|
+
"version": "6.12.9",
|
|
4
4
|
"description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, with automatic failover when one hits its limit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
"node": ">=18.0.0"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
|
-
"@types/node": "^26.6.
|
|
91
|
-
"tsx": "^4.
|
|
90
|
+
"@types/node": "^26.6.2",
|
|
91
|
+
"tsx": "^4.23.15",
|
|
92
92
|
"typescript": "^5.7.0"
|
|
93
93
|
}
|
|
94
94
|
}
|