@askalf/dario 6.0.33 → 6.0.34

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/pool.js CHANGED
@@ -94,6 +94,61 @@ function formatDurationMs(ms) {
94
94
  const m = totalMins % 60;
95
95
  return h > 0 ? `${h}h ${m}m` : `${m}m`;
96
96
  }
97
+ /**
98
+ * The identity of the rate-limit window a reading was measured against:
99
+ * its representative claim plus its reset second, or null when the reading
100
+ * states no live window (no reset, a reset that has passed, or no claim).
101
+ *
102
+ * Two seats that report the same key are one subscription under two aliases
103
+ * (dario#1244, "a few have the same issue"): two independent windows all but
104
+ * never share a reset second, and two readings of one window always do. The
105
+ * organization id is deliberately NOT part of the key — several seats can
106
+ * share an organization and still have their own windows — the window itself
107
+ * is the fact that matters for headroom.
108
+ */
109
+ export function windowKey(rl, now) {
110
+ if (!(rl.reset > 0) || rl.reset * 1000 <= now)
111
+ return null;
112
+ if (!rl.claim || rl.claim === 'unknown')
113
+ return null;
114
+ return `${rl.claim}@${rl.reset}`;
115
+ }
116
+ /** For every seat, the other aliases whose last reading names the same live window. */
117
+ export function windowPeers(accounts, now) {
118
+ const byKey = new Map();
119
+ for (const a of accounts) {
120
+ const k = windowKey(a.rateLimit, now);
121
+ if (!k)
122
+ continue;
123
+ const list = byKey.get(k);
124
+ if (list)
125
+ list.push(a.alias);
126
+ else
127
+ byKey.set(k, [a.alias]);
128
+ }
129
+ const out = new Map();
130
+ for (const a of accounts) {
131
+ const k = windowKey(a.rateLimit, now);
132
+ out.set(a.alias, k ? (byKey.get(k) ?? []).filter((alias) => alias !== a.alias) : []);
133
+ }
134
+ return out;
135
+ }
136
+ /**
137
+ * How many windows the pool really has: each measured live window once, and
138
+ * each seat without a live reading as its own (nothing says otherwise yet).
139
+ */
140
+ export function distinctWindows(accounts, now) {
141
+ const keys = new Set();
142
+ let unmeasured = 0;
143
+ for (const a of accounts) {
144
+ const k = windowKey(a.rateLimit, now);
145
+ if (k)
146
+ keys.add(k);
147
+ else
148
+ unmeasured++;
149
+ }
150
+ return keys.size + unmeasured;
151
+ }
97
152
  /**
98
153
  * Cool-down schedule after auth failures. First failure: 60s. Each
99
154
  * consecutive failure doubles the window up to 30 minutes. Cleared
@@ -373,6 +428,8 @@ export class AccountPool {
373
428
  refreshToken: opts.refreshToken,
374
429
  expiresAt: opts.expiresAt,
375
430
  grantedAt: opts.grantedAt ?? keep?.grantedAt,
431
+ organizationId: opts.organizationId ?? keep?.organizationId,
432
+ adoptedFrom: keep?.adoptedFrom,
376
433
  identity: keep?.identity ?? {
377
434
  deviceId: opts.deviceId,
378
435
  accountUuid: opts.accountUuid,
@@ -598,6 +655,7 @@ export class AccountPool {
598
655
  if (!account)
599
656
  return;
600
657
  account.rateLimit = snapshot;
658
+ account.adoptedFrom = undefined;
601
659
  account.requestCount++;
602
660
  }
603
661
  /**
@@ -614,10 +672,37 @@ export class AccountPool {
614
672
  const now = snapshot.updatedAt || Date.now();
615
673
  const wasParked = account.rateLimit.status === 'rejected' && !rateLimitWindowPassed(account.rateLimit, now);
616
674
  account.rateLimit = { ...snapshot, status: 'rejected' };
675
+ account.adoptedFrom = undefined;
617
676
  account.rejectedCount++;
618
677
  account.lastRejectedAt = now;
619
678
  return !wasParked;
620
679
  }
680
+ /**
681
+ * Record the organization a response said this seat belongs to. Returns
682
+ * true when it is news — the first observation, or a change (an alias
683
+ * re-granted on another organization) — so the caller persists it once.
684
+ */
685
+ noteOrganization(alias, organizationId) {
686
+ const account = this.accounts.get(alias);
687
+ if (!account || !organizationId || account.organizationId === organizationId)
688
+ return false;
689
+ account.organizationId = organizationId;
690
+ return true;
691
+ }
692
+ /**
693
+ * Take a peer instance's reading of `alias` (pool-sync.ts): its snapshot
694
+ * replaces ours, `rejected` parks the seat on it. Counters are left alone
695
+ * — a request the peer served or a 429 it took are the peer's facts — and
696
+ * `adoptedFrom` records whose reading this is. False for an unknown alias.
697
+ */
698
+ adoptSnapshot(alias, snapshot, rejected, from) {
699
+ const account = this.accounts.get(alias);
700
+ if (!account)
701
+ return false;
702
+ account.rateLimit = rejected ? { ...snapshot, status: 'rejected' } : { ...snapshot };
703
+ account.adoptedFrom = from;
704
+ return true;
705
+ }
621
706
  updateTokens(alias, accessToken, refreshToken, expiresAt) {
622
707
  const account = this.accounts.get(alias);
623
708
  if (!account)
@@ -737,6 +822,7 @@ export function reconcilePoolAccounts(pool, accounts) {
737
822
  deviceId: a.deviceId,
738
823
  accountUuid: a.accountUuid,
739
824
  grantedAt: a.grantedAt,
825
+ organizationId: a.organizationId,
740
826
  });
