@askalf/dario 6.0.38 → 6.0.39
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/accounts.d.ts +87 -0
- package/dist/accounts.js +162 -19
- package/dist/admin-api.d.ts +14 -1
- package/dist/admin-api.js +10 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +80 -3
- package/dist/doctor-core.d.ts +34 -0
- package/dist/doctor-core.js +91 -3
- package/dist/pool.d.ts +146 -17
- package/dist/pool.js +264 -54
- package/dist/proxy.js +86 -28
- package/docs/admin-api.md +1 -3
- package/docs/commands.md +1 -0
- package/docs/multi-account-pool.md +26 -7
- package/package.json +1 -1
package/dist/proxy.js
CHANGED
|
@@ -12,7 +12,8 @@ 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,
|
|
15
|
+
import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, reconcilePoolAccounts, resolvePoolStrategy, utilFreshness, rateLimitWindow, accountAction, accountPeers, distinctAccounts, describeRejection, maskEmail } from './pool.js';
|
|
16
|
+
import { backfillIdentity } from './accounts.js';
|
|
16
17
|
import { PoolSync, DEFAULT_POOL_SYNC_INTERVAL_MS } from './pool-sync.js';
|
|
17
18
|
import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, CODEX_CLAIM } from './analytics.js';
|
|
18
19
|
import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
|
|
@@ -1330,22 +1331,23 @@ export async function startProxy(opts = {}) {
|
|
|
1330
1331
|
const accountsList = await loadAllAccounts();
|
|
1331
1332
|
const poolStrategy = resolvePoolStrategy(opts.poolStrategy);
|
|
1332
1333
|
const pool = new AccountPool(poolStrategy);
|
|
1333
|
-
// Two aliases
|
|
1334
|
-
// (dario#1244). Said once per pair,
|
|
1335
|
-
// listings carry it
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1334
|
+
// Two aliases that are one account (same OAuth account uuid) are one
|
|
1335
|
+
// subscription counted twice (dario#1244). Said once per pair, from what the
|
|
1336
|
+
// records know at load and after any reconcile; the listings carry it
|
|
1337
|
+
// permanently as `sameAccountAs` / `sharesWindowWith`. Identity, not the
|
|
1338
|
+
// reset-second inference this replaced (dario#1263).
|
|
1339
|
+
const announcedAccountPairs = new Set();
|
|
1340
|
+
const announceSameAccounts = () => {
|
|
1341
|
+
const peers = accountPeers(pool.all());
|
|
1342
|
+
for (const seat of pool.all()) {
|
|
1343
|
+
for (const peer of peers.get(seat.alias) ?? []) {
|
|
1344
|
+
const pair = [seat.alias, peer].sort().join('|');
|
|
1345
|
+
if (announcedAccountPairs.has(pair))
|
|
1346
|
+
continue;
|
|
1347
|
+
announcedAccountPairs.add(pair);
|
|
1348
|
+
const who = seat.accountEmail ? ` (${maskEmail(seat.accountEmail)})` : '';
|
|
1349
|
+
console.error(`[dario] seats "${seat.alias}" and "${peer}" are the same account${who} — one subscription under two aliases; ${distinctAccounts(pool.all())} distinct accounts across ${pool.size} seats`);
|
|
1350
|
+
}
|
|
1349
1351
|
}
|
|
1350
1352
|
};
|
|
1351
1353
|
// Shared pool state across instances (docs/multi-instance.md): opt-in,
|
|
@@ -1437,8 +1439,13 @@ export async function startProxy(opts = {}) {
|
|
|
1437
1439
|
accountUuid: acc.accountUuid,
|
|
1438
1440
|
grantedAt: acc.grantedAt,
|
|
1439
1441
|
organizationId: acc.organizationId,
|
|
1442
|
+
accountId: acc.accountId,
|
|
1443
|
+
accountEmail: acc.accountEmail,
|
|
1444
|
+
rateLimitTier: acc.rateLimitTier,
|
|
1445
|
+
seatTier: acc.seatTier,
|
|
1440
1446
|
});
|
|
1441
1447
|
}
|
|
1448
|
+
announceSameAccounts();
|
|
1442
1449
|
// Startup self-heal (dario#790): eagerly refresh any account whose access
|
|
1443
1450
|
// token is already expired or within the 45-min refresh window BEFORE the
|
|
1444
1451
|
// proxy starts serving. On a container recreate after >8h uptime the
|
|
@@ -1461,6 +1468,22 @@ export async function startProxy(opts = {}) {
|
|
|
1461
1468
|
// observed on (dario#1244) — the one write that touches the record.
|
|
1462
1469
|
const refreshed = await refreshAccountToken(withObservedOrganization(saved, acc.organizationId));
|
|
1463
1470
|
pool.updateTokens(acc.alias, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt);
|
|
1471
|
+
// Identity back-fill (dario#1263): a record from before accountId learns
|
|
1472
|
+
// who it is on its first refresh under this release — one GET through
|
|
1473
|
+
// this proxy's fetch, never fatal — and the running seat is reconciled so
|
|
1474
|
+
// the listings and the same-account line know at once.
|
|
1475
|
+
if (!refreshed.accountId) {
|
|
1476
|
+
const identified = await backfillIdentity(refreshed, fetch).catch(() => null);
|
|
1477
|
+
if (identified?.accountId) {
|
|
1478
|
+
pool.add(acc.alias, {
|
|
1479
|
+
accessToken: identified.accessToken, refreshToken: identified.refreshToken, expiresAt: identified.expiresAt,
|
|
1480
|
+
deviceId: identified.deviceId, accountUuid: identified.accountUuid, grantedAt: identified.grantedAt,
|
|
1481
|
+
organizationId: identified.organizationId, accountId: identified.accountId, accountEmail: identified.accountEmail,
|
|
1482
|
+
rateLimitTier: identified.rateLimitTier, seatTier: identified.seatTier,
|
|
1483
|
+
});
|
|
1484
|
+
announceSameAccounts();
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1464
1487
|
// Mirror a refreshed `login` token back to credentials.json so the
|
|
1465
1488
|
// legacy file (and `dario doctor`) tracks the pool store (#808).
|
|
1466
1489
|
await mirrorLoginToCredentials(refreshed).catch((err) => {
|
|
@@ -1515,6 +1538,22 @@ export async function startProxy(opts = {}) {
|
|
|
1515
1538
|
// observed on (dario#1244) — the one write that touches the record.
|
|
1516
1539
|
const refreshed = await refreshAccountToken(withObservedOrganization(saved, acc.organizationId));
|
|
1517
1540
|
pool.updateTokens(acc.alias, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt);
|
|
1541
|
+
// Identity back-fill (dario#1263): a record from before accountId learns
|
|
1542
|
+
// who it is on its first refresh under this release — one GET through
|
|
1543
|
+
// this proxy's fetch, never fatal — and the running seat is reconciled so
|
|
1544
|
+
// the listings and the same-account line know at once.
|
|
1545
|
+
if (!refreshed.accountId) {
|
|
1546
|
+
const identified = await backfillIdentity(refreshed, fetch).catch(() => null);
|
|
1547
|
+
if (identified?.accountId) {
|
|
1548
|
+
pool.add(acc.alias, {
|
|
1549
|
+
accessToken: identified.accessToken, refreshToken: identified.refreshToken, expiresAt: identified.expiresAt,
|
|
1550
|
+
deviceId: identified.deviceId, accountUuid: identified.accountUuid, grantedAt: identified.grantedAt,
|
|
1551
|
+
organizationId: identified.organizationId, accountId: identified.accountId, accountEmail: identified.accountEmail,
|
|
1552
|
+
rateLimitTier: identified.rateLimitTier, seatTier: identified.seatTier,
|
|
1553
|
+
});
|
|
1554
|
+
announceSameAccounts();
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1518
1557
|
// Mirror a refreshed `login` token back to credentials.json so the
|
|
1519
1558
|
// legacy file (and `dario doctor`) tracks the pool store (#808).
|
|
1520
1559
|
await mirrorLoginToCredentials(refreshed).catch((err) => {
|
|
@@ -1559,7 +1598,12 @@ export async function startProxy(opts = {}) {
|
|
|
1559
1598
|
accountUuid: acc.accountUuid,
|
|
1560
1599
|
grantedAt: acc.grantedAt,
|
|
1561
1600
|
organizationId: acc.organizationId,
|
|
1601
|
+
accountId: acc.accountId,
|
|
1602
|
+
accountEmail: acc.accountEmail,
|
|
1603
|
+
rateLimitTier: acc.rateLimitTier,
|
|
1604
|
+
seatTier: acc.seatTier,
|
|
1562
1605
|
});
|
|
1606
|
+
announceSameAccounts();
|
|
1563
1607
|
}
|
|
1564
1608
|
}
|
|
1565
1609
|
}
|
|
@@ -2147,7 +2191,7 @@ export async function startProxy(opts = {}) {
|
|
|
2147
2191
|
// just persisted metadata — the same snapshot GET /accounts exposes.
|
|
2148
2192
|
poolStatus: () => {
|
|
2149
2193
|
const snapNow = Date.now();
|
|
2150
|
-
const peers =
|
|
2194
|
+
const peers = accountPeers(pool.all());
|
|
2151
2195
|
const snap = new Map();
|
|
2152
2196
|
for (const a of pool.all()) {
|
|
2153
2197
|
snap.set(a.alias, {
|
|
@@ -2166,6 +2210,11 @@ export async function startProxy(opts = {}) {
|
|
|
2166
2210
|
lastRejectedAt: a.lastRejectedAt ?? null,
|
|
2167
2211
|
organizationId: a.organizationId ?? null,
|
|
2168
2212
|
sharesWindowWith: peers.get(a.alias) ?? [],
|
|
2213
|
+
sameAccountAs: peers.get(a.alias) ?? [],
|
|
2214
|
+
accountId: a.accountId ?? null,
|
|
2215
|
+
accountEmail: maskEmail(a.accountEmail),
|
|
2216
|
+
rateLimitTier: a.rateLimitTier ?? null,
|
|
2217
|
+
seatTier: a.seatTier ?? null,
|
|
2169
2218
|
readingFrom: a.adoptedFrom ?? null,
|
|
2170
2219
|
// Raw streak, not just the cooldown boolean: a single 401 also
|
|
2171
2220
|
// shows `auth-cooldown` for 60s, indistinguishable from a
|
|
@@ -2238,7 +2287,7 @@ export async function startProxy(opts = {}) {
|
|
|
2238
2287
|
// the `dario accounts` CLI, not HTTP.
|
|
2239
2288
|
if (urlPath === '/accounts' && req.method === 'GET') {
|
|
2240
2289
|
const now = Date.now();
|
|
2241
|
-
const peers =
|
|
2290
|
+
const peers = accountPeers(pool.all());
|
|
2242
2291
|
const accounts = pool.all().map(a => {
|
|
2243
2292
|
const inCooldown = isInAuthCooldown(a, now);
|
|
2244
2293
|
const cooldownMs = inCooldown && a.lastAuthFailureAt
|
|
@@ -2276,11 +2325,17 @@ export async function startProxy(opts = {}) {
|
|
|
2276
2325
|
// parked seat no longer reads as one that was never called.
|
|
2277
2326
|
rejectedCount: a.rejectedCount,
|
|
2278
2327
|
lastRejectedAt: a.lastRejectedAt ?? null,
|
|
2279
|
-
// Which organization the token belongs to,
|
|
2280
|
-
//
|
|
2281
|
-
// aliases (dario#1244
|
|
2328
|
+
// Which organization the token belongs to, who the token IS (OAuth
|
|
2329
|
+
// account uuid, masked email), and which other seats are the same
|
|
2330
|
+
// account — one subscription under several aliases (dario#1244,
|
|
2331
|
+
// #1263: identity, not a shared reset second).
|
|
2282
2332
|
organizationId: a.organizationId ?? null,
|
|
2283
2333
|
sharesWindowWith: peers.get(a.alias) ?? [],
|
|
2334
|
+
sameAccountAs: peers.get(a.alias) ?? [],
|
|
2335
|
+
accountId: a.accountId ?? null,
|
|
2336
|
+
accountEmail: maskEmail(a.accountEmail),
|
|
2337
|
+
rateLimitTier: a.rateLimitTier ?? null,
|
|
2338
|
+
seatTier: a.seatTier ?? null,
|
|
2284
2339
|
// Whose reading this is: a peer instance's id (shared pool state)
|
|
2285
2340
|
// or null for this instance's own.
|
|
2286
2341
|
readingFrom: a.adoptedFrom ?? null,
|
|
@@ -2306,9 +2361,11 @@ export async function startProxy(opts = {}) {
|
|
|
2306
2361
|
mode: 'pool',
|
|
2307
2362
|
...pool.status(),
|
|
2308
2363
|
stickyBindings: pool.stickyCount(),
|
|
2309
|
-
//
|
|
2310
|
-
//
|
|
2311
|
-
|
|
2364
|
+
// Accounts the pool really has: each identified account once, each
|
|
2365
|
+
// not-yet-identified seat as its own. `distinctWindows` keeps its name
|
|
2366
|
+
// for readers of the older payload; an account is its windows.
|
|
2367
|
+
distinctWindows: distinctAccounts(pool.all()),
|
|
2368
|
+
distinctAccounts: distinctAccounts(pool.all()),
|
|
2312
2369
|
// Shared pool state (pool-sync.ts) — null when off.
|
|
2313
2370
|
sharedState: poolSync ? poolSync.status() : null,
|
|
2314
2371
|
accounts,
|
|
@@ -3920,7 +3977,9 @@ export async function startProxy(opts = {}) {
|
|
|
3920
3977
|
// returns status='rejected' on 429, which makes the next `select()` call
|
|
3921
3978
|
// route traffic away from this account until it resets.
|
|
3922
3979
|
if (poolAccount) {
|
|
3923
|
-
|
|
3980
|
+
// The family is what lets a bucket the wire does not name by family
|
|
3981
|
+
// (`7d_oi`) be learned as binding THIS request's model (dario#1262).
|
|
3982
|
+
const snapshot = parseRateLimits(upstream.headers, modelFamily(requestModel));
|
|
3924
3983
|
if (upstream.status === 429) {
|
|
3925
3984
|
// Say so the moment a seat leaves rotation. With a peer to fail
|
|
3926
3985
|
// over to the client sees 200, and nothing else named the seat,
|
|
@@ -3929,7 +3988,7 @@ export async function startProxy(opts = {}) {
|
|
|
3929
3988
|
// those repeats are verbose-only.
|
|
3930
3989
|
const parked = pool.markRejected(poolAccount.alias, snapshot);
|
|
3931
3990
|
if (parked || verbose) {
|
|
3932
|
-
console.error(`[dario] #${requestCount} rate limited (429) on account "${poolAccount.alias}": ${
|
|
3991
|
+
console.error(`[dario] #${requestCount} rate limited (429) on account "${poolAccount.alias}": ${describeRejection(pool.get(poolAccount.alias)?.rateLimit ?? snapshot)}`);
|
|
3933
3992
|
}
|
|
3934
3993
|
}
|
|
3935
3994
|
else {
|
|
@@ -3941,7 +4000,6 @@ export async function startProxy(opts = {}) {
|
|
|
3941
4000
|
const organizationId = upstream.headers.get('anthropic-organization-id');
|
|
3942
4001
|
if (organizationId)
|
|
3943
4002
|
pool.noteOrganization(poolAccount.alias, organizationId);
|
|
3944
|
-
announceWindowPeers(poolAccount.alias);
|
|
3945
4003
|
poolSync?.reportSeat(poolAccount.alias);
|
|
3946
4004
|
// First-sight detector for per-model rate-limit buckets. Anthropic
|
|
3947
4005
|
// ships these unannounced — e.g. `7d_sonnet-utilization` appeared
|
package/docs/admin-api.md
CHANGED
|
@@ -104,9 +104,7 @@ rejection lifts), representative `claim` (e.g. `five_hour`), routing
|
|
|
104
104
|
`status`, `request_count` (requests served), `rejected_count` /
|
|
105
105
|
`last_rejected_at` (429s answered — a 429 serves nothing, so it is not a
|
|
106
106
|
request), `organization_id` (the organization the token belongs to, learned
|
|
107
|
-
from its responses and written to the record with its next refresh), `shares_window_with` (aliases
|
|
108
|
-
reading names the same live window — one subscription under several aliases,
|
|
109
|
-
see [One subscription under two aliases](./multi-account-pool.md#one-subscription-under-two-aliases)),
|
|
107
|
+
from its responses and written to the record with its next refresh), `account_id` / `account_email` (who the token is, from its OAuth profile at grant time; the email masked), `same_account_as` (aliases that are the same account — one subscription under several aliases; `shares_window_with` is the same list under its older name, see [One subscription under two aliases](./multi-account-pool.md#one-subscription-under-two-aliases)), `rate_limit_tier` / `seat_tier` when the profile stated them,
|
|
110
108
|
and `consecutive_auth_failures`. What each `status` means and what
|
|
111
109
|
to do about it: [Reading a seat's `status`](./multi-account-pool.md#reading-a-seats-status).
|
|
112
110
|
It's the admin-token-gated equivalent of the proxy-key-gated `GET /accounts`
|
package/docs/commands.md
CHANGED
|
@@ -16,6 +16,7 @@ This page is the per-flag reference. For environment variables grouped by task
|
|
|
16
16
|
| `dario refresh` | Force an immediate Claude token refresh |
|
|
17
17
|
| `dario logout` | Delete stored Claude credentials |
|
|
18
18
|
| `dario accounts check <alias> [--models=a,b]` | Read-only, in-place seat probe: one tiny request per model, pinned to that seat through the running proxy (`x-dario-account` + `x-dario-admin-token`, needs `DARIO_ADMIN=1`). A pinned request never fails over, so the upstream status is the seat's own answer. |
|
|
19
|
+
| `dario accounts identity [--fresh <alias>...\|--all]` | Which client identity each seat presents in `metadata.user_id`, where it came from, and which seats share one across different accounts; `--fresh` gives the named seats their own (the running proxy presents it on the next request). See [Client identity](./multi-account-pool.md#client-identity-what-a-seat-presents-as). |
|
|
19
20
|
| `dario accounts list` / `add <alias>` / `remove <alias>` | Multi-account pool management. `add <alias>` on a fresh pool auto back-fills your existing `dario login` credentials as `login`, so your first `add` trips the 2+ pool threshold on its own — see [Multi-account pool mode](./multi-account-pool.md). |
|
|
20
21
|
| `dario backend list` / `add <name> --key=<key> [--base-url=<url>]` / `remove <name>` | OpenAI-compat backend management |
|
|
21
22
|
| `dario subagent install` / `remove` / `status` | CC sub-agent lifecycle. See [sub-agent hook](./sub-agent.md). |
|
|
@@ -98,10 +98,12 @@ curl http://localhost:3456/analytics # per-account / per-model stats, burn ra
|
|
|
98
98
|
| `status` | What it means | What to do |
|
|
99
99
|
|---|---|---|
|
|
100
100
|
| `allowed` | The seat's last response was a 200 with headroom. `util5h` / `util7d` are that response's reading — a ratio against 1.0, so `0.42` is 42% — `lastObservedAt` / `utilAgeMs` say how old it is, `resetAt` / `resetInMs` when its representative window rolls. | Nothing. |
|
|
101
|
-
| `rejected` | The seat's last response was a 429
|
|
101
|
+
| `rejected` | The seat's last response was a 429 **that named an exhausted window**: `claim` says which (`five_hour`, `seven_day`, …) and the reading is at or past the 1.0 threshold — `util5h: 1.04` is 104% of the five-hour window, not 1%. `rejectedCount` / `lastRejectedAt` say the seat was tried — a 429 serves nothing, so `requestCount` does not move — and `resetInMs` says how long it stays parked. Requests route around it; it returns on its own when the window rolls. | Nothing — the window clears itself. If the reading surprises you (your usage page for that account says 0%), the token belongs to a different organization than the page you are looking at, or to the same organization as another seat: the reading is Anthropic's own, taken on that token. `dario accounts check <alias>` asks the seat directly. |
|
|
102
102
|
| `unknown` | No current observation: a seat that has served nothing yet, or a rejection whose window has rolled (`resetInMs: 0`) and that nothing has measured since. | Nothing; the next request measures it. |
|
|
103
103
|
| `auth-cooldown` | Upstream answered 401/403 or `invalid_grant`. `consecutiveAuthFailures` tells a blip (1) from a dead refresh token (a streak); the cool-down doubles with the streak, from 1 minute to 30. | A streak means re-grant the seat — `dario accounts remove` + `add`, or the admin login flow under the same alias. A new grant starts the seat fresh: no carried-over cool-down, rejection or identity. See [Refresh-token grant age](#refresh-token-grant-age) for the 28-day wall behind most streaks. |
|
|
104
104
|
|
|
105
|
+
**A 429 that names no exhausted window is not a parking.** A rejection whose headers show no claim, or a claim with utilization nowhere near 1.0 (`5h 0%, 7d 0%, claim unknown`), is a refusal of some other kind — concurrency, an account-level lock, a monthly credit — and the `reset` it states is not this seat's window rolling. Until 6.0.39 the status code alone decided, and a seat on the fleet box was parked for 546 hours on exactly that reading. Such a seat now cools for the response's own `retry-after` (or one minute) and stays probeable; the log says so: `429 without an exhausted window (5h 0%, 7d 0%, claim unknown; stated reset in 546h 49m not honoured) — cooling 1m, seat stays probeable`.
|
|
106
|
+
|
|
105
107
|
**When every seat is parked.** A pool whose seats are all `rejected` inside live windows does not probe them again: dario answers the request itself with `429`, `retry-after` set to the earliest reset, `x-dario-upstream-rejection: pool_parked`, and nothing sent upstream. One log line marks the transition (`pool parked: all 6 seats are over their rate-limit windows, earliest resets in 21m`). Before 6.0.35 every such request re-probed the earliest-reset seat, so `rejectedCount` on that seat grew by one per request — a seat reading `rejected_count: 500` next to `request_count: 1` was that, not a seat that needed a re-login. With a `--pool-fallback` armed, the request goes to the fallback instead, as before.
|
|
106
108
|
|
|
107
109
|
The proxy logs every parking as it happens, once per window: `rate limited (429) on account "spare": 5h 104%, 7d 25%, claim five_hour, resets in 37m — parked until the window rolls`. The re-probes the all-exhausted fallback makes of an already-parked seat are logged only under `-v`.
|
|
@@ -110,16 +112,33 @@ The proxy logs every parking as it happens, once per window: `rate limited (429)
|
|
|
110
112
|
|
|
111
113
|
## One subscription under two aliases
|
|
112
114
|
|
|
113
|
-
A pool of six is only six
|
|
115
|
+
A pool of six is only six accounts if the six tokens belong to six accounts. Two aliases granted from the same account share one set of windows and one set of limits — the pool routes on real headroom either way, and the duplicate simply parks on the first 429 until the window rolls, but the operator should know.
|
|
116
|
+
|
|
117
|
+
dario knows it from the token itself. At grant time (`dario accounts add`, the admin login, the keychain import, the `login` back-fill) it reads the token's OAuth profile and records the **account uuid** — plus the masked email and the organization's tier fields — on the seat's record. A record written before this exists is filled in on its next token refresh.
|
|
118
|
+
|
|
119
|
+
- **`accountId`** / **`accountEmail`** — who the token is. Two seats with the same `accountId` are the same account, full stop.
|
|
120
|
+
- **`sameAccountAs`** (and `sharesWindowWith`, the same list under its original name) — the other aliases that are this account.
|
|
121
|
+
- **`organizationId`** — the `anthropic-organization-id` the seat's responses carry. Several accounts can share an organization (a Team) and still have their own windows: *organization* is not *account*.
|
|
122
|
+
|
|
123
|
+
Until 6.0.38, `sharesWindowWith` was **inferred**: two seats whose last readings named the same window (`claim@reset`) were called one subscription, on the assumption that independent windows never share a reset second. They do — Anthropic aligns the five-hour reset to a 20-minute grid, so a window has 15 possible reset seconds and a pool of 18 seats collides by pigeonhole. That inference told an operator seven independent colleagues were one subscription (dario#1263). It is gone; nothing is claimed that the token did not say.
|
|
124
|
+
|
|
125
|
+
Both facts are on `GET /accounts` (`distinctAccounts` — and `distinctWindows`, kept for readers of the older payload — count the accounts the pool really has), on `GET /admin/accounts` as `account_id` / `account_email` / `same_account_as`, in `dario accounts list --live`, and on the `Accounts` row of `dario doctor`. The proxy also says it once per pair at start-up: `seats "busy" and "twin" are the same account (ma***@example.com)`.
|
|
114
126
|
|
|
115
|
-
|
|
127
|
+
## Client identity: what a seat presents as
|
|
116
128
|
|
|
117
|
-
|
|
118
|
-
- **`sharesWindowWith`** — the other aliases whose last reading names the same live window (same representative claim, same reset second). Two independent windows all but never share a reset second; two readings of one window always do. This is the fact that matters for headroom, and it is deliberately not derived from the organization: seats on one organization can still have their own windows.
|
|
129
|
+
Every request carries `metadata.user_id` — a client identity (`device_id`, `account_uuid`) that Claude Code derives from its install, and that Anthropic ties the bearer token to. dario stores one per seat.
|
|
119
130
|
|
|
120
|
-
|
|
131
|
+
Before 6.0.39 every add path copied the **machine's** Claude Code identity into every alias when Claude Code was installed. On a machine running a pool of colleagues' tokens that meant eighteen different accounts all presenting one identity (dario#1244). Whether that alone changes what Anthropic counts is not established — it is hygiene, not a diagnosis — but a seat should present the account it belongs to.
|
|
132
|
+
|
|
133
|
+
Now a new alias takes the local Claude Code identity only when no other alias holds it, or when the holder is proven (same `accountId`) to be the same account; otherwise the alias gets its own, exactly as a machine without Claude Code always did. Existing seats are not rewritten behind your back. To see what each seat presents, and which seats share one identity across different accounts:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
dario accounts identity # per-seat report
|
|
137
|
+
dario accounts identity --fresh <alias>... # give these seats their own
|
|
138
|
+
dario accounts identity --fresh --all
|
|
139
|
+
```
|
|
121
140
|
|
|
122
|
-
|
|
141
|
+
The running proxy presents a rewritten identity on the seat's next request; no restart. `dario doctor` warns on the `Client identity` row when seats share one across different accounts.
|
|
123
142
|
|
|
124
143
|
## Consumers: who a request is for
|
|
125
144
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.39",
|
|
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": {
|