@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 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
  }
@@ -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 pool = new AccountPool();
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} (max-headroom select; ${ps.healthy}/${ps.accounts} healthy)`
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
- private readonly strategy;
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
- if (poolStrategy !== 'headroom') {
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)`);
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({ version: darioVersion(), ...s }));
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.0",
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": {