@askalf/dario 6.0.33 → 6.0.35

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/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, accountAction } 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';
@@ -32,7 +33,7 @@ import { selectPoolFallbackModels } from './pool-fallback-tier.js';
32
33
  import { RequestQueue, QueueFullError, QueueTimeoutError, DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_QUEUED, DEFAULT_QUEUE_TIMEOUT_MS } from './request-queue.js';
33
34
  import { redactSecrets } from './redact.js';
34
35
  import { BAKED_BASE_MODELS, withLongContextVariants, buildOpenAIModelsList, getModelCatalog, getCachedBases, resolveAliasAgainst, prewarmModelCatalog, retryModelCatalogNow, isSuspendedModel } from './model-catalog.js';
35
- import { classifyUpstreamRejection, diagnosticSnippet } from './upstream-rejection.js';
36
+ import { classifyUpstreamRejection, diagnosticSnippet, POOL_PARKED } from './upstream-rejection.js';
36
37
  import { ProviderCooldowns, canAttempt, allProvidersCooled, cooldownRetryAfterMs, parseRetryAfterMs, ALL_PROVIDERS_RATE_LIMITED, } from './provider-cooldown.js';
37
38
  const ANTHROPIC_API = 'https://api.anthropic.com';
38
39
  const DEFAULT_PORT = 3456;
@@ -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
  }
@@ -1612,10 +1655,14 @@ export async function startProxy(opts = {}) {
1612
1655
  overlayTemplateHeaderValues(staticHeaders, CC_TEMPLATE.header_values);
1613
1656
  }
1614
1657
  let requestCount = 0;
1658
+ // dario#1244: the "pool parked" line is logged on the transition into the
1659
+ // state, not on every request that arrives while it holds.
1660
+ let poolParkedAnnounced = false;
1615
1661
  const queue = new RequestQueue({
1616
1662
  maxConcurrent: opts.maxConcurrent ?? DEFAULT_MAX_CONCURRENT,
1617
1663
  maxQueued: opts.maxQueued ?? DEFAULT_MAX_QUEUED,
1618
1664
  queueTimeoutMs: opts.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS,
1665
+ maxConcurrentPerConsumer: opts.maxConcurrentPerConsumer ?? 0,
1619
1666
  });
1620
1667
  const upstreamTimeoutMs = opts.upstreamTimeoutMs ?? UPSTREAM_TIMEOUT_MS;
1621
1668
  // Cache context-1m beta availability. Set false once per account after the
