@askalf/dario 6.11.0 → 6.11.1
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/README.md +1 -1
- package/dist/cli.js +6 -1
- package/dist/doctor-core.js +11 -5
- package/dist/pool.d.ts +26 -1
- package/dist/pool.js +32 -0
- package/dist/proxy.js +12 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Prefer Docker? `ghcr.io/askalf/dario:latest` — multi-arch (`amd64` + `arm64`),
|
|
|
64
64
|
- **Every tool, one URL.** No per-tool keys and no second bill: anything with a base-URL setting runs on the plan you already pay for. [Per-tool setup](docs/tools.md)
|
|
65
65
|
- **Either plan, either wire shape.** Your ChatGPT plan behind Claude Code and the Anthropic SDKs; Codex CLI on your Claude plan. [Two plans](docs/two-plans.md)
|
|
66
66
|
- **Failover that finishes the answer.** A 429 on one plan is re-served by the other, and a stream that dies mid-answer is completed on the same model first. [Failover](docs/two-plans.md#failover-between-subscriptions)
|
|
67
|
-
- **Many seats, one endpoint.** Per-model headroom routing, session-sticky prompt cache, in-flight 429 failover across every seat you hold. [The pool](docs/pool-keys-and-analytics.md)
|
|
67
|
+
- **Many seats, one endpoint.** Per-model headroom routing, session-sticky prompt cache, in-flight 429 failover across every seat you hold. New conversations go to the seat with the most headroom by default; `--pool-strategy=fill-first` fills seats in alias order and `expiring-first` spends the seat whose weekly window resets soonest. The active strategy shows in the startup banner, `GET /status`, `dario doctor` and `dario accounts list`. [The pool](docs/pool-keys-and-analytics.md)
|
|
68
68
|
- **One key per developer.** Named keys with a preferred seat, a model allowlist and a daily budget. [Keys](docs/pool-keys-and-analytics.md#one-key-per-developer)
|
|
69
69
|
- **Knows what it saved you.** A ledger prices every request at API rates; see it in the TUI, `/analytics`, Prometheus `/metrics` and a per-request timing split. [Analytics](docs/pool-keys-and-analytics.md#what-it-would-have-cost)
|
|
70
70
|
- **Tracks Claude Code on its own.** Eleven unattended watchers catch wire-shape drift and ship the fix, usually the same day. [Drift](docs/tracking-claude-code.md)
|
package/dist/cli.js
CHANGED
|
@@ -52,7 +52,7 @@ function readBudgetFlags(args) {
|
|
|
52
52
|
return budget;
|
|
53
53
|
}
|
|
54
54
|
import { loadAllAccounts as loadAllAccountsForIdentity, regenerateClientIdentity } from './accounts.js';
|
|
55
|
-
import { maskEmail, parsePoolHeadroomFloor } from './pool.js';
|
|
55
|
+
import { maskEmail, parsePoolHeadroomFloor, configuredPoolRouting, describePoolStrategy } from './pool.js';
|
|
56
56
|
import { realpathSync, readFileSync } from 'node:fs';
|
|
57
57
|
import { join } from 'node:path';
|
|
58
58
|
import { homedir } from 'node:os';
|
|
@@ -1344,6 +1344,11 @@ async function accounts() {
|
|
|
1344
1344
|
const loaded = await loadAllAccounts();
|
|
1345
1345
|
const now = Date.now();
|
|
1346
1346
|
console.log(` Pool of ${aliases.length} (${aliases.length === 1 ? '1 account' : aliases.length + ' accounts'})`);
|
|
1347
|
+
{
|
|
1348
|
+
const { loadConfig } = await import('./config-file.js');
|
|
1349
|
+
const routing = configuredPoolRouting(loadConfig().config.pool);
|
|
1350
|
+
console.log(` Routing: ${describePoolStrategy(routing.strategy, routing.headroomFloor)}`);
|
|
1351
|
+
}
|
|
1347
1352
|
if (aliases.length === 1) {
|
|
1348
1353
|
console.log(' (A pool of one — add another with `dario accounts add <alias>` to load-balance.)');
|
|
1349
1354
|
}
|
package/dist/doctor-core.js
CHANGED
|
@@ -1132,8 +1132,13 @@ export async function runChecks(opts = {}) {
|
|
|
1132
1132
|
// (v5.0): it confirms the sole account is eligible, not rejected.
|
|
1133
1133
|
if (aliases.length >= 1) {
|
|
1134
1134
|
try {
|
|
1135
|
-
const { AccountPool } = await import('./pool.js');
|
|
1136
|
-
const
|
|
1135
|
+
const { AccountPool, configuredPoolRouting, describePoolStrategy } = await import('./pool.js');
|
|
1136
|
+
const { loadConfig } = await import('./config-file.js');
|
|
1137
|
+
// The preview must use the strategy the proxy will run, not the
|
|
1138
|
+
// default: `new AccountPool()` here always predicted a max-headroom
|
|
1139
|
+
// pick, which is the wrong seat on a fill-first pool.
|
|
1140
|
+
const routing = configuredPoolRouting(loadConfig().config.pool);
|
|
1141
|
+
const pool = new AccountPool(routing.strategy, routing.headroomFloor);
|
|
1137
1142
|
for (const acc of loaded) {
|
|
1138
1143
|
pool.add(acc.alias, {
|
|
1139
1144
|
accessToken: acc.accessToken,
|
|
@@ -1148,9 +1153,10 @@ export async function runChecks(opts = {}) {
|
|
|
1148
1153
|
checks.push({
|
|
1149
1154
|
status: 'info',
|
|
1150
1155
|
label: 'Pool routing',
|
|
1151
|
-
detail: next
|
|
1152
|
-
? `next: ${next.alias} (
|
|
1153
|
-
: `no eligible account — all rejected or near-expiry (${ps.exhausted}/${ps.accounts} exhausted)
|
|
1156
|
+
detail: (next
|
|
1157
|
+
? `next: ${next.alias} (${ps.healthy}/${ps.accounts} healthy)`
|
|
1158
|
+
: `no eligible account — all rejected or near-expiry (${ps.exhausted}/${ps.accounts} exhausted)`)
|
|
1159
|
+
+ ` · ${describePoolStrategy(routing.strategy, routing.headroomFloor)} (env/config; a running proxy's --pool-strategy flag shows in GET /status)`,
|
|
1154
1160
|
});
|
|
1155
1161
|
}
|
|
1156
1162
|
catch (err) {
|
package/dist/pool.d.ts
CHANGED
|
@@ -369,6 +369,10 @@ export declare function isProbeable(account: PoolAccount, now?: number, family?:
|
|
|
369
369
|
*/
|
|
370
370
|
export declare function accountAction(account: PoolAccount, now?: number): 'none' | 'wait' | 'regrant';
|
|
371
371
|
export interface PoolStatus {
|
|
372
|
+
/** The routing strategy in force for new conversations. */
|
|
373
|
+
strategy: PoolStrategy;
|
|
374
|
+
/** Headroom at/below which a seat counts as drained (ratio). */
|
|
375
|
+
headroomFloor: number;
|
|
372
376
|
accounts: number;
|
|
373
377
|
healthy: number;
|
|
374
378
|
exhausted: number;
|
|
@@ -413,6 +417,27 @@ export type PoolStrategy = 'headroom' | 'fill-first' | 'expiring-first';
|
|
|
413
417
|
* in this codebase (see resolveSessionRotationConfig).
|
|
414
418
|
*/
|
|
415
419
|
export declare function resolvePoolStrategy(explicit?: string | null, env?: NodeJS.ProcessEnv): PoolStrategy;
|
|
420
|
+
/**
|
|
421
|
+
* One line saying where new conversations go, for every surface that names the
|
|
422
|
+
* strategy (startup banner, `dario doctor`, `dario accounts list`). The running
|
|
423
|
+
* strategy used to be invisible: the banner printed it only when it was NOT the
|
|
424
|
+
* default, and nothing else reported it, so a deployment whose compose file
|
|
425
|
+
* overrode the default ran that way unnoticed (fleet box, 2026-09-22).
|
|
426
|
+
*/
|
|
427
|
+
export declare function describePoolStrategy(strategy: PoolStrategy, floor?: number): string;
|
|
428
|
+
/**
|
|
429
|
+
* The strategy and floor a proxy started with no flags would run with, for
|
|
430
|
+
* commands that do not run the proxy (`doctor`, `accounts list`): env first,
|
|
431
|
+
* then the config file, then the defaults — the proxy's own order minus the
|
|
432
|
+
* CLI flag, which only the running process knows (GET /status reports it).
|
|
433
|
+
*/
|
|
434
|
+
export declare function configuredPoolRouting(file: {
|
|
435
|
+
strategy?: string;
|
|
436
|
+
headroomFloor?: number;
|
|
437
|
+
} | undefined, env?: NodeJS.ProcessEnv): {
|
|
438
|
+
strategy: PoolStrategy;
|
|
439
|
+
headroomFloor: number;
|
|
440
|
+
};
|
|
416
441
|
/**
|
|
417
442
|
* Which wire buckets bind which model family, when the wire does not say it
|
|
418
443
|
* by name. `7d_sonnet` names its family; `7d_oi` does not — it is the plan's
|
|
@@ -576,7 +601,7 @@ export declare function resolvePoolHeadroomFloor(explicit?: string | number | nu
|
|
|
576
601
|
*/
|
|
577
602
|
export declare function seatExpiry(account: PoolAccount, now?: number): number;
|
|
578
603
|
export declare class AccountPool {
|
|
579
|
-
|
|
604
|
+
readonly strategy: PoolStrategy;
|
|
580
605
|
/** Headroom at/below which a seat counts as drained — see DEFAULT_POOL_HEADROOM_FLOOR. */
|
|
581
606
|
readonly headroomFloor: number;
|
|
582
607
|
private accounts;
|
package/dist/pool.js
CHANGED
|
@@ -428,6 +428,36 @@ export function resolvePoolStrategy(explicit, env = process.env) {
|
|
|
428
428
|
}
|
|
429
429
|
return 'headroom';
|
|
430
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* One line saying where new conversations go, for every surface that names the
|
|
433
|
+
* strategy (startup banner, `dario doctor`, `dario accounts list`). The running
|
|
434
|
+
* strategy used to be invisible: the banner printed it only when it was NOT the
|
|
435
|
+
* default, and nothing else reported it, so a deployment whose compose file
|
|
436
|
+
* overrode the default ran that way unnoticed (fleet box, 2026-09-22).
|
|
437
|
+
*/
|
|
438
|
+
export function describePoolStrategy(strategy, floor = DEFAULT_POOL_HEADROOM_FLOOR) {
|
|
439
|
+
const pct = `${Math.round(floor * 100)}% floor`;
|
|
440
|
+
switch (strategy) {
|
|
441
|
+
case 'fill-first':
|
|
442
|
+
return `fill-first — new conversations fill the alphabetically-first seat until the ${pct}, then spill`;
|
|
443
|
+
case 'expiring-first':
|
|
444
|
+
return `expiring-first — new conversations fill the seat whose 7-day window resets soonest until the ${pct}, then spill`;
|
|
445
|
+
default:
|
|
446
|
+
return `headroom (default) — new conversations go to the seat with the most headroom; ${pct}`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* The strategy and floor a proxy started with no flags would run with, for
|
|
451
|
+
* commands that do not run the proxy (`doctor`, `accounts list`): env first,
|
|
452
|
+
* then the config file, then the defaults — the proxy's own order minus the
|
|
453
|
+
* CLI flag, which only the running process knows (GET /status reports it).
|
|
454
|
+
*/
|
|
455
|
+
export function configuredPoolRouting(file, env = process.env) {
|
|
456
|
+
return {
|
|
457
|
+
strategy: resolvePoolStrategy(env.DARIO_POOL_STRATEGY ?? file?.strategy, env),
|
|
458
|
+
headroomFloor: resolvePoolHeadroomFloor(env.DARIO_POOL_HEADROOM_FLOOR ?? file?.headroomFloor, env),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
431
461
|
/**
|
|
432
462
|
* Match `anthropic-ratelimit-unified-7d_<family>-utilization`. Generic on
|
|
433
463
|
* `<family>` so a future `7d_opus` / `7d_haiku` (or anything Anthropic
|
|
@@ -1264,6 +1294,8 @@ export class AccountPool {
|
|
|
1264
1294
|
const avgHeadroom = headrooms.length > 0 ? headrooms.reduce((a, b) => a + b, 0) / headrooms.length : 0;
|
|
1265
1295
|
const best = this.select();
|
|
1266
1296
|
return {
|
|
1297
|
+
strategy: this.strategy,
|
|
1298
|
+
headroomFloor: this.headroomFloor,
|
|
1267
1299
|
accounts: all.length,
|
|
1268
1300
|
healthy: healthy.length,
|
|
1269
1301
|
exhausted: all.length - healthy.length,
|
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, DEFAULT_POOL_HEADROOM_FLOOR, utilFreshness, rateLimitWindow, accountAction, accountPeers, distinctAccounts, describeRejection, maskEmail, isAccountEligible } from './pool.js';
|
|
16
|
+
import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, activeParkedBuckets, reconcilePoolAccounts, resolvePoolStrategy, resolvePoolHeadroomFloor, describePoolStrategy, DEFAULT_POOL_HEADROOM_FLOOR, utilFreshness, rateLimitWindow, accountAction, accountPeers, distinctAccounts, describeRejection, maskEmail, isAccountEligible } 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, costOfTokens, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, CODEX_CLAIM } from './analytics.js';
|
|
@@ -1521,10 +1521,9 @@ export async function startProxy(opts = {}) {
|
|
|
1521
1521
|
if (opts.poolSharedState && !poolSync) {
|
|
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
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
}
|
|
1524
|
+
// Always named, the default included: a deployment that overrides the
|
|
1525
|
+
// default should be visible at a glance, and one that does not should say so.
|
|
1526
|
+
console.log(` Pool strategy: ${describePoolStrategy(poolStrategy, pool.headroomFloor)}`);
|
|
1528
1527
|
if (poolSync) {
|
|
1529
1528
|
console.log(` Pool shared state: on (instance ${poolSync.instance}, via ${lockUrl}, pulling peers every ${poolSync.intervalMs}ms; fails open)`);
|
|
1530
1529
|
poolSync.start();
|
|
@@ -2680,7 +2679,14 @@ export async function startProxy(opts = {}) {
|
|
|
2680
2679
|
// Version for auto-update checks (#640) — /status is key-gated (loopback
|
|
2681
2680
|
// or DARIO_API_KEY), so no public-disclosure concern like /health has.
|
|
2682
2681
|
res.writeHead(200, JSON_HEADERS);
|
|
2683
|
-
res.end(JSON.stringify({
|
|
2682
|
+
res.end(JSON.stringify({
|
|
2683
|
+
version: darioVersion(),
|
|
2684
|
+
...s,
|
|
2685
|
+
// The routing the running process uses — the only surface that can
|
|
2686
|
+
// report a --pool-strategy flag (doctor and `accounts list` read env
|
|
2687
|
+
// and config). null with no pool.
|
|
2688
|
+
pool: pool.size > 0 ? { strategy: pool.strategy, headroomFloor: pool.headroomFloor, accounts: pool.size } : null,
|
|
2689
|
+
}));
|
|
2684
2690
|
return;
|
|
2685
2691
|
}
|
|
2686
2692
|
// Pool status endpoint — shows loaded accounts, headroom, and the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.11.
|
|
3
|
+
"version": "6.11.1",
|
|
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": {
|