@askalf/dario 6.10.5 → 6.11.0
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 +10 -5
- package/dist/config-file.d.ts +3 -1
- package/dist/config-file.js +2 -1
- package/dist/pool.d.ts +26 -1
- package/dist/pool.js +53 -7
- package/dist/proxy.js +2 -1
- package/dist/tui/tabs/config.js +2 -2
- package/docs/commands.md +1 -1
- package/docs/integrations/cordon.md +2 -2
- package/docs/multi-account-pool.md +3 -1
- package/docs/pool-keys-and-analytics.md +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -520,11 +520,14 @@ async function proxy() {
|
|
|
520
520
|
// `fill-first` concentrates them on the alphabetically-first eligible seat
|
|
521
521
|
// until it drains to the 2% floor, then spills — primary/backup semantics,
|
|
522
522
|
// alias naming (`1-main`, `2-overflow`) is the ordering knob. Sticky
|
|
523
|
-
// bindings behave identically under both.
|
|
523
|
+
// bindings behave identically under both. `expiring-first` is fill-first
|
|
524
|
+
// ordered by each seat's 7-day reset, soonest first: spend the capacity that
|
|
525
|
+
// expires soonest before touching the rest.
|
|
524
526
|
const poolStrategyFromFlag = args.find((a) => a.startsWith('--pool-strategy='))?.split('=')[1];
|
|
525
527
|
if (poolStrategyFromFlag !== undefined
|
|
526
|
-
&& poolStrategyFromFlag !== 'headroom' && poolStrategyFromFlag !== 'fill-first'
|
|
527
|
-
|
|
528
|
+
&& poolStrategyFromFlag !== 'headroom' && poolStrategyFromFlag !== 'fill-first'
|
|
529
|
+
&& poolStrategyFromFlag !== 'expiring-first') {
|
|
530
|
+
console.error(`[dario] Invalid --pool-strategy "${poolStrategyFromFlag}". Must be headroom, fill-first or expiring-first.`);
|
|
528
531
|
process.exit(1);
|
|
529
532
|
}
|
|
530
533
|
const poolStrategy = poolStrategyFromFlag
|
|
@@ -2171,13 +2174,15 @@ async function help() {
|
|
|
2171
2174
|
dario returns 504 "queue-timeout"
|
|
2172
2175
|
(default: 60000).
|
|
2173
2176
|
Env: DARIO_QUEUE_TIMEOUT_MS. (dario#80)
|
|
2174
|
-
--pool-strategy=<headroom|fill-first>
|
|
2177
|
+
--pool-strategy=<headroom|fill-first|expiring-first>
|
|
2175
2178
|
Where new conversations land in a multi-
|
|
2176
2179
|
account pool. headroom (default) spreads
|
|
2177
2180
|
them to the seat with the most slack;
|
|
2178
2181
|
fill-first concentrates them on the
|
|
2179
2182
|
alphabetically-first eligible seat until
|
|
2180
|
-
it drains to the
|
|
2183
|
+
it drains to the floor, then spills;
|
|
2184
|
+
expiring-first does the same in order of
|
|
2185
|
+
each seat's 7-day reset, soonest first.
|
|
2181
2186
|
Sticky bindings are unaffected.
|
|
2182
2187
|
Env: DARIO_POOL_STRATEGY.
|
|
2183
2188
|
--pool-headroom-floor=<ratio|percent>
|
package/dist/config-file.d.ts
CHANGED
|
@@ -86,8 +86,10 @@ export interface DarioConfig {
|
|
|
86
86
|
* most headroom; `fill-first` concentrates them on the alphabetically-
|
|
87
87
|
* first eligible seat until it drains to the 2% floor, then spills to
|
|
88
88
|
* the next — primary/backup semantics, alias order is the knob.
|
|
89
|
+
* `expiring-first` is fill-first ordered by each seat's 7-day reset,
|
|
90
|
+
* soonest first.
|
|
89
91
|
*/
|
|
90
|
-
strategy?: 'headroom' | 'fill-first';
|
|
92
|
+
strategy?: 'headroom' | 'fill-first' | 'expiring-first';
|
|
91
93
|
/** Headroom at/below which a seat counts as drained — ratio 0.02..0.5 (dario#1333). */
|
|
92
94
|
headroomFloor?: number;
|
|
93
95
|
};
|
package/dist/config-file.js
CHANGED
|
@@ -308,7 +308,8 @@ function sanitize(parsed) {
|
|
|
308
308
|
}
|
|
309
309
|
if (isPlainObject(parsed.pool)) {
|
|
310
310
|
out.pool = {};
|
|
311
|
-
if (parsed.pool.strategy === 'headroom' || parsed.pool.strategy === 'fill-first'
|
|
311
|
+
if (parsed.pool.strategy === 'headroom' || parsed.pool.strategy === 'fill-first'
|
|
312
|
+
|| parsed.pool.strategy === 'expiring-first') {
|
|
312
313
|
out.pool.strategy = parsed.pool.strategy;
|
|
313
314
|
}
|
|
314
315
|
if (typeof parsed.pool.headroomFloor === 'number' && Number.isFinite(parsed.pool.headroomFloor)) {
|
package/dist/pool.d.ts
CHANGED
|
@@ -33,6 +33,14 @@ export interface RateLimitSnapshot {
|
|
|
33
33
|
* were on the response.
|
|
34
34
|
*/
|
|
35
35
|
perModel7d: Record<string, number>;
|
|
36
|
+
/**
|
|
37
|
+
* Epoch SECONDS the seat's own 7-day window resets at, from
|
|
38
|
+
* `anthropic-ratelimit-unified-7d-reset`; 0 when the response did not carry
|
|
39
|
+
* it. Distinct from `reset`, which is the representative claim's reset (on
|
|
40
|
+
* most responses the five-hour one). `expiring-first` orders seats by it:
|
|
41
|
+
* capacity that expires soonest is spent first.
|
|
42
|
+
*/
|
|
43
|
+
reset7d?: number;
|
|
36
44
|
overageUtil: number;
|
|
37
45
|
claim: string;
|
|
38
46
|
reset: number;
|
|
@@ -385,7 +393,18 @@ export interface PoolStatus {
|
|
|
385
393
|
* pick the fill order. Sticky bindings behave identically in both modes;
|
|
386
394
|
* strategy only decides where UNBOUND (new) conversations land.
|
|
387
395
|
*/
|
|
388
|
-
export type PoolStrategy = 'headroom' | 'fill-first';
|
|
396
|
+
export type PoolStrategy = 'headroom' | 'fill-first' | 'expiring-first';
|
|
397
|
+
/**
|
|
398
|
+
* `expiring-first` — fill-first, ordered by WHEN each seat's capacity expires
|
|
399
|
+
* instead of by alias: new conversations fill the seat whose 7-day window
|
|
400
|
+
* resets soonest until it drains to the floor, then spill to the next-soonest.
|
|
401
|
+
* Subscription capacity is use-it-or-lose-it; spending the soonest-expiring
|
|
402
|
+
* first is what gets the most of it used. Fleet box, 2026-09-22: seat pro1 got
|
|
403
|
+
* a fresh limit reset expiring in ~50h while login's window had ~70h left, and
|
|
404
|
+
* alias-ordered fill-first kept every new conversation on login. A seat with
|
|
405
|
+
* no 7d reading yet (or whose window has already rolled) goes last; ties break
|
|
406
|
+
* by alias. Sticky bindings behave identically.
|
|
407
|
+
*/
|
|
389
408
|
/**
|
|
390
409
|
* Resolve the pool strategy from an explicit value (CLI flag / config file,
|
|
391
410
|
* already precedence-merged by the caller) with `DARIO_POOL_STRATEGY` as
|
|
@@ -550,6 +569,12 @@ export declare function parsePoolHeadroomFloor(value: string | number | null | u
|
|
|
550
569
|
* default, matching `resolvePoolStrategy`.
|
|
551
570
|
*/
|
|
552
571
|
export declare function resolvePoolHeadroomFloor(explicit?: string | number | null, env?: NodeJS.ProcessEnv): number;
|
|
572
|
+
/**
|
|
573
|
+
* The 7-day reset `expiring-first` orders by, in epoch ms: the seat's own
|
|
574
|
+
* reading while it is still ahead of `now`, else +Infinity (never read, or the
|
|
575
|
+
* window has rolled and the next reset is unknown), which sorts it last.
|
|
576
|
+
*/
|
|
577
|
+
export declare function seatExpiry(account: PoolAccount, now?: number): number;
|
|
553
578
|
export declare class AccountPool {
|
|
554
579
|
private readonly strategy;
|
|
555
580
|
/** Headroom at/below which a seat counts as drained — see DEFAULT_POOL_HEADROOM_FLOOR. */
|
package/dist/pool.js
CHANGED
|
@@ -319,7 +319,15 @@ export function familyParkedOnBuckets(rl, family, now = Date.now()) {
|
|
|
319
319
|
}
|
|
320
320
|
/** `next` with the parked buckets `prev` still holds carried forward (see `RateLimitSnapshot.parkedBuckets`). */
|
|
321
321
|
export function withParkedBuckets(prev, next, now = Date.now()) {
|
|
322
|
-
|
|
322
|
+
// A reading that MEASURED a parked bucket below the threshold is that bucket's
|
|
323
|
+
// own answer: it has room again before its stated reset (a limit reset on the
|
|
324
|
+
// account, a plan upgrade), so its park lifts. A reading without the bucket's
|
|
325
|
+
// header (an Opus 200 carries no 7d_oi) says nothing about it and keeps it.
|
|
326
|
+
// Fleet box, 2026-09-22: pro1's oi was reset by the operator; a pinned Fable
|
|
327
|
+
// 200 read 7d_oi 0.0 allowed, and the park still held Fable off pro1 until the
|
|
328
|
+
// 429's reset three days out.
|
|
329
|
+
const parked = activeParkedBuckets(prev, now)
|
|
330
|
+
.filter((b) => !(b in next.perModel7d && next.perModel7d[b] < 0.99));
|
|
323
331
|
if (parked.length === 0)
|
|
324
332
|
return next;
|
|
325
333
|
return { ...next, parkedBuckets: parked, parkedBucketsUntil: prev.parkedBucketsUntil };
|
|
@@ -392,6 +400,17 @@ export function accountAction(account, now = Date.now()) {
|
|
|
392
400
|
return 'wait';
|
|
393
401
|
return 'none';
|
|
394
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* `expiring-first` — fill-first, ordered by WHEN each seat's capacity expires
|
|
405
|
+
* instead of by alias: new conversations fill the seat whose 7-day window
|
|
406
|
+
* resets soonest until it drains to the floor, then spill to the next-soonest.
|
|
407
|
+
* Subscription capacity is use-it-or-lose-it; spending the soonest-expiring
|
|
408
|
+
* first is what gets the most of it used. Fleet box, 2026-09-22: seat pro1 got
|
|
409
|
+
* a fresh limit reset expiring in ~50h while login's window had ~70h left, and
|
|
410
|
+
* alias-ordered fill-first kept every new conversation on login. A seat with
|
|
411
|
+
* no 7d reading yet (or whose window has already rolled) goes last; ties break
|
|
412
|
+
* by alias. Sticky bindings behave identically.
|
|
413
|
+
*/
|
|
395
414
|
/**
|
|
396
415
|
* Resolve the pool strategy from an explicit value (CLI flag / config file,
|
|
397
416
|
* already precedence-merged by the caller) with `DARIO_POOL_STRATEGY` as
|
|
@@ -404,7 +423,7 @@ export function resolvePoolStrategy(explicit, env = process.env) {
|
|
|
404
423
|
if (typeof c !== 'string')
|
|
405
424
|
continue;
|
|
406
425
|
const s = c.trim().toLowerCase();
|
|
407
|
-
if (s === 'headroom' || s === 'fill-first')
|
|
426
|
+
if (s === 'headroom' || s === 'fill-first' || s === 'expiring-first')
|
|
408
427
|
return s;
|
|
409
428
|
}
|
|
410
429
|
return 'headroom';
|
|
@@ -526,6 +545,7 @@ export function parseRateLimits(headers, family) {
|
|
|
526
545
|
status: get('status') || 'unknown',
|
|
527
546
|
util5h: parseFloat(get('5h-utilization')) || 0,
|
|
528
547
|
util7d: parseFloat(get('7d-utilization')) || 0,
|
|
548
|
+
reset7d: parseInt(get('7d-reset'), 10) || 0,
|
|
529
549
|
perModel7d,
|
|
530
550
|
overageUtil: parseFloat(get('overage-utilization')) || 0,
|
|
531
551
|
claim: get('representative-claim') || 'unknown',
|
|
@@ -789,6 +809,23 @@ function pickMaxHeadroom(accounts, family) {
|
|
|
789
809
|
// readdir whose order the OS doesn't guarantee, and the operator can control
|
|
790
810
|
// alias names but not readdir. Returns null when every candidate is at/below
|
|
791
811
|
// the floor so the caller can fall back to max-headroom.
|
|
812
|
+
/**
|
|
813
|
+
* The 7-day reset `expiring-first` orders by, in epoch ms: the seat's own
|
|
814
|
+
* reading while it is still ahead of `now`, else +Infinity (never read, or the
|
|
815
|
+
* window has rolled and the next reset is unknown), which sorts it last.
|
|
816
|
+
*/
|
|
817
|
+
export function seatExpiry(account, now = Date.now()) {
|
|
818
|
+
const r = (account.rateLimit.reset7d ?? 0) * 1000;
|
|
819
|
+
return r > now ? r : Number.POSITIVE_INFINITY;
|
|
820
|
+
}
|
|
821
|
+
function pickExpiringFirst(accounts, family, floor = DEFAULT_POOL_HEADROOM_FLOOR, now = Date.now()) {
|
|
822
|
+
const order = [...accounts].sort((a, b) => (seatExpiry(a, now) - seatExpiry(b, now)) || (a.alias < b.alias ? -1 : a.alias > b.alias ? 1 : 0));
|
|
823
|
+
for (const a of order) {
|
|
824
|
+
if (computeHeadroom(a.rateLimit, family) > floor)
|
|
825
|
+
return a;
|
|
826
|
+
}
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
792
829
|
function pickFillFirst(accounts, family, floor = DEFAULT_POOL_HEADROOM_FLOOR) {
|
|
793
830
|
let best = null;
|
|
794
831
|
for (const a of accounts) {
|
|
@@ -924,8 +961,10 @@ export class AccountPool {
|
|
|
924
961
|
const all = [...this.accounts.values()];
|
|
925
962
|
const eligible = all.filter(a => isAccountEligible(a, now, family));
|
|
926
963
|
if (eligible.length > 0) {
|
|
927
|
-
if (this.strategy === 'fill-first') {
|
|
928
|
-
const first =
|
|
964
|
+
if (this.strategy === 'fill-first' || this.strategy === 'expiring-first') {
|
|
965
|
+
const first = this.strategy === 'fill-first'
|
|
966
|
+
? pickFillFirst(eligible, family, this.headroomFloor)
|
|
967
|
+
: pickExpiringFirst(eligible, family, this.headroomFloor, now);
|
|
929
968
|
if (first)
|
|
930
969
|
return first;
|
|
931
970
|
// Every eligible account is at/below the floor — the terminal state
|
|
@@ -1091,8 +1130,10 @@ export class AccountPool {
|
|
|
1091
1130
|
// after a 429 is the next alias in line, not the max-headroom seat —
|
|
1092
1131
|
// otherwise a single failover would defeat the concentration the
|
|
1093
1132
|
// strategy exists to provide.
|
|
1094
|
-
if (this.strategy === 'fill-first') {
|
|
1095
|
-
const first =
|
|
1133
|
+
if (this.strategy === 'fill-first' || this.strategy === 'expiring-first') {
|
|
1134
|
+
const first = this.strategy === 'fill-first'
|
|
1135
|
+
? pickFillFirst(eligible, family, this.headroomFloor)
|
|
1136
|
+
: pickExpiringFirst(eligible, family, this.headroomFloor, now);
|
|
1096
1137
|
if (first)
|
|
1097
1138
|
return first;
|
|
1098
1139
|
}
|
|
@@ -1112,7 +1153,12 @@ export class AccountPool {
|
|
|
1112
1153
|
const account = this.accounts.get(alias);
|
|
1113
1154
|
if (!account)
|
|
1114
1155
|
return;
|
|
1115
|
-
|
|
1156
|
+
const at = snapshot.updatedAt || Date.now();
|
|
1157
|
+
// A reading without the 7d-reset header does not un-learn it while it is
|
|
1158
|
+
// still ahead: expiring-first would otherwise send the seat to the back.
|
|
1159
|
+
const prevReset7d = account.rateLimit.reset7d ?? 0;
|
|
1160
|
+
const next = !snapshot.reset7d && prevReset7d * 1000 > at ? { ...snapshot, reset7d: prevReset7d } : snapshot;
|
|
1161
|
+
account.rateLimit = withParkedBuckets(account.rateLimit, withBoundBuckets(account.rateLimit, next), at);
|
|
1116
1162
|
account.adoptedFrom = undefined;
|
|
1117
1163
|
account.requestCount++;
|
|
1118
1164
|
}
|
package/dist/proxy.js
CHANGED
|
@@ -1522,7 +1522,8 @@ export async function startProxy(opts = {}) {
|
|
|
1522
1522
|
console.error('[dario] --pool-shared-state needs DARIO_REFRESH_LOCK_URL (the lock service carries the shared state) — running with this instance\'s own state');
|
|
1523
1523
|
}
|
|
1524
1524
|
if (poolStrategy !== 'headroom') {
|
|
1525
|
-
|
|
1525
|
+
const order = poolStrategy === 'expiring-first' ? 'the seat whose 7-day window resets soonest' : 'the alphabetically-first seat';
|
|
1526
|
+
console.log(` Pool strategy: ${poolStrategy} (new conversations fill ${order}, spill at the ${Math.round(pool.headroomFloor * 100)}% floor)`);
|
|
1526
1527
|
}
|
|
1527
1528
|
if (poolSync) {
|
|
1528
1529
|
console.log(` Pool shared state: on (instance ${poolSync.instance}, via ${lockUrl}, pulling peers every ${poolSync.intervalMs}ms; fails open)`);
|
package/dist/tui/tabs/config.js
CHANGED
|
@@ -38,7 +38,7 @@ const FIELDS = [
|
|
|
38
38
|
{ path: 'thinkTime.maxMs', label: 'Think-time cap (ms)', type: 'number', hint: 'upper bound for the whole formula' },
|
|
39
39
|
{ path: 'sessionStart.minMs', label: 'Session-start min', type: 'number', hint: 'first-request delay floor' },
|
|
40
40
|
{ path: 'sessionStart.jitterMs', label: 'Session-start jitter', type: 'number' },
|
|
41
|
-
{ path: 'pool.strategy', label: 'Pool strategy', type: 'string', hint: '"headroom" (default) or "fill-first"' },
|
|
41
|
+
{ path: 'pool.strategy', label: 'Pool strategy', type: 'string', hint: '"headroom" (default), "expiring-first" or "fill-first"' },
|
|
42
42
|
// ── Overage-guard (v4.1, dario#288) ─────────────────────────
|
|
43
43
|
{ path: 'overageGuard.enabled', label: 'Overage-guard', type: 'bool', hint: 'halt proxy on any representative-claim=overage' },
|
|
44
44
|
{ path: 'overageGuard.behavior', label: 'Overage behavior', type: 'string', hint: '"halt" (default) or "warn"' },
|
|
@@ -271,7 +271,7 @@ function commitEdit(state) {
|
|
|
271
271
|
*/
|
|
272
272
|
const STRING_ENUMS = {
|
|
273
273
|
'overageGuard.behavior': ['halt', 'warn'],
|
|
274
|
-
'pool.strategy': ['headroom', 'fill-first'],
|
|
274
|
+
'pool.strategy': ['headroom', 'fill-first', 'expiring-first'],
|
|
275
275
|
};
|
|
276
276
|
function doSave(state) {
|
|
277
277
|
try {
|
package/docs/commands.md
CHANGED
|
@@ -51,7 +51,7 @@ This page is the per-flag reference. For environment variables grouped by task
|
|
|
51
51
|
| `--session-rotate-jitter=<ms>` / `DARIO_SESSION_JITTER_MS` | Jitter sampled once per session at creation — hides the exact idle floor. (v3.28) | `0` |
|
|
52
52
|
| `--session-max-age=<ms>` / `DARIO_SESSION_MAX_AGE_MS` | Hard ceiling on a session-id's lifetime regardless of activity. (v3.28) | off |
|
|
53
53
|
| `--session-per-client` / `DARIO_SESSION_PER_CLIENT=1` | Split session-id registry by a per-client header so multi-UI fan-out doesn't collapse onto one id. (v3.28) | off |
|
|
54
|
-
| `--pool-strategy=<headroom\|fill-first>` / `DARIO_POOL_STRATEGY` | Where new conversations land in a multi-account pool. `headroom` spreads them to the seat with the most slack; `fill-first` concentrates them on the alphabetically-first eligible seat until it drains to the
|
|
54
|
+
| `--pool-strategy=<headroom\|fill-first\|expiring-first>` / `DARIO_POOL_STRATEGY` | Where new conversations land in a multi-account pool. `headroom` spreads them to the seat with the most slack; `fill-first` concentrates them on the alphabetically-first eligible seat until it drains to the floor, then spills to the next — primary/backup semantics, alias naming (`1-main`, `2-overflow`) picks the fill order; `expiring-first` does the same in order of each seat's 7-day reset, soonest first, so capacity that expires soonest is spent first. Sticky bindings behave identically under all three. See [Multi-account pool](./multi-account-pool.md#routing-strategy). | `headroom` |
|
|
55
55
|
| `--pool-fallback=<models>` / `DARIO_POOL_FALLBACK` / config `poolFallback.model` | Strictly opt-in. When every pool seat is drained or in auth cool-down (at selection, or mid-flight on a 429 with no peer left), serve the request as `<model>` from whichever provider can, instead of surfacing the 429/503. Accepts a **chain** — `gpt-5.6-sol,claude-sonnet-5` — read left to right, each provider taking the first entry it can serve. A Codex/ChatGPT subscription that lists the model is preferred (no per-token cost) and works on **both** wire shapes; otherwise a configured openai-compat backend, which is still OpenAI-path only (no Messages translation on that route). A chain also makes failover **symmetric**: a rate-limited or failing subscription hands the request back to the Claude pool. Only a 429/5xx fails over — a 400 surfaces, since a bad request would just reproduce itself elsewhere. Every substituted response carries `x-dario-pool-fallback: <model>` — never silent. Needs a Codex account (`dario add altman`) or a backend (`dario backend add …`); `dario doctor` reports it as INERT with neither. Empty pool still 503s (setup error, not traffic to re-bill). Empty flag value disables, overriding env + config. See [Pool-exhausted fallback](./multi-account-pool.md#pool-exhausted-fallback). | off |
|
|
56
56
|
| `--system-prompt=<verbatim\|partial\|aggressive\|filepath>` / `DARIO_SYSTEM_PROMPT` | System-prompt mode for outbound CC-shaped requests. `partial` strips behavioral constraints (Tone-and-style, Text-output, scope/verbosity/comment bullets) for ~1.2–2.8× output capability on open-ended work. `aggressive` adds prompt-level RLHF restatement removal (<3% over partial — alignment is RLHF-trained). `<filepath>` fully replaces the slot with file contents. Empirically validated as unfingerprinted by the billing classifier — see [`system-prompt.md`](./system-prompt.md) and [`research/system-prompt-classifier-study.md`](./research/system-prompt-classifier-study.md). (v3.34) | `verbatim` |
|
|
57
57
|
| `--upstream-proxy=<url>` / `--via=<url>` / `DARIO_UPSTREAM_PROXY` | Route dario's outbound fetches (api.anthropic.com, OpenAI-compat backends, OAuth) through an HTTP/HTTPS proxy. Pair with the HTTP proxy mode of a VPN provider (Mullvad, AirVPN), a corporate proxy, privoxy/Tor, etc. Localhost calls bypass. Requires Bun runtime; SOCKS5 not supported. Full provider matrix + setup in [`vpn-routing.md`](./vpn-routing.md). (v3.35) | unset |
|
|
@@ -24,8 +24,8 @@ services:
|
|
|
24
24
|
expose: ["3456"]
|
|
25
25
|
|
|
26
26
|
cordon:
|
|
27
|
-
# multi-arch, Sigstore-attested: gh attestation verify oci://ghcr.io/askalf/cordon:v0.
|
|
28
|
-
image: ghcr.io/askalf/cordon:v0.
|
|
27
|
+
# multi-arch, Sigstore-attested: gh attestation verify oci://ghcr.io/askalf/cordon:v0.3.0 --repo askalf/cordon
|
|
28
|
+
image: ghcr.io/askalf/cordon:v0.3.0
|
|
29
29
|
environment:
|
|
30
30
|
ANTHROPIC_BASE: http://dario:3456
|
|
31
31
|
OPENAI_BASE: http://dario:3456
|
|
@@ -29,7 +29,9 @@ Two situations where that beats spreading:
|
|
|
29
29
|
- **Primary/backup seats.** A `z-backup` account stays completely untouched — fresh 5h and 7d windows — until `a-main` is actually drained. Headroom spreading would nibble at both from the first request.
|
|
30
30
|
- **Cache concentration.** Every fresh conversation lands where the prompt-cache pressure already is, so the spill seat's windows are fully fresh when the primary hits its wall.
|
|
31
31
|
|
|
32
|
-
Alias order is the operator's knob: name seats `1-main` / `2-overflow` to pick the fill order. Strategy only decides where **unbound** conversations land — sticky bindings (below) behave identically in
|
|
32
|
+
Alias order is the operator's knob: name seats `1-main` / `2-overflow` to pick the fill order. Strategy only decides where **unbound** conversations land — sticky bindings (below) behave identically in every mode, and a conversation bound to a seat stays there until that seat is rejected, expiring, or under the floor.
|
|
33
|
+
|
|
34
|
+
`--pool-strategy=expiring-first` is fill-first ordered by **when each seat's capacity expires** instead of by alias: new conversations fill the seat whose 7-day window resets soonest (`anthropic-ratelimit-unified-7d-reset`) until it drains to the floor, then spill to the next-soonest; failover follows the same order. Subscription capacity is use-it-or-lose-it, so this is the order that gets the most of it used — the case it was built for is a seat that just got a limit reset, or joined the pool mid-week, sitting beside one whose window has days left. A seat with no 7-day reading yet, or whose window has already rolled, goes last; ties break by alias, so a pool with no readings behaves exactly like fill-first.
|
|
33
35
|
|
|
34
36
|
## Session stickiness
|
|
35
37
|
|
|
@@ -21,7 +21,7 @@ Three things it does that a round-robin doesn't:
|
|
|
21
21
|
|
|
22
22
|
<img src="../.github/readme/pool.jpg" alt="Three pooled seats, work, personal and side, each with a headroom bar. dario routes the request to the seat with the most headroom." width="100%">
|
|
23
23
|
|
|
24
|
-
`--pool-strategy=fill-first` concentrates new conversations on one seat until it drains, for primary/backup setups. `--pool-headroom-floor=5%` (env `DARIO_POOL_HEADROOM_FLOOR`, config `pool.headroomFloor`; default 2%) 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. 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).
|
|
24
|
+
`--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`; default 2%) 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. 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).
|
|
25
25
|
|
|
26
26
|
## One key per developer
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.11.0",
|
|
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": {
|