@@ -2093,6 +2140,7 @@ export async function startProxy(opts = {}) {
2093
2140
  // just persisted metadata — the same snapshot GET /accounts exposes.
2094
2141
  poolStatus: () => {
2095
2142
  const snapNow = Date.now();
2143
+ const peers = windowPeers(pool.all(), snapNow);
2096
2144
  const snap = new Map();
2097
2145
  for (const a of pool.all()) {
2098
2146
  snap.set(a.alias, {
@@ -2105,9 +2153,13 @@ export async function startProxy(opts = {}) {
2105
2153
  ...rateLimitWindow(a.rateLimit, snapNow),
2106
2154
  claim: a.rateLimit.claim,
2107
2155
  status: reportedAccountStatus(a, snapNow),
2156
+ action: accountAction(a, snapNow),
2108
2157
  requestCount: a.requestCount,
2109
2158
  rejectedCount: a.rejectedCount,
2110
2159
  lastRejectedAt: a.lastRejectedAt ?? null,
2160
+ organizationId: a.organizationId ?? null,
2161
+ sharesWindowWith: peers.get(a.alias) ?? [],
2162
+ readingFrom: a.adoptedFrom ?? null,
2111
2163
  // Raw streak, not just the cooldown boolean: a single 401 also
2112
2164
  // shows `auth-cooldown` for 60s, indistinguishable from a
2113
2165
  // genuinely dead refresh token by that field alone. The magnitude
@@ -2179,6 +2231,7 @@ export async function startProxy(opts = {}) {
2179
2231
  // the `dario accounts` CLI, not HTTP.
2180
2232
  if (urlPath === '/accounts' && req.method === 'GET') {
2181
2233
  const now = Date.now();
2234
+ const peers = windowPeers(pool.all(), now);
2182
2235
  const accounts = pool.all().map(a => {
2183
2236
  const inCooldown = isInAuthCooldown(a, now);
2184
2237
  const cooldownMs = inCooldown && a.lastAuthFailureAt
@@ -2209,11 +2262,21 @@ export async function startProxy(opts = {}) {
2209
2262
  ...rateLimitWindow(a.rateLimit, now),
2210
2263
  claim: a.rateLimit.claim,
2211
2264
  status: reportedAccountStatus(a, now),
2265
+ // The one-word next step: none · wait · regrant (dario#1244).
2266
+ action: accountAction(a, now),
2212
2267
  requestCount: a.requestCount,
2213
2268
  // 429s answered — the attempts requestCount does not count, so a
2214
2269
  // parked seat no longer reads as one that was never called.
2215
2270
  rejectedCount: a.rejectedCount,
2216
2271
  lastRejectedAt: a.lastRejectedAt ?? null,
2272
+ // Which organization the token belongs to, and which other seats
2273
+ // report the same live window — one subscription under several
2274
+ // aliases (dario#1244).
2275
+ organizationId: a.organizationId ?? null,
2276
+ sharesWindowWith: peers.get(a.alias) ?? [],
2277
+ // Whose reading this is: a peer instance's id (shared pool state)
2278
+ // or null for this instance's own.
2279
+ readingFrom: a.adoptedFrom ?? null,
2217
2280
  expiresInMs: Math.max(0, a.expiresAt - now),
2218
2281
  // Refresh-token grant age (refresh-grant.ts): the wall a token
2219
2282
  // refresh cannot move. null fields = grant date unknown.
@@ -2236,6 +2299,11 @@ export async function startProxy(opts = {}) {
2236
2299
  mode: 'pool',
2237
2300
  ...pool.status(),
2238
2301
  stickyBindings: pool.stickyCount(),
2302
+ // Windows the pool really has: each measured window once, each
2303
+ // unmeasured seat as its own.
2304
+ distinctWindows: distinctWindows(pool.all(), now),
2305
+ // Shared pool state (pool-sync.ts) — null when off.
2306
+ sharedState: poolSync ? poolSync.status() : null,
2239
2307
  accounts,
2240
2308
  }));
2241
2309
  return;
@@ -2462,12 +2530,19 @@ export async function startProxy(opts = {}) {
2462
2530
  res.end(JSON.stringify(buildHaltErrorBody(state)));
2463
2531
  return;
2464
2532
  }
2533
+ // Who this request is for (dario#1244 follow-up). An `x-dario-consumer`
2534
+ // header names the consumer for both the per-consumer concurrency cap and
2535
+ // attribution. Without one, attribution falls back to a hash of the
2536
+ // body's user id once the body is parsed; the cap needs the name before
2537
+ // the slot is taken, so only the header gates.
2538
+ const consumerFromHeaders = consumerFromHeader(req.headers[CONSUMER_HEADER]);
2539
+ let consumer = consumerFromHeaders;
2465
2540
  // Proxy to Anthropic (with concurrency control). The bounded queue
2466
2541
  // replaces the v3.30.x-and-earlier unbounded semaphore — dario#80. A
2467
2542
  // queue-full condition returns an explicit 429 with a `"queue-full"`
2468
2543
  // marker in the body; a queue-timeout returns 504 with `"queue-timeout"`.
2469
2544
  try {
2470
- await queue.acquire();
2545
+ await queue.acquire(consumerFromHeaders);
2471
2546
  }
2472
2547
  catch (err) {
2473
2548
  if (err instanceof QueueFullError) {
@@ -2556,6 +2631,31 @@ export async function startProxy(opts = {}) {
2556
2631
  //
2557
2632
  // Shared because two callers must agree: the selector, and the dispatch
2558
2633
  // below for a request the selector DEFERRED but no provider could serve.
2634
+ /**
2635
+ * Every seat in the pool is parked inside a live window (dario#1244).
2636
+ * The old fallback re-probed the earliest-reset seat on every request:
2637
+ * one upstream round trip per request that could only 429,
2638
+ * `rejectedCount` climbing by one each time — 500 on one seat inside a
2639
+ * single window on the reporter's gateway — and the client waiting on a
2640
+ * verdict dario already held. Answer it here: 429, `retry-after` at the
2641
+ * earliest reset, the marker in the same channel the other rejection
2642
+ * classes use, and nothing sent upstream.
2643
+ */
2644
+ const writePoolParked = (untilMs) => {
2645
+ const retryAfterSec = Math.max(1, Math.ceil((untilMs - Date.now()) / 1000));
2646
+ res.writeHead(429, {
2647
+ ...JSON_HEADERS,
2648
+ 'retry-after': String(retryAfterSec),
2649
+ 'x-dario-upstream-rejection': POOL_PARKED,
2650
+ });
2651
+ res.end(JSON.stringify({
2652
+ error: {
2653
+ type: 'rate_limit_error',
2654
+ message: `All ${pool.size} pool seat${pool.size === 1 ? '' : 's'} are over their rate-limit windows; the earliest resets in ${retryAfterSec}s. Nothing was sent upstream.`,
2655
+ },
2656
+ reason: POOL_PARKED,
2657
+ }));
2658
+ };
2559
2659
  const writePoolUnavailable = () => {
2560
2660
  res.writeHead(503, JSON_HEADERS);
2561
2661
  res.end(JSON.stringify(pool.size === 0
@@ -2669,6 +2769,20 @@ export async function startProxy(opts = {}) {
2669
2769
  return true;
2670
2770
  }
2671
2771
  poolAccount = pool.select();
2772
+ if (poolAccount)
2773
+ poolParkedAnnounced = false;
2774
+ // Every seat parked inside a live window (dario#1244): cool the
2775
+ // provider to the earliest reset so a fallback chain sees the Claude
2776
+ // half as what it is, say so once, and — unless a fallback is armed —
2777
+ // answer the client here instead of spending a probe that can only 429.
2778
+ const parkedUntil = poolAccount ? null : pool.parkedUntil();
2779
+ if (parkedUntil !== null) {
2780
+ providerCooldowns.note('claude', parkedUntil - Date.now());
2781
+ if (!poolParkedAnnounced) {
2782
+ poolParkedAnnounced = true;
2783
+ console.error(`[dario] #${requestCount} pool parked: all ${pool.size} seats are over their rate-limit windows, earliest resets in ${Math.max(1, Math.ceil((parkedUntil - Date.now()) / 60000))}m — answering 429 locally until then, nothing sent upstream`);
2784
+ }
2785
+ }
2672
2786
  if (!poolAccount) {
2673
2787
  // Pool-exhausted fallback: when armed, the pool HAS accounts (all
2674
2788
  // drained / cooling), and the client speaks OpenAI shape, defer —
@@ -2694,7 +2808,10 @@ export async function startProxy(opts = {}) {
2694
2808
  // see, not traffic to quietly re-bill somewhere else.
2695
2809
  const fallbackViable = poolFallbackModels.length > 0 && pool.size > 0;
2696
2810
  if (!fallbackViable) {
2697
- writePoolUnavailable();
2811
+ if (parkedUntil !== null)
2812
+ writePoolParked(parkedUntil);
2813
+ else
2814
+ writePoolUnavailable();
2698
2815
  return false;
2699
2816
  }
2700
2817
  }
@@ -2757,8 +2874,11 @@ export async function startProxy(opts = {}) {
2757
2874
  const v = JSON.parse(text);
2758
2875
  if (v === null || typeof v !== 'object' || Array.isArray(v))
2759
2876
  invalid = 'request body must be a JSON object';
2760
- else
2877
+ else {
2761
2878
  parsedBody = v;
2879
+ if (!consumer)
2880
+ consumer = consumerFromBody(parsedBody);
2881
+ }
2762
2882
  }
2763
2883
  catch (err) {
2764
2884
  invalid = `request body is not valid JSON: ${err instanceof Error ? err.message : String(err)}`;
@@ -3055,6 +3175,7 @@ export async function startProxy(opts = {}) {
3055
3175
  codexRequestCounts.set(o.alias, (codexRequestCounts.get(o.alias) ?? 0) + 1);
3056
3176
  analytics.record({
3057
3177
  timestamp: Date.now(),
3178
+ consumer,
3058
3179
  account: o.alias,
3059
3180
  model: o.model || rawModel || 'codex',
3060
3181
  inputTokens: o.inputTokens, outputTokens: o.outputTokens,
@@ -3072,13 +3193,13 @@ export async function startProxy(opts = {}) {
3072
3193
  method: req.method ?? '', path: urlPath, model: o.model || rawModel || undefined,
3073
3194
  status: o.status, latency_ms: o.latencyMs, in_tokens: o.inputTokens, out_tokens: o.outputTokens,
3074
3195
  cache_read: o.cacheReadTokens, cache_create: o.cacheCreateTokens,
3075
- claim: CODEX_CLAIM, bucket: 'subscription', account: o.alias, stream: o.stream,
3196
+ claim: CODEX_CLAIM, bucket: 'subscription', account: o.alias, consumer, stream: o.stream,
3076
3197
  });
3077
3198
  if (verbose)
3078
3199
  console.log(formatUsageLogLine(codexReq, {
3079
3200
  inputTokens: o.inputTokens, outputTokens: o.outputTokens,
3080
3201
  cacheReadTokens: o.cacheReadTokens, cacheCreateTokens: o.cacheCreateTokens,
3081
- }));
3202
+ }, consumer));
3082
3203
  },
3083
3204
  // Cool codex on a rate limit only — a 5xx or an unreachable backend
3084
3205
  // is an outage, and parking a provider for that would keep it out
@@ -3222,6 +3343,13 @@ export async function startProxy(opts = {}) {
3222
3343
  // would fall through to the Claude path with no account and an empty
3223
3344
  // bearer token, turning a clean 503 into a confusing upstream 401.
3224
3345
  if (!upstreamApiKey && !poolAccount) {
3346
+ // A fallback was armed but nothing could serve, and the pool itself is
3347
+ // parked: the exact reset beats a cool-down estimate (dario#1244).
3348
+ const parkedNow = pool.parkedUntil();
3349
+ if (parkedNow !== null) {
3350
+ writePoolParked(parkedNow);
3351
+ return;
3352
+ }
3225
3353
  // A chain where every entry is cooling is a rate limit, not a
3226
3354
  // misconfiguration — say so in the machine-readable way, once.
3227
3355
  if (allProvidersCooled(poolFallbackModels.length > 0 ? ['codex', 'claude'] : ['claude'], providerCooldowns)) {
@@ -3332,7 +3460,20 @@ export async function startProxy(opts = {}) {
3332
3460
  // Rotating off mid-session costs cache-create on every turn.
3333
3461
  stickyKey = computeStickyKey(userMsg);
3334
3462
  if (stickyKey && !pinnedAccount) {
3463
+ // Shared state (pool-sync.ts): a conversation a peer instance
3464
+ // already bound lands on the same seat here, so its prompt
3465
+ // cache is read rather than rewritten. Only consulted when this
3466
+ // instance holds no binding of its own; a binding made here is
3467
+ // published for the peers.
3468
+ const hadLocalBinding = pool.stickyAliasFor(stickyKey) !== null;
3469
+ if (poolSync && !hadLocalBinding) {
3470
+ const peerAlias = await poolSync.lookupSticky(stickyKey);
3471
+ if (peerAlias)
3472
+ pool.rebindSticky(stickyKey, peerAlias);
3473
+ }
3335
3474
  const preferred = pool.selectSticky(stickyKey, modelFamily(requestModel));
3475
+ if (poolSync && preferred && !hadLocalBinding)
3476
+ poolSync.bindSticky(stickyKey, preferred.alias);
3336
3477
  if (preferred && preferred.alias !== poolAccount?.alias) {
3337
3478
  poolAccount = preferred;
3338
3479
  accessToken = preferred.accessToken;
@@ -3746,6 +3887,14 @@ export async function startProxy(opts = {}) {
3746
3887
  else {
3747
3888
  pool.updateRateLimits(poolAccount.alias, snapshot);
3748
3889
  }
3890
+ // Which organization answered (dario#1244). Held in the pool at
3891
+ // once; written to the seat's record by its next token refresh —
3892
+ // the write that already exists — so nothing here races a refresh.
3893
+ const organizationId = upstream.headers.get('anthropic-organization-id');
3894
+ if (organizationId)
3895
+ pool.noteOrganization(poolAccount.alias, organizationId);
3896
+ announceWindowPeers(poolAccount.alias);
3897
+ poolSync?.reportSeat(poolAccount.alias);
3749
3898
  // First-sight detector for per-model rate-limit buckets. Anthropic
3750
3899
  // ships these unannounced — e.g. `7d_sonnet-utilization` appeared
3751
3900
  // around 2026-04-25 — and verbose-mode users want a heads-up the
@@ -4021,6 +4170,7 @@ export async function startProxy(opts = {}) {
4021
4170
  headers['Authorization'] = `Bearer ${accessToken}`;
4022
4171
  headers['x-claude-code-session-id'] = resolveOutboundSession(nextAccount, clientSessionKey).sessionId;
4023
4172
  pool.rebindSticky(stickyKey, nextAccount.alias);
4173
+ poolSync?.bindSticky(stickyKey, nextAccount.alias);
4024
4174
  peekedBody = null;
4025
4175
  continue dispatchLoop;
4026
4176
  }
@@ -4068,6 +4218,7 @@ export async function startProxy(opts = {}) {
4068
4218
  const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
4069
4219
  analytics.record({
4070
4220
  timestamp: Date.now(),
4221
+ consumer,
4071
4222
  account: poolAccount?.alias ?? ACCOUNT_KEY_APIKEY,
4072
4223
  model: requestModel,
4073
4224
  inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
@@ -4121,6 +4272,7 @@ export async function startProxy(opts = {}) {
4121
4272
  headers['Authorization'] = `Bearer ${accessToken}`;
4122
4273
  headers['x-claude-code-session-id'] = resolveOutboundSession(nextAccount, clientSessionKey).sessionId;
4123
4274
  pool.rebindSticky(stickyKey, nextAccount.alias);
4275
+ poolSync?.bindSticky(stickyKey, nextAccount.alias);
4124
4276
  continue dispatchLoop;
4125
4277
  }
4126
4278
  // No peer available — forward the saved generic-403 bytes when the
@@ -4154,6 +4306,7 @@ export async function startProxy(opts = {}) {
4154
4306
  headers['Authorization'] = `Bearer ${accessToken}`;
4155
4307
  headers['x-claude-code-session-id'] = resolveOutboundSession(nextAccount, clientSessionKey).sessionId;
4156
4308
  pool.rebindSticky(stickyKey, nextAccount.alias);
4309
+ poolSync?.bindSticky(stickyKey, nextAccount.alias);
4157
4310
  continue dispatchLoop;
4158
4311
  }
4159
4312
  }
@@ -4189,6 +4342,7 @@ export async function startProxy(opts = {}) {
4189
4342
  const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
4190
4343
  analytics.record({
4191
4344
  timestamp: Date.now(),
4345
+ consumer,
4192
4346
  account: poolAccount?.alias ?? ACCOUNT_KEY_APIKEY,
4193
4347
  model: requestModel,
4194
4348
  inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
@@ -4446,6 +4600,7 @@ export async function startProxy(opts = {}) {
4446
4600
  const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
4447
4601
  analytics.record({
4448
4602
  timestamp: Date.now(),
4603
+ consumer,
4449
4604
  account: poolAccount?.alias ?? ACCOUNT_KEY_APIKEY,
4450
4605
  model: requestModel,
4451
4606
  inputTokens: streamInputTokens, outputTokens: streamOutputTokens,
@@ -4465,6 +4620,7 @@ export async function startProxy(opts = {}) {
4465
4620
  claim: poolAccount?.rateLimit.claim,
4466
4621
  bucket: poolAccount ? billingBucketFromClaim(poolAccount.rateLimit.claim) : undefined,
4467
4622
  account: poolAccount?.alias,
4623
+ consumer,
4468
4624
  client: detectedClientForLog,
4469
4625
  preserve_tools: preserveToolsEffective,
4470
4626
  stream: true,
@@ -4473,7 +4629,7 @@ export async function startProxy(opts = {}) {
4473
4629
  console.log(formatUsageLogLine(requestCount, {
4474
4630
  inputTokens: streamInputTokens, outputTokens: streamOutputTokens,
4475
4631
  cacheReadTokens: streamCacheReadTokens, cacheCreateTokens: streamCacheCreateTokens,
4476
- }));
4632
+ }, consumer));
4477
4633
  }
4478
4634
  else {
4479
4635
  // Buffer and forward
@@ -4512,6 +4668,7 @@ export async function startProxy(opts = {}) {
4512
4668
  const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
4513
4669
  analytics.record({
4514
4670
  timestamp: Date.now(),
4671
+ consumer,
4515
4672
  account: poolAccount?.alias ?? ACCOUNT_KEY_APIKEY,
4516
4673
  model: bufferedUsage.model || requestModel,
4517
4674
  inputTokens: bufferedUsage.inputTokens, outputTokens: bufferedUsage.outputTokens,
@@ -4533,12 +4690,13 @@ export async function startProxy(opts = {}) {
4533
4690
  claim: poolAccount?.rateLimit.claim,
4534
4691
  bucket: poolAccount ? billingBucketFromClaim(poolAccount.rateLimit.claim) : undefined,
4535
4692
  account: poolAccount?.alias,
4693
+ consumer,
4536
4694
  client: detectedClientForLog,
4537
4695
  preserve_tools: preserveToolsEffective,
4538
4696
  stream: false,
4539
4697
  });
4540
4698
  if (verbose && bufferedUsage)
4541
- console.log(formatUsageLogLine(requestCount, bufferedUsage));
4699
+ console.log(formatUsageLogLine(requestCount, bufferedUsage, consumer));
4542
4700
  if (verbose)
4543
4701
  console.log(`[dario] #${requestCount} ${upstream.status}`);
4544
4702
  }
@@ -4550,6 +4708,7 @@ export async function startProxy(opts = {}) {
4550
4708
  ts: new Date().toISOString(), req: requestCount,
4551
4709
  method: req.method ?? '', path: urlPath,
4552
4710
  model: requestModel || undefined,
4711
+ consumer,
4553
4712
  client: detectedClientForLog,
4554
4713
  preserve_tools: preserveToolsEffective,
4555
4714
  };
@@ -4590,7 +4749,7 @@ export async function startProxy(opts = {}) {
4590
4749
  clearTimeout(upstreamTimeout);
4591
4750
  if (onClientClose !== null)
4592
4751
  req.off('close', onClientClose);
4593
- queue.release();
4752
+ queue.release(consumerFromHeaders);
4594
4753
  }
4595
4754
  });
4596
4755
  server.on('error', async (err) => {
@@ -4819,6 +4978,7 @@ export async function startProxy(opts = {}) {
4819
4978
  console.log('\n[dario] Shutting down...');
4820
4979
  clearInterval(presenceInterval);
4821
4980
  clearInterval(refreshInterval);
4981
+ poolSync?.stop();
4822
4982
  if (logFileStream)
4823
4983
  logFileStream.end();
4824
4984
  // 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
  }