741
827
  }
742
828
  for (const existing of pool.all()) {
package/dist/proxy.d.ts CHANGED
@@ -345,12 +345,25 @@ interface ProxyOptions {
345
345
  * `--pool-strategy` / `DARIO_POOL_STRATEGY` / config `pool.strategy`.
346
346
  */
347
347
  poolStrategy?: string;
348
+ /**
349
+ * Share rate-limit readings and sticky bindings with other instances
350
+ * through the refresh-lock service (pool-sync.ts). Needs
351
+ * `DARIO_REFRESH_LOCK_URL` / `DARIO_REFRESH_LOCK_TOKEN`. Off by default.
352
+ */
353
+ poolSharedState?: boolean;
354
+ /** How often to pull peers' readings, ms. Default 2000. */
355
+ poolSharedStateIntervalMs?: number;
348
356
  /** Max concurrent in-flight requests. Default 10. dario#80. */
349
357
  maxConcurrent?: number;
350
358
  /** Max requests buffered waiting for a concurrency slot. Default 128. dario#80. */
351
359
  maxQueued?: number;
352
360
  /** Max ms a queued request waits before it times out with 504. Default 60000. dario#80. */
353
361
  queueTimeoutMs?: number;
362
+ /**
363
+ * Max in-flight requests per consumer (`x-dario-consumer` header). A
364
+ * consumer at its cap waits while others keep flowing. 0 / unset = off.
365
+ */
366
+ maxConcurrentPerConsumer?: number;
354
367
  /**
355
368
  * Max ms before the upstream fetch is aborted. Default 300000 (5 min,
356
369
  * matching the Anthropic SDK). Injectable so tests can exercise the
@@ -545,6 +558,7 @@ export interface ProxyLogEntry {
545
558
  claim?: string;
546
559
  bucket?: string;
547
560
  account?: string;
561
+ consumer?: string;
548
562
  client?: string;
549
563
  preserve_tools?: boolean;
550
564
  stream?: boolean;
package/dist/proxy.js CHANGED
@@ -12,13 +12,14 @@ import { darioVersion } from './version.js';
12
12
  import { buildCCRequest, applyCcPromptCaching, isGenuineCCClient, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, overlayTemplateHeaderValues, forwardClientCCIdentityHeaders, isMcpToolName, CC_TEMPLATE, effectiveCacheControl, withForced1hBeta } from './cc-template.js';
13
13
  import { stampCch, hasCchSeed } from './cch.js';
14
14
  import { describeTemplate, detectDrift, checkCCCompat, probeInstalledCCVersion } from './live-fingerprint.js';
15
- import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, reconcilePoolAccounts, resolvePoolStrategy, utilFreshness, rateLimitWindow, describeRateLimitSnapshot } from './pool.js';
16
- import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS, CODEX_CLAIM } from './analytics.js';
15
+ import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, reconcilePoolAccounts, resolvePoolStrategy, utilFreshness, rateLimitWindow, describeRateLimitSnapshot, windowPeers, distinctWindows } from './pool.js';
16
+ import { PoolSync, DEFAULT_POOL_SYNC_INTERVAL_MS } from './pool-sync.js';
17
+ import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, CODEX_CLAIM } from './analytics.js';
17
18
  import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
18
19
  import { notify as osNotify } from './notify.js';
19
20
  import { grantAge, grantThresholds, worstGrantLevel, describeGrantAge } from './refresh-grant.js';
20
21
  import { resolveSeatPin, SEAT_PIN_HEADER, SEAT_PIN_TOKEN_HEADER } from './seat-pin.js';
21
- import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncLoginFromCredentialsIfStale, ensureLoginCredentialsInPool, mirrorLoginToCredentials } from './accounts.js';
22
+ import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncLoginFromCredentialsIfStale, ensureLoginCredentialsInPool, mirrorLoginToCredentials, withObservedOrganization } from './accounts.js';
22
23
  import { handleAdminRequest } from './admin-api.js';
23
24
  import { createTokenBucket } from './rate-limit.js';
24
25
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
@@ -1328,9 +1329,45 @@ export async function startProxy(opts = {}) {
1328
1329
  const accountsList = await loadAllAccounts();
1329
1330
  const poolStrategy = resolvePoolStrategy(opts.poolStrategy);
1330
1331
  const pool = new AccountPool(poolStrategy);
1332
+ // Two aliases reporting one window are one subscription counted twice
1333
+ // (dario#1244). Said once per pair, when the second reading arrives; the
1334
+ // listings carry it permanently as `sharesWindowWith`.
1335
+ const announcedWindowPairs = new Set();
1336
+ const announceWindowPeers = (alias) => {
1337
+ const now = Date.now();
1338
+ const seat = pool.get(alias);
1339
+ if (!seat)
1340
+ return;
1341
+ for (const peer of windowPeers(pool.all(), now).get(alias) ?? []) {
1342
+ const pair = [alias, peer].sort().join('|');
1343
+ if (announcedWindowPairs.has(pair))
1344
+ continue;
1345
+ announcedWindowPairs.add(pair);
1346
+ const windows = distinctWindows(pool.all(), now);
1347
+ console.error(`[dario] seats "${alias}" and "${peer}" report the same ${seat.rateLimit.claim} window (resets ${new Date(seat.rateLimit.reset * 1000).toISOString()}) — one subscription under two aliases; the pool has ${windows} distinct window${windows === 1 ? '' : 's'} across ${pool.size} seats`);
1348
+ }
1349
+ };
1350
+ // Shared pool state across instances (docs/multi-instance.md): opt-in,
1351
+ // rides the refresh-lock service, fails open. Off → byte-identical to before.
1352
+ const lockUrl = process.env['DARIO_REFRESH_LOCK_URL'];
1353
+ const poolSync = opts.poolSharedState && lockUrl
1354
+ ? new PoolSync(pool, {
1355
+ baseUrl: lockUrl,
1356
+ token: process.env['DARIO_REFRESH_LOCK_TOKEN'] ?? '',
1357
+ intervalMs: opts.poolSharedStateIntervalMs ?? DEFAULT_POOL_SYNC_INTERVAL_MS,
1358
+ log: (line) => console.error(line),
1359
+ })
1360
+ : null;
1361
+ if (opts.poolSharedState && !poolSync) {
1362
+ 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');
1363
+ }
1331
1364
  if (poolStrategy !== 'headroom') {
1332
1365
  console.log(` Pool strategy: ${poolStrategy} (new conversations fill the alphabetically-first seat, spill at the 2% floor)`);
1333
1366
  }
1367
+ if (poolSync) {
1368
+ console.log(` Pool shared state: on (instance ${poolSync.instance}, via ${lockUrl}, pulling peers every ${poolSync.intervalMs}ms; fails open)`);
1369
+ poolSync.start();
1370
+ }
1334
1371
  // Per-model rate-limit bucket families seen during this proxy run. First-
1335
1372
  // sight is logged once when verbose so a new Anthropic bucket (e.g. an
1336
1373
  // eventual `7d_opus`) doesn't slip past unnoticed. Pure observability —
@@ -1398,6 +1435,7 @@ export async function startProxy(opts = {}) {
1398
1435
  deviceId: acc.deviceId,
1399
1436
  accountUuid: acc.accountUuid,
1400
1437
  grantedAt: acc.grantedAt,
1438
+ organizationId: acc.organizationId,
1401
1439
  });
1402
1440
  }
1403
1441
  // Startup self-heal (dario#790): eagerly refresh any account whose access
@@ -1418,7 +1456,9 @@ export async function startProxy(opts = {}) {
1418
1456
  const saved = await loadAccount(acc.alias);
1419
1457
  if (!saved)
1420
1458
  return;
1421
- const refreshed = await refreshAccountToken(saved);
1459
+ // The refresh's write carries the organization the seat was
1460
+ // observed on (dario#1244) — the one write that touches the record.
1461
+ const refreshed = await refreshAccountToken(withObservedOrganization(saved, acc.organizationId));
1422
1462
  pool.updateTokens(acc.alias, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt);
1423
1463
  // Mirror a refreshed `login` token back to credentials.json so the
1424
1464
  // legacy file (and `dario doctor`) tracks the pool store (#808).
@@ -1470,7 +1510,9 @@ export async function startProxy(opts = {}) {
1470
1510
  const saved = await loadAccount(acc.alias);
1471
1511
  if (!saved)
1472
1512
  continue;
1473
- const refreshed = await refreshAccountToken(saved);
1513
+ // The refresh's write carries the organization the seat was
1514
+ // observed on (dario#1244) — the one write that touches the record.
1515
+ const refreshed = await refreshAccountToken(withObservedOrganization(saved, acc.organizationId));
1474
1516
  pool.updateTokens(acc.alias, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt);
1475
1517
  // Mirror a refreshed `login` token back to credentials.json so the
1476
1518
  // legacy file (and `dario doctor`) tracks the pool store (#808).
@@ -1515,6 +1557,7 @@ export async function startProxy(opts = {}) {
1515
1557
  deviceId: acc.deviceId,
1516
1558
  accountUuid: acc.accountUuid,
1517
1559
  grantedAt: acc.grantedAt,
1560
+ organizationId: acc.organizationId,
1518
1561
  });
1519
1562
  }
1520
1563
  }
@@ -1616,6 +1659,7 @@ export async function startProxy(opts = {}) {
1616
1659
  maxConcurrent: opts.maxConcurrent ?? DEFAULT_MAX_CONCURRENT,
1617
1660
  maxQueued: opts.maxQueued ?? DEFAULT_MAX_QUEUED,
1618
1661
  queueTimeoutMs: opts.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS,
1662
+ maxConcurrentPerConsumer: opts.maxConcurrentPerConsumer ?? 0,
1619
1663
  });
1620
1664
  const upstreamTimeoutMs = opts.upstreamTimeoutMs ?? UPSTREAM_TIMEOUT_MS;
1621
1665
  // Cache context-1m beta availability. Set false once per account after the
@@ -2093,6 +2137,7 @@ export async function startProxy(opts = {}) {
2093
2137
  // just persisted metadata — the same snapshot GET /accounts exposes.
2094
2138
  poolStatus: () => {
2095
2139
  const snapNow = Date.now();
2140
+ const peers = windowPeers(pool.all(), snapNow);
2096
2141
  const snap = new Map();
2097
2142
  for (const a of pool.all()) {
2098
2143
  snap.set(a.alias, {
@@ -2108,6 +2153,9 @@ export async function startProxy(opts = {}) {
2108
2153
  requestCount: a.requestCount,
2109
2154
  rejectedCount: a.rejectedCount,
2110
2155
  lastRejectedAt: a.lastRejectedAt ?? null,
2156
+ organizationId: a.organizationId ?? null,
2157
+ sharesWindowWith: peers.get(a.alias) ?? [],
2158
+ readingFrom: a.adoptedFrom ?? null,
2111
2159
  // Raw streak, not just the cooldown boolean: a single 401 also
2112
2160
  // shows `auth-cooldown` for 60s, indistinguishable from a
2113
2161
  // genuinely dead refresh token by that field alone. The magnitude
@@ -2179,6 +2227,7 @@ export async function startProxy(opts = {}) {
2179
2227
  // the `dario accounts` CLI, not HTTP.
2180
2228
  if (urlPath === '/accounts' && req.method === 'GET') {
2181
2229
  const now = Date.now();
2230
+ const peers = windowPeers(pool.all(), now);
2182
2231
  const accounts = pool.all().map(a => {
2183
2232
  const inCooldown = isInAuthCooldown(a, now);
2184
2233
  const cooldownMs = inCooldown && a.lastAuthFailureAt
@@ -2214,6 +2263,14 @@ export async function startProxy(opts = {}) {
2214
2263
  // parked seat no longer reads as one that was never called.
2215
2264
  rejectedCount: a.rejectedCount,
2216
2265
  lastRejectedAt: a.lastRejectedAt ?? null,
2266
+ // Which organization the token belongs to, and which other seats
2267
+ // report the same live window — one subscription under several
2268
+ // aliases (dario#1244).
2269
+ organizationId: a.organizationId ?? null,
2270
+ sharesWindowWith: peers.get(a.alias) ?? [],
2271
+ // Whose reading this is: a peer instance's id (shared pool state)
2272
+ // or null for this instance's own.
2273
+ readingFrom: a.adoptedFrom ?? null,
2217
2274
  expiresInMs: Math.max(0, a.expiresAt - now),
2218
2275
  // Refresh-token grant age (refresh-grant.ts): the wall a token
2219
2276
  // refresh cannot move. null fields = grant date unknown.
@@ -2236,6 +2293,11 @@ export async function startProxy(opts = {}) {
2236
2293
  mode: 'pool',
2237
2294
  ...pool.status(),
2238
2295
  stickyBindings: pool.stickyCount(),
2296
+ // Windows the pool really has: each measured window once, each
2297
+ // unmeasured seat as its own.
2298
+ distinctWindows: distinctWindows(pool.all(), now),
2299
+ // Shared pool state (pool-sync.ts) — null when off.
2300
+ sharedState: poolSync ? poolSync.status() : null,
2239
2301
  accounts,
2240
2302
  }));
2241
2303
  return;
@@ -2462,12 +2524,19 @@ export async function startProxy(opts = {}) {
2462
2524
  res.end(JSON.stringify(buildHaltErrorBody(state)));
2463
2525
  return;
2464
2526
  }
2527
+ // Who this request is for (dario#1244 follow-up). An `x-dario-consumer`
2528
+ // header names the consumer for both the per-consumer concurrency cap and
2529
+ // attribution. Without one, attribution falls back to a hash of the
2530
+ // body's user id once the body is parsed; the cap needs the name before
2531
+ // the slot is taken, so only the header gates.
2532
+ const consumerFromHeaders = consumerFromHeader(req.headers[CONSUMER_HEADER]);
2533
+ let consumer = consumerFromHeaders;
2465
2534
  // Proxy to Anthropic (with concurrency control). The bounded queue
2466
2535
  // replaces the v3.30.x-and-earlier unbounded semaphore — dario#80. A
2467
2536
  // queue-full condition returns an explicit 429 with a `"queue-full"`
2468
2537
  // marker in the body; a queue-timeout returns 504 with `"queue-timeout"`.
2469
2538
  try {
2470
- await queue.acquire();
2539
+ await queue.acquire(consumerFromHeaders);
2471
2540
  }
2472
2541
  catch (err) {
2473
2542
  if (err instanceof QueueFullError) {
@@ -2757,8 +2826,11 @@ export async function startProxy(opts = {}) {
2757
2826
  const v = JSON.parse(text);
2758
2827
  if (v === null || typeof v !== 'object' || Array.isArray(v))
2759
2828
  invalid = 'request body must be a JSON object';
2760
- else
2829
+ else {
2761
2830
  parsedBody = v;
2831
+ if (!consumer)
2832
+ consumer = consumerFromBody(parsedBody);
2833
+ }
2762
2834
  }
2763
2835
  catch (err) {
2764
2836
  invalid = `request body is not valid JSON: ${err instanceof Error ? err.message : String(err)}`;
@@ -3055,6 +3127,7 @@ export async function startProxy(opts = {}) {
3055
3127
  codexRequestCounts.set(o.alias, (codexRequestCounts.get(o.alias) ?? 0) + 1);
3056
3128
  analytics.record({
3057
3129
  timestamp: Date.now(),
3130
+ consumer,
3058
3131
  account: o.alias,
3059
3132
  model: o.model || rawModel || 'codex',
3060
3133
  inputTokens: o.inputTokens, outputTokens: o.outputTokens,
@@ -3072,13 +3145,13 @@ export async function startProxy(opts = {}) {
3072
3145
  method: req.method ?? '', path: urlPath, model: o.model || rawModel || undefined,
3073
3146
  status: o.status, latency_ms: o.latencyMs, in_tokens: o.inputTokens, out_tokens: o.outputTokens,
3074
3147
  cache_read: o.cacheReadTokens, cache_create: o.cacheCreateTokens,
3075
- claim: CODEX_CLAIM, bucket: 'subscription', account: o.alias, stream: o.stream,
3148
+ claim: CODEX_CLAIM, bucket: 'subscription', account: o.alias, consumer, stream: o.stream,
3076
3149
  });
3077
3150
  if (verbose)
3078
3151
  console.log(formatUsageLogLine(codexReq, {
3079
3152
  inputTokens: o.inputTokens, outputTokens: o.outputTokens,
3080
3153
  cacheReadTokens: o.cacheReadTokens, cacheCreateTokens: o.cacheCreateTokens,
3081
- }));
3154
+ }, consumer));
3082
3155
  },
3083
3156
  // Cool codex on a rate limit only — a 5xx or an unreachable backend
3084
3157
  // is an outage, and parking a provider for that would keep it out
@@ -3332,7 +3405,20 @@ export async function startProxy(opts = {}) {
3332
3405
  // Rotating off mid-session costs cache-create on every turn.
3333
3406
  stickyKey = computeStickyKey(userMsg);
3334
3407
  if (stickyKey && !pinnedAccount) {
3408
+ // Shared state (pool-sync.ts): a conversation a peer instance
3409
+ // already bound lands on the same seat here, so its prompt
3410
+ // cache is read rather than rewritten. Only consulted when this
3411
+ // instance holds no binding of its own; a binding made here is
3412
+ // published for the peers.
3413
+ const hadLocalBinding = pool.stickyAliasFor(stickyKey) !== null;
3414
+ if (poolSync && !hadLocalBinding) {
3415
+ const peerAlias = await poolSync.lookupSticky(stickyKey);
3416
+ if (peerAlias)
3417
+ pool.rebindSticky(stickyKey, peerAlias);
3418
+ }
3335
3419
  const preferred = pool.selectSticky(stickyKey, modelFamily(requestModel));
3420
+ if (poolSync && preferred && !hadLocalBinding)
3421
+ poolSync.bindSticky(stickyKey, preferred.alias);
3336
3422
  if (preferred && preferred.alias !== poolAccount?.alias) {
3337
3423
  poolAccount = preferred;
3338
3424
  accessToken = preferred.accessToken;
@@ -3746,6 +3832,14 @@ export async function startProxy(opts = {}) {
3746
3832
  else {
3747
3833
  pool.updateRateLimits(poolAccount.alias, snapshot);
3748
3834
  }
3835
+ // Which organization answered (dario#1244). Held in the pool at
3836
+ // once; written to the seat's record by its next token refresh —
3837
+ // the write that already exists — so nothing here races a refresh.
3838
+ const organizationId = upstream.headers.get('anthropic-organization-id');
3839
+ if (organizationId)
3840
+ pool.noteOrganization(poolAccount.alias, organizationId);
3841
+ announceWindowPeers(poolAccount.alias);
3842
+ poolSync?.reportSeat(poolAccount.alias);
3749
3843
  // First-sight detector for per-model rate-limit buckets. Anthropic
3750
3844
  // ships these unannounced — e.g. `7d_sonnet-utilization` appeared
3751
3845
  // around 2026-04-25 — and verbose-mode users want a heads-up the
@@ -4021,6 +4115,7 @@ export async function startProxy(opts = {}) {
4021
4115
  headers['Authorization'] = `Bearer ${accessToken}`;
4022
4116
  headers['x-claude-code-session-id'] = resolveOutboundSession(nextAccount, clientSessionKey).sessionId;
4023
4117
  pool.rebindSticky(stickyKey, nextAccount.alias);
4118
+ poolSync?.bindSticky(stickyKey, nextAccount.alias);
4024
4119
  peekedBody = null;
4025
4120
  continue dispatchLoop;
4026
4121
  }
@@ -4068,6 +4163,7 @@ export async function startProxy(opts = {}) {
4068
4163
  const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
4069
4164
  analytics.record({
4070
4165
  timestamp: Date.now(),
4166
+ consumer,
4071
4167
  account: poolAccount?.alias ?? ACCOUNT_KEY_APIKEY,
4072
4168
  model: requestModel,
4073
4169
  inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
@@ -4121,6 +4217,7 @@ export async function startProxy(opts = {}) {
4121
4217
  headers['Authorization'] = `Bearer ${accessToken}`;
4122
4218
  headers['x-claude-code-session-id'] = resolveOutboundSession(nextAccount, clientSessionKey).sessionId;
4123
4219
  pool.rebindSticky(stickyKey, nextAccount.alias);
4220
+ poolSync?.bindSticky(stickyKey, nextAccount.alias);
4124
4221
  continue dispatchLoop;
4125
4222
  }
4126
4223
  // No peer available — forward the saved generic-403 bytes when the
@@ -4154,6 +4251,7 @@ export async function startProxy(opts = {}) {
4154
4251
  headers['Authorization'] = `Bearer ${accessToken}`;
4155
4252
  headers['x-claude-code-session-id'] = resolveOutboundSession(nextAccount, clientSessionKey).sessionId;
4156
4253
  pool.rebindSticky(stickyKey, nextAccount.alias);
4254
+ poolSync?.bindSticky(stickyKey, nextAccount.alias);
4157
4255
  continue dispatchLoop;
4158
4256
  }
4159
4257
  }
@@ -4189,6 +4287,7 @@ export async function startProxy(opts = {}) {
4189
4287
  const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
4190
4288
  analytics.record({
4191
4289
  timestamp: Date.now(),
4290
+ consumer,
4192
4291
  account: poolAccount?.alias ?? ACCOUNT_KEY_APIKEY,
4193
4292
  model: requestModel,
4194
4293
  inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
@@ -4446,6 +4545,7 @@ export async function startProxy(opts = {}) {
4446
4545
  const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
4447
4546
  analytics.record({
4448
4547
  timestamp: Date.now(),
4548
+ consumer,
4449
4549
  account: poolAccount?.alias ?? ACCOUNT_KEY_APIKEY,
4450
4550
  model: requestModel,
4451
4551
  inputTokens: streamInputTokens, outputTokens: streamOutputTokens,
@@ -4465,6 +4565,7 @@ export async function startProxy(opts = {}) {
4465
4565
  claim: poolAccount?.rateLimit.claim,
4466
4566
  bucket: poolAccount ? billingBucketFromClaim(poolAccount.rateLimit.claim) : undefined,
4467
4567
  account: poolAccount?.alias,
4568
+ consumer,
4468
4569
  client: detectedClientForLog,
4469
4570
  preserve_tools: preserveToolsEffective,
4470
4571
  stream: true,
@@ -4473,7 +4574,7 @@ export async function startProxy(opts = {}) {
4473
4574
  console.log(formatUsageLogLine(requestCount, {
4474
4575
  inputTokens: streamInputTokens, outputTokens: streamOutputTokens,
4475
4576
  cacheReadTokens: streamCacheReadTokens, cacheCreateTokens: streamCacheCreateTokens,
4476
- }));
4577
+ }, consumer));
4477
4578
  }
4478
4579
  else {
4479
4580
  // Buffer and forward
@@ -4512,6 +4613,7 @@ export async function startProxy(opts = {}) {
4512
4613
  const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
4513
4614
  analytics.record({
4514
4615
  timestamp: Date.now(),
4616
+ consumer,
4515
4617
  account: poolAccount?.alias ?? ACCOUNT_KEY_APIKEY,
4516
4618
  model: bufferedUsage.model || requestModel,
4517
4619
  inputTokens: bufferedUsage.inputTokens, outputTokens: bufferedUsage.outputTokens,
@@ -4533,12 +4635,13 @@ export async function startProxy(opts = {}) {
4533
4635
  claim: poolAccount?.rateLimit.claim,
4534
4636
  bucket: poolAccount ? billingBucketFromClaim(poolAccount.rateLimit.claim) : undefined,
4535
4637
  account: poolAccount?.alias,
4638
+ consumer,
4536
4639
  client: detectedClientForLog,
4537
4640
  preserve_tools: preserveToolsEffective,
4538
4641
  stream: false,
4539
4642
  });
4540
4643
  if (verbose && bufferedUsage)
4541
- console.log(formatUsageLogLine(requestCount, bufferedUsage));
4644
+ console.log(formatUsageLogLine(requestCount, bufferedUsage, consumer));
4542
4645
  if (verbose)
4543
4646
  console.log(`[dario] #${requestCount} ${upstream.status}`);
4544
4647
  }
@@ -4550,6 +4653,7 @@ export async function startProxy(opts = {}) {
4550
4653
  ts: new Date().toISOString(), req: requestCount,
4551
4654
  method: req.method ?? '', path: urlPath,
4552
4655
  model: requestModel || undefined,
4656
+ consumer,
4553
4657
  client: detectedClientForLog,
4554
4658
  preserve_tools: preserveToolsEffective,
4555
4659
  };
@@ -4590,7 +4694,7 @@ export async function startProxy(opts = {}) {
4590
4694
  clearTimeout(upstreamTimeout);
4591
4695
  if (onClientClose !== null)
4592
4696
  req.off('close', onClientClose);
4593
- queue.release();
4697
+ queue.release(consumerFromHeaders);
4594
4698
  }
4595
4699
  });
4596
4700
  server.on('error', async (err) => {
@@ -4819,6 +4923,7 @@ export async function startProxy(opts = {}) {
4819
4923
  console.log('\n[dario] Shutting down...');
4820
4924
  clearInterval(presenceInterval);
4821
4925
  clearInterval(refreshInterval);
4926
+ poolSync?.stop();
4822
4927
  if (logFileStream)
4823
4928
  logFileStream.end();
4824
4929
  // Flush tokens first (best-effort, bounded), then close the server. The
@@ -52,6 +52,10 @@ export interface QueueState {
52
52
  */
53
53
  export interface QueueSnapshot extends QueueState {
54
54
  stalledSince: number | null;
55
+ /** Per-consumer in-flight ceiling (`--max-concurrent-per-consumer`); 0 = off. */
56
+ maxConcurrentPerConsumer: number;
57
+ /** Distinct consumers with a request in flight right now. */
58
+ consumersActive: number;
55
59
  }
56
60
  export type AdmitDecision = {
57
61
  action: 'admit';
@@ -63,6 +67,14 @@ export type AdmitDecision = {
63
67
  };
64
68
  /** Pure admission decision — no side effects, no clock dep. */
65
69
  export declare function decideAdmit(state: QueueState): AdmitDecision;
70
+ /**
71
+ * Pure per-consumer gate (dario#1244 follow-up — a team gateway where one
72
+ * heavy user could hold every slot). A consumer already holding `cap` slots
73
+ * waits even when the queue has room: `enqueue` if it does, `reject` if
74
+ * not. Returns null when the gate does not apply (cap off, or the consumer
75
+ * is under it), so `decideAdmit` decides as before.
76
+ */
77
+ export declare function decideConsumerAdmit(activeForConsumer: number, cap: number, state: QueueState): AdmitDecision | null;
66
78
  /** Pure timeout check — separated so tests can pass an explicit clock. */
67
79
  export declare function isQueueEntryExpired(enqueuedAt: number, now: number, timeoutMs: number): boolean;
68
80
  export declare class QueueFullError extends Error {
@@ -75,6 +87,13 @@ export interface RequestQueueOptions {
75
87
  maxConcurrent?: number;
76
88
  maxQueued?: number;
77
89
  queueTimeoutMs?: number;
90
+ /**
91
+ * In-flight ceiling per consumer (see `acquire(consumer)`). 0 / unset =
92
+ * off. A consumer at its cap waits in the queue; its waiters never block
93
+ * another consumer's — `release` admits the first waiter whose consumer
94
+ * is under the cap, not the first waiter.
95
+ */
96
+ maxConcurrentPerConsumer?: number;
78
97
  /**
79
98
  * Whether timeout timers are `unref`'d so they don't by themselves keep
80
99
  * the Node event loop alive. Default `true` — appropriate for the proxy,
@@ -95,8 +114,10 @@ export declare class RequestQueue {
95
114
  readonly maxConcurrent: number;
96
115
  readonly maxQueued: number;
97
116
  readonly queueTimeoutMs: number;
117
+ readonly maxConcurrentPerConsumer: number;
98
118
  readonly unrefTimers: boolean;
99
119
  private active;
120
+ private activeByConsumer;
100
121
  private queue;
101
122
  private readonly now;
102
123
  private stalledSince;
@@ -111,15 +132,24 @@ export declare class RequestQueue {
111
132
  * `release()` refreshes it, by clearing first (see there).
112
133
  */
113
134
  private updateStall;
135
+ /** A consumer is under its cap when there is no cap, no consumer, or room. */
136
+ private underCap;
137
+ private admit;
114
138
  /**
115
139
  * Acquire a concurrency slot. Resolves when admitted; throws
116
140
  * `QueueFullError` when the queue is at its `maxQueued` cap, throws
117
141
  * `QueueTimeoutError` when a queued request waited longer than
118
- * `queueTimeoutMs`.
142
+ * `queueTimeoutMs`. `consumer` names who the request is for: with a
143
+ * per-consumer cap set, a consumer at its cap waits even while slots are
144
+ * free, and `release(consumer)` must be called with the same name.
145
+ */
146
+ acquire(consumer?: string): Promise<void>;
147
+ /**
148
+ * Release a slot. The first queued entry whose consumer is under its cap is
149
+ * admitted — FIFO among the admissible, so a capped consumer's waiters do
150
+ * not hold up anyone else's; they get in when that consumer releases.
119
151
  */
120
- acquire(): Promise<void>;
121
- /** Release a slot. The next queued entry (if any) is admitted in FIFO order. */
122
- release(): void;
152
+ release(consumer?: string): void;
123
153
  /** Snapshot of queue state — exposed for /health + /analytics + tests. */
124
154
  snapshot(): QueueSnapshot;
125
155
  }