@askalf/dario 6.15.0 → 6.17.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.
Files changed (44) hide show
  1. package/README.md +19 -21
  2. package/dist/analytics-ui.html +835 -0
  3. package/dist/analytics.d.ts +26 -1
  4. package/dist/analytics.js +51 -3
  5. package/dist/body-identity.d.ts +25 -0
  6. package/dist/body-identity.js +68 -0
  7. package/dist/cc-template.js +3 -14
  8. package/dist/cli.d.ts +7 -0
  9. package/dist/cli.js +44 -7
  10. package/dist/codex-backend.js +12 -4
  11. package/dist/config-file.d.ts +1 -1
  12. package/dist/config-file.js +2 -2
  13. package/dist/credits-rung.d.ts +72 -0
  14. package/dist/credits-rung.js +133 -0
  15. package/dist/doctor-core.d.ts +2 -0
  16. package/dist/doctor-core.js +47 -10
  17. package/dist/donuts.d.ts +18 -17
  18. package/dist/donuts.js +46 -54
  19. package/dist/ledger.d.ts +20 -0
  20. package/dist/ledger.js +40 -1
  21. package/dist/metrics.js +2 -1
  22. package/dist/midstream.js +6 -2
  23. package/dist/openai-backend.js +8 -3
  24. package/dist/pool.d.ts +18 -1
  25. package/dist/pool.js +31 -10
  26. package/dist/proxy.d.ts +14 -1
  27. package/dist/proxy.js +351 -105
  28. package/dist/relaunch-signals.d.ts +35 -0
  29. package/dist/relaunch-signals.js +41 -0
  30. package/dist/stream-drain.d.ts +4 -3
  31. package/dist/stream-drain.js +4 -3
  32. package/dist/tui/tabs/config.js +2 -2
  33. package/dist/upstream-deadline.d.ts +25 -0
  34. package/dist/upstream-deadline.js +76 -0
  35. package/docs/analytics.md +10 -8
  36. package/docs/api-equivalent-spend.md +9 -2
  37. package/docs/commands.md +1 -1
  38. package/docs/configuration.md +1 -0
  39. package/docs/faq.md +5 -0
  40. package/docs/multi-account-pool.md +3 -1
  41. package/docs/pool-keys-and-analytics.md +2 -0
  42. package/docs/two-plans.md +2 -0
  43. package/docs/who-its-for.md +2 -2
  44. package/package.json +8 -4
package/dist/pool.d.ts CHANGED
@@ -396,8 +396,24 @@ export interface PoolStatus {
396
396
  * order is the operator's knob — name seats `1-main` / `2-overflow` to
397
397
  * pick the fill order. Sticky bindings behave identically in both modes;
398
398
  * strategy only decides where UNBOUND (new) conversations land.
399
+ *
400
+ * `headroom-by-plan` (opt-in): headroom, with each seat's headroom counted
401
+ * by its plan size (see planWeight). Never the default: utilization is a
402
+ * ratio of an account-wide budget that also carries the owner's own use, no
403
+ * absolute capacity reaches the wire, and the window that binds (5h or 7d)
404
+ * matters more than the plan name. A smaller plan regularly has more real
405
+ * room than a bigger one. It is for operators who know their seats are
406
+ * otherwise idle and want the bigger plan to carry proportionally more.
407
+ */
408
+ export type PoolStrategy = 'headroom' | 'headroom-by-plan' | 'fill-first' | 'expiring-first';
409
+ /**
410
+ * How much a seat's headroom counts under `headroom-by-plan`: N for a Max Nx
411
+ * plan (from the profile's `rate_limit_tier`, e.g. `default_claude_max_20x`),
412
+ * 1 for Pro, and 1 for anything unrecognised or unknown, so a seat whose plan
413
+ * dario cannot read is never favoured. Only the whole `default_claude_max_Nx`
414
+ * tier name counts; a tier that merely contains `max_Nx` is unrecognised.
399
415
  */
400
- export type PoolStrategy = 'headroom' | 'fill-first' | 'expiring-first';
416
+ export declare function planWeight(rateLimitTier: string | undefined): number;
401
417
  /**
402
418
  * `expiring-first` — fill-first, ordered by WHEN each seat's capacity expires
403
419
  * instead of by alias: new conversations fill the seat whose 7-day window
@@ -602,6 +618,7 @@ export declare function headroomFloorProblem(value: string | number | null | und
602
618
  */
603
619
  export declare function headroomFloorFlagProblem(value: string): string;
604
620
  export declare function resolvePoolHeadroomFloor(explicit?: string | number | null, env?: NodeJS.ProcessEnv): number;
621
+ export declare function pickMaxHeadroom(accounts: PoolAccount[], family?: string | null, byPlan?: boolean): PoolAccount;
605
622
  /**
606
623
  * The 7-day reset `expiring-first` orders by, in epoch ms: the seat's own
607
624
  * reading while it is still ahead of `now`, else +Infinity (never read, or the
package/dist/pool.js CHANGED
@@ -408,6 +408,18 @@ export function accountAction(account, now = Date.now()) {
408
408
  return 'wait';
409
409
  return 'none';
410
410
  }
411
+ /**
412
+ * How much a seat's headroom counts under `headroom-by-plan`: N for a Max Nx
413
+ * plan (from the profile's `rate_limit_tier`, e.g. `default_claude_max_20x`),
414
+ * 1 for Pro, and 1 for anything unrecognised or unknown, so a seat whose plan
415
+ * dario cannot read is never favoured. Only the whole `default_claude_max_Nx`
416
+ * tier name counts; a tier that merely contains `max_Nx` is unrecognised.
417
+ */
418
+ export function planWeight(rateLimitTier) {
419
+ const m = /^default_claude_max_(\d+)x$/i.exec(rateLimitTier ?? '');
420
+ const n = m ? Number(m[1]) : 1;
421
+ return Number.isFinite(n) && n >= 1 ? n : 1;
422
+ }
411
423
  /**
412
424
  * `expiring-first` — fill-first, ordered by WHEN each seat's capacity expires
413
425
  * instead of by alias: new conversations fill the seat whose 7-day window
@@ -431,7 +443,7 @@ export function resolvePoolStrategy(explicit, env = process.env) {
431
443
  if (typeof c !== 'string')
432
444
  continue;
433
445
  const s = c.trim().toLowerCase();
434
- if (s === 'headroom' || s === 'fill-first' || s === 'expiring-first')
446
+ if (s === 'headroom' || s === 'headroom-by-plan' || s === 'fill-first' || s === 'expiring-first')
435
447
  return s;
436
448
  }
437
449
  return 'headroom';
@@ -446,6 +458,8 @@ export function resolvePoolStrategy(explicit, env = process.env) {
446
458
  export function describePoolStrategy(strategy, floor = DEFAULT_POOL_HEADROOM_FLOOR) {
447
459
  const pct = `${Math.round(floor * 100)}% floor`;
448
460
  switch (strategy) {
461
+ case 'headroom-by-plan':
462
+ return `headroom-by-plan: new conversations go to the seat with the most headroom counted by plan size (Max Nx counts N, Pro 1); ${pct}`;
449
463
  case 'fill-first':
450
464
  return `fill-first — new conversations fill the alphabetically-first seat until the ${pct}, then spill`;
451
465
  case 'expiring-first':
@@ -866,14 +880,21 @@ export function resolvePoolHeadroomFloor(explicit, env = process.env) {
866
880
  // Pick the account with the most headroom in a single pass. The prior
867
881
  // `.reduce()` form recomputed the incumbent's headroom every iteration
868
882
  // (~2n computeHeadroom calls); this computes each once (#642-audit).
869
- function pickMaxHeadroom(accounts, family) {
883
+ // `byPlan` scales positive headroom by planWeight; zero and negative headroom
884
+ // stay unscaled, so a bigger plan that is over its limit never ranks lower
885
+ // than a smaller one that is over by the same amount.
886
+ export function pickMaxHeadroom(accounts, family, byPlan = false) {
887
+ const score = (a) => {
888
+ const h = computeHeadroom(a.rateLimit, family);
889
+ return byPlan && h > 0 ? h * planWeight(a.rateLimitTier) : h;
890
+ };
870
891
  let best = accounts[0];
871
- let bestHeadroom = computeHeadroom(best.rateLimit, family);
892
+ let bestScore = score(best);
872
893
  for (let i = 1; i < accounts.length; i++) {
873
- const h = computeHeadroom(accounts[i].rateLimit, family);
874
- if (h > bestHeadroom) {
894
+ const s = score(accounts[i]);
895
+ if (s > bestScore) {
875
896
  best = accounts[i];
876
- bestHeadroom = h;
897
+ bestScore = s;
877
898
  }
878
899
  }
879
900
  return best;
@@ -1107,10 +1128,10 @@ export class AccountPool {
1107
1128
  return first;
1108
1129
  }
1109
1130
  if (!this.hardHeadroomFloor)
1110
- return pickMaxHeadroom(eligible, family);
1131
+ return pickMaxHeadroom(eligible, family, this.strategy === 'headroom-by-plan');
1111
1132
  const aboveFloor = eligible.filter(a => isAboveHeadroomFloor(computeHeadroom(a.rateLimit, family, now), this.headroomFloor));
1112
1133
  return aboveFloor.length > 0
1113
- ? pickMaxHeadroom(aboveFloor, family)
1134
+ ? pickMaxHeadroom(aboveFloor, family, this.strategy === 'headroom-by-plan')
1114
1135
  : pickFloorProbe(eligible, family, this.headroomFloor, now);
1115
1136
  }
1116
1137
  // No seat is eligible. A seat parked inside a live window is not
@@ -1287,10 +1308,10 @@ export class AccountPool {
1287
1308
  return first;
1288
1309
  }
1289
1310
  if (!this.hardHeadroomFloor)
1290
- return pickMaxHeadroom(eligible, family);
1311
+ return pickMaxHeadroom(eligible, family, this.strategy === 'headroom-by-plan');
1291
1312
  const aboveFloor = eligible.filter(a => isAboveHeadroomFloor(computeHeadroom(a.rateLimit, family, now), this.headroomFloor));
1292
1313
  return aboveFloor.length > 0
1293
- ? pickMaxHeadroom(aboveFloor, family)
1314
+ ? pickMaxHeadroom(aboveFloor, family, this.strategy === 'headroom-by-plan')
1294
1315
  : pickFloorProbe(eligible, family, this.headroomFloor, now);
1295
1316
  }
1296
1317
  // Mid-flight: the seats a 429 could still hand this request to. A seat
package/dist/proxy.d.ts CHANGED
@@ -503,7 +503,10 @@ interface ProxyOptions {
503
503
  maxConcurrentPerConsumer?: number;
504
504
  /**
505
505
  * Max ms before the upstream fetch is aborted. Default 300000 (5 min,
506
- * matching the Anthropic SDK). Injectable so tests can exercise the
506
+ * matching the Anthropic SDK). It caps the wait until the response
507
+ * streams; once a 2xx body is being read it is the longest allowed gap
508
+ * between two chunks, so a live stream is never cut. `--upstream-timeout`
509
+ * / `DARIO_UPSTREAM_TIMEOUT_MS`. Injectable so tests can exercise the
507
510
  * timeout → slot-release path without waiting 5 minutes (dario#905).
508
511
  */
509
512
  upstreamTimeoutMs?: number;
@@ -668,6 +671,16 @@ interface ProxyOptions {
668
671
  * a CLI flag, so the key never lands in `ps`/argv).
669
672
  */
670
673
  upstreamApiKey?: string;
674
+ /**
675
+ * The key behind a `credits:<model>` entry of `--pool-fallback`
676
+ * (credits-rung.ts): a Console API key of the organization a Max plan is
677
+ * linked to, spending the monthly API credits that plan includes. Used
678
+ * only when every seat is drained or cooling and no subscription rung can
679
+ * serve; the pool stays the first choice. Sourced from
680
+ * DARIO_CREDITS_API_KEY (env-only, like `upstreamApiKey`, so it never lands
681
+ * in argv). A chain entry without the key is inert and said so at startup.
682
+ */
683
+ creditsApiKey?: string;
671
684
  /**
672
685
  * Overage-guard — halt the proxy on the first response that bills to
673
686
  * anything other than the subscription pool (`overage`, `api`, or a new