@askalf/dario 6.0.37 → 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/anthropic-responses-translate.d.ts +20 -1
- package/dist/anthropic-responses-translate.js +15 -2
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +80 -3
- package/dist/codex-backend.d.ts +16 -3
- package/dist/codex-backend.js +30 -15
- package/dist/doctor-core.d.ts +34 -0
- package/dist/doctor-core.js +91 -3
- package/dist/effort.d.ts +14 -0
- package/dist/effort.js +26 -0
- package/dist/pool.d.ts +146 -17
- package/dist/pool.js +264 -54
- package/dist/proxy.js +142 -36
- package/docs/admin-api.md +1 -3
- package/docs/commands.md +1 -0
- package/docs/configuration.md +33 -0
- package/docs/multi-account-pool.md +26 -7
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -15,7 +15,26 @@ export interface AccountCredentials {
|
|
|
15
15
|
* on a fresh grant.
|
|
16
16
|
*/
|
|
17
17
|
organizationId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The Anthropic account uuid this grant belongs to, from the OAuth profile
|
|
20
|
+
* (`fetchOAuthProfile`) at grant time — or, for a record from before this
|
|
21
|
+
* field, on its next token refresh. THE fact that says whether two aliases
|
|
22
|
+
* are one subscription (dario#1244, #1263); everything else was inference.
|
|
23
|
+
*/
|
|
24
|
+
accountId?: string;
|
|
25
|
+
/** Email on that account. Masked on every listing (`maskEmail`); stored whole. */
|
|
26
|
+
accountEmail?: string;
|
|
27
|
+
/** `organization.organization_type` / `rate_limit_tier` / `seat_tier` from the same profile. */
|
|
28
|
+
organizationType?: string;
|
|
29
|
+
rateLimitTier?: string;
|
|
30
|
+
seatTier?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Where `deviceId`/`accountUuid` came from: the machine's Claude Code identity,
|
|
33
|
+
* or generated for this alias. Absent on records written before this field.
|
|
34
|
+
*/
|
|
35
|
+
identityFrom?: IdentitySource;
|
|
18
36
|
}
|
|
37
|
+
export type IdentitySource = 'claude-code' | 'generated';
|
|
19
38
|
export declare function listAccountAliases(): Promise<string[]>;
|
|
20
39
|
export declare function loadAccount(alias: string): Promise<AccountCredentials | null>;
|
|
21
40
|
export declare function loadAllAccounts(): Promise<AccountCredentials[]>;
|
|
@@ -37,8 +56,76 @@ export declare function detectClaudeIdentity(): Promise<{
|
|
|
37
56
|
deviceId: string;
|
|
38
57
|
accountUuid: string;
|
|
39
58
|
} | null>;
|
|
59
|
+
/**
|
|
60
|
+
* The OAuth profile behind an access token. This is what an Anthropic
|
|
61
|
+
* subscription token can say about itself, and dario never asked (#1263):
|
|
62
|
+
* the account uuid, its email, the organization uuid and tier fields. Probed
|
|
63
|
+
* 2026-09-08 with a live token — `account.uuid`, `account.email`,
|
|
64
|
+
* `organization.uuid`, `organization.rate_limit_tier`, `organization.seat_tier`
|
|
65
|
+
* are all present. Read-only, one GET, and it is the one fact that settles
|
|
66
|
+
* "are these two aliases the same subscription" without guessing.
|
|
67
|
+
*/
|
|
68
|
+
export declare const OAUTH_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
69
|
+
export interface OAuthProfile {
|
|
70
|
+
accountId: string;
|
|
71
|
+
accountEmail?: string;
|
|
72
|
+
organizationId?: string;
|
|
73
|
+
organizationType?: string;
|
|
74
|
+
rateLimitTier?: string;
|
|
75
|
+
seatTier?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Fetch the profile for `accessToken`. Null on ANY failure — a non-2xx, a
|
|
79
|
+
* timeout, a body without an account uuid — because identity is a nicety
|
|
80
|
+
* layered on a grant that already succeeded; a profile outage must never
|
|
81
|
+
* turn a working login into a failed one.
|
|
82
|
+
*/
|
|
83
|
+
export declare function fetchOAuthProfile(accessToken: string, fetchImpl?: typeof fetch): Promise<OAuthProfile | null>;
|
|
84
|
+
/** The record fields a profile fills in; nothing when there is no profile. */
|
|
85
|
+
export declare function profileFields(profile: OAuthProfile | null): Partial<AccountCredentials>;
|
|
86
|
+
/**
|
|
87
|
+
* Which client identity a NEW alias presents in `metadata.user_id`.
|
|
88
|
+
*
|
|
89
|
+
* Every add path used to copy the machine's Claude Code identity into every
|
|
90
|
+
* alias when one was installed. On a machine running a pool of colleagues'
|
|
91
|
+
* tokens that meant 18 different OAuth accounts all presenting ONE
|
|
92
|
+
* `device_id`/`account_uuid` (dario#1244, 2026-09-08) — and Anthropic ties what
|
|
93
|
+
* it sees to that identity (doctor-core.ts, identity drift: the bearer is
|
|
94
|
+
* cross-validated against it). The startup banner says as much: "requests
|
|
95
|
+
* may be billed as Extra Usage" without one.
|
|
96
|
+
*
|
|
97
|
+
* Rule: the local Claude Code identity belongs to the account it was granted
|
|
98
|
+
* to. A new alias takes it only when no other alias holds it, or when the
|
|
99
|
+
* holder is PROVEN to be the same account (same OAuth `accountId`). Otherwise
|
|
100
|
+
* the alias gets its own — the same fresh identity a machine without Claude
|
|
101
|
+
* Code has always produced, which the fleet box runs every seat on.
|
|
102
|
+
*/
|
|
103
|
+
export declare function chooseClientIdentity(existing: readonly AccountCredentials[], profile: OAuthProfile | null, local?: {
|
|
104
|
+
deviceId: string;
|
|
105
|
+
accountUuid: string;
|
|
106
|
+
} | null): Promise<{
|
|
107
|
+
deviceId: string;
|
|
108
|
+
accountUuid: string;
|
|
109
|
+
identityFrom: IdentitySource;
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* Give each named alias its own client identity (`dario accounts identity
|
|
113
|
+
* --fresh`). Tokens, organization and account identity are untouched — only
|
|
114
|
+
* `deviceId`/`accountUuid` change, and the running proxy presents the new pair on
|
|
115
|
+
* the seat's next request (pool.add takes a changed on-disk identity).
|
|
116
|
+
* Returns the aliases rewritten; an unknown alias is skipped, not an error.
|
|
117
|
+
*/
|
|
118
|
+
export declare function regenerateClientIdentity(aliases: readonly string[]): Promise<string[]>;
|
|
40
119
|
/** Refresh an account's OAuth token using dario's auto-detected CC OAuth config. */
|
|
41
120
|
export declare function refreshAccountToken(creds: AccountCredentials): Promise<AccountCredentials>;
|
|
121
|
+
/**
|
|
122
|
+
* Fill in `accountId` (and the profile's other fields) on a record that predates
|
|
123
|
+
* them, from one profile GET; saves and returns the record, unchanged when
|
|
124
|
+
* nothing could be learned. Kept OUT of the token refresh itself so the
|
|
125
|
+
* refresh stays a single, sequenced token exchange — a caller with a mocked
|
|
126
|
+
* or injected fetch sees exactly the calls it always did (dario#1263).
|
|
127
|
+
*/
|
|
128
|
+
export declare function backfillIdentity(creds: AccountCredentials, fetchImpl?: typeof fetch): Promise<AccountCredentials>;
|
|
42
129
|
/** Test-only — inspect the in-flight map. Production code has no business peeking. */
|
|
43
130
|
export declare function _accountRefreshesInFlightSizeForTest(): number;
|
|
44
131
|
/**
|
package/dist/accounts.js
CHANGED
|
@@ -135,6 +135,125 @@ export async function detectClaudeIdentity() {
|
|
|
135
135
|
}
|
|
136
136
|
return null;
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The OAuth profile behind an access token. This is what an Anthropic
|
|
140
|
+
* subscription token can say about itself, and dario never asked (#1263):
|
|
141
|
+
* the account uuid, its email, the organization uuid and tier fields. Probed
|
|
142
|
+
* 2026-09-08 with a live token — `account.uuid`, `account.email`,
|
|
143
|
+
* `organization.uuid`, `organization.rate_limit_tier`, `organization.seat_tier`
|
|
144
|
+
* are all present. Read-only, one GET, and it is the one fact that settles
|
|
145
|
+
* "are these two aliases the same subscription" without guessing.
|
|
146
|
+
*/
|
|
147
|
+
export const OAUTH_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
|
|
148
|
+
/**
|
|
149
|
+
* Fetch the profile for `accessToken`. Null on ANY failure — a non-2xx, a
|
|
150
|
+
* timeout, a body without an account uuid — because identity is a nicety
|
|
151
|
+
* layered on a grant that already succeeded; a profile outage must never
|
|
152
|
+
* turn a working login into a failed one.
|
|
153
|
+
*/
|
|
154
|
+
export async function fetchOAuthProfile(accessToken, fetchImpl = fetch) {
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetchImpl(OAUTH_PROFILE_URL, {
|
|
157
|
+
method: 'GET',
|
|
158
|
+
headers: {
|
|
159
|
+
Authorization: `Bearer ${accessToken}`,
|
|
160
|
+
Accept: 'application/json',
|
|
161
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
162
|
+
},
|
|
163
|
+
signal: AbortSignal.timeout(8_000),
|
|
164
|
+
});
|
|
165
|
+
if (!res.ok)
|
|
166
|
+
return null;
|
|
167
|
+
const data = await res.json();
|
|
168
|
+
const uuid = data.account?.uuid;
|
|
169
|
+
if (typeof uuid !== 'string' || uuid.length === 0)
|
|
170
|
+
return null;
|
|
171
|
+
const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
|
|
172
|
+
const profile = { accountId: uuid };
|
|
173
|
+
const email = str(data.account?.email);
|
|
174
|
+
if (email)
|
|
175
|
+
profile.accountEmail = email;
|
|
176
|
+
const org = str(data.organization?.uuid);
|
|
177
|
+
if (org)
|
|
178
|
+
profile.organizationId = org;
|
|
179
|
+
const orgType = str(data.organization?.organization_type);
|
|
180
|
+
if (orgType)
|
|
181
|
+
profile.organizationType = orgType;
|
|
182
|
+
const tier = str(data.organization?.rate_limit_tier);
|
|
183
|
+
if (tier)
|
|
184
|
+
profile.rateLimitTier = tier;
|
|
185
|
+
const seat = str(data.organization?.seat_tier);
|
|
186
|
+
if (seat)
|
|
187
|
+
profile.seatTier = seat;
|
|
188
|
+
return profile;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** The record fields a profile fills in; nothing when there is no profile. */
|
|
195
|
+
export function profileFields(profile) {
|
|
196
|
+
if (!profile)
|
|
197
|
+
return {};
|
|
198
|
+
const out = { accountId: profile.accountId };
|
|
199
|
+
if (profile.accountEmail)
|
|
200
|
+
out.accountEmail = profile.accountEmail;
|
|
201
|
+
if (profile.organizationId)
|
|
202
|
+
out.organizationId = profile.organizationId;
|
|
203
|
+
if (profile.organizationType)
|
|
204
|
+
out.organizationType = profile.organizationType;
|
|
205
|
+
if (profile.rateLimitTier)
|
|
206
|
+
out.rateLimitTier = profile.rateLimitTier;
|
|
207
|
+
if (profile.seatTier)
|
|
208
|
+
out.seatTier = profile.seatTier;
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Which client identity a NEW alias presents in `metadata.user_id`.
|
|
213
|
+
*
|
|
214
|
+
* Every add path used to copy the machine's Claude Code identity into every
|
|
215
|
+
* alias when one was installed. On a machine running a pool of colleagues'
|
|
216
|
+
* tokens that meant 18 different OAuth accounts all presenting ONE
|
|
217
|
+
* `device_id`/`account_uuid` (dario#1244, 2026-09-08) — and Anthropic ties what
|
|
218
|
+
* it sees to that identity (doctor-core.ts, identity drift: the bearer is
|
|
219
|
+
* cross-validated against it). The startup banner says as much: "requests
|
|
220
|
+
* may be billed as Extra Usage" without one.
|
|
221
|
+
*
|
|
222
|
+
* Rule: the local Claude Code identity belongs to the account it was granted
|
|
223
|
+
* to. A new alias takes it only when no other alias holds it, or when the
|
|
224
|
+
* holder is PROVEN to be the same account (same OAuth `accountId`). Otherwise
|
|
225
|
+
* the alias gets its own — the same fresh identity a machine without Claude
|
|
226
|
+
* Code has always produced, which the fleet box runs every seat on.
|
|
227
|
+
*/
|
|
228
|
+
export async function chooseClientIdentity(existing, profile, local) {
|
|
229
|
+
const cc = local === undefined ? await detectClaudeIdentity() : local;
|
|
230
|
+
if (cc && (cc.deviceId || cc.accountUuid)) {
|
|
231
|
+
const holder = existing.find((a) => a.deviceId === cc.deviceId && a.accountUuid === cc.accountUuid);
|
|
232
|
+
const sameAccount = holder !== undefined && profile !== null
|
|
233
|
+
&& typeof holder.accountId === 'string' && holder.accountId === profile.accountId;
|
|
234
|
+
if (!holder || sameAccount)
|
|
235
|
+
return { deviceId: cc.deviceId, accountUuid: cc.accountUuid, identityFrom: 'claude-code' };
|
|
236
|
+
}
|
|
237
|
+
return { deviceId: randomUUID(), accountUuid: randomUUID(), identityFrom: 'generated' };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Give each named alias its own client identity (`dario accounts identity
|
|
241
|
+
* --fresh`). Tokens, organization and account identity are untouched — only
|
|
242
|
+
* `deviceId`/`accountUuid` change, and the running proxy presents the new pair on
|
|
243
|
+
* the seat's next request (pool.add takes a changed on-disk identity).
|
|
244
|
+
* Returns the aliases rewritten; an unknown alias is skipped, not an error.
|
|
245
|
+
*/
|
|
246
|
+
export async function regenerateClientIdentity(aliases) {
|
|
247
|
+
const done = [];
|
|
248
|
+
for (const alias of aliases) {
|
|
249
|
+
const acc = await loadAccount(alias);
|
|
250
|
+
if (!acc)
|
|
251
|
+
continue;
|
|
252
|
+
await saveAccount({ ...acc, deviceId: randomUUID(), accountUuid: randomUUID(), identityFrom: 'generated' });
|
|
253
|
+
done.push(alias);
|
|
254
|
+
}
|
|
255
|
+
return done;
|
|
256
|
+
}
|
|
138
257
|
// Per-alias single-flight map: if a refresh is in flight for an alias,
|
|
139
258
|
// concurrent callers share the same promise instead of issuing parallel
|
|
140
259
|
// refresh_token requests. The pool's 15-min background timer is the only
|
|
@@ -290,6 +409,23 @@ async function doRefreshAccountToken(creds) {
|
|
|
290
409
|
await saveAccount(updated);
|
|
291
410
|
return updated;
|
|
292
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Fill in `accountId` (and the profile's other fields) on a record that predates
|
|
414
|
+
* them, from one profile GET; saves and returns the record, unchanged when
|
|
415
|
+
* nothing could be learned. Kept OUT of the token refresh itself so the
|
|
416
|
+
* refresh stays a single, sequenced token exchange — a caller with a mocked
|
|
417
|
+
* or injected fetch sees exactly the calls it always did (dario#1263).
|
|
418
|
+
*/
|
|
419
|
+
export async function backfillIdentity(creds, fetchImpl = fetch) {
|
|
420
|
+
if (creds.accountId)
|
|
421
|
+
return creds;
|
|
422
|
+
const profile = await fetchOAuthProfile(creds.accessToken, fetchImpl);
|
|
423
|
+
if (!profile)
|
|
424
|
+
return creds;
|
|
425
|
+
const updated = { ...creds, ...profileFields(profile) };
|
|
426
|
+
await saveAccount(updated);
|
|
427
|
+
return updated;
|
|
428
|
+
}
|
|
293
429
|
/** Test-only — inspect the in-flight map. Production code has no business peeking. */
|
|
294
430
|
export function _accountRefreshesInFlightSizeForTest() {
|
|
295
431
|
return accountRefreshesInFlight.size;
|
|
@@ -373,11 +509,11 @@ export async function addAccountViaOAuth(alias) {
|
|
|
373
509
|
throw new Error(`Token exchange failed (${tokenRes.status}): ${redactSecrets(body.slice(0, 200))}`);
|
|
374
510
|
}
|
|
375
511
|
const tokens = await tokenRes.json();
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
512
|
+
// Who this token is (one GET, never fatal), then which client identity
|
|
513
|
+
// the alias presents — see chooseClientIdentity.
|
|
514
|
+
const profile = await fetchOAuthProfile(tokens.access_token);
|
|
515
|
+
const others = (await loadAllAccounts()).filter((a) => a.alias !== alias);
|
|
516
|
+
const identity = await chooseClientIdentity(others, profile);
|
|
381
517
|
const creds = {
|
|
382
518
|
alias,
|
|
383
519
|
accessToken: tokens.access_token,
|
|
@@ -386,7 +522,9 @@ export async function addAccountViaOAuth(alias) {
|
|
|
386
522
|
scopes: tokens.scope?.split(' ') ?? cfg.scopes.split(' '),
|
|
387
523
|
deviceId: identity.deviceId,
|
|
388
524
|
accountUuid: identity.accountUuid,
|
|
525
|
+
identityFrom: identity.identityFrom,
|
|
389
526
|
grantedAt: Date.now(),
|
|
527
|
+
...profileFields(profile),
|
|
390
528
|
};
|
|
391
529
|
await saveAccount(creds);
|
|
392
530
|
resolve(creds);
|
|
@@ -510,10 +648,9 @@ export async function completeAddAccount(alias, code, codeVerifier, state) {
|
|
|
510
648
|
throw new Error(`Token exchange failed (${tokenRes.status}): ${redactSecrets(body.slice(0, 200))}`);
|
|
511
649
|
}
|
|
512
650
|
const tokens = await tokenRes.json();
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
};
|
|
651
|
+
const profile = await fetchOAuthProfile(tokens.access_token);
|
|
652
|
+
const others = (await loadAllAccounts()).filter((a) => a.alias !== alias);
|
|
653
|
+
const identity = await chooseClientIdentity(others, profile);
|
|
517
654
|
const creds = {
|
|
518
655
|
alias,
|
|
519
656
|
accessToken: tokens.access_token,
|
|
@@ -522,7 +659,9 @@ export async function completeAddAccount(alias, code, codeVerifier, state) {
|
|
|
522
659
|
scopes: tokens.scope?.split(' ') ?? cfg.scopes.split(' '),
|
|
523
660
|
deviceId: identity.deviceId,
|
|
524
661
|
accountUuid: identity.accountUuid,
|
|
662
|
+
identityFrom: identity.identityFrom,
|
|
525
663
|
grantedAt: Date.now(),
|
|
664
|
+
...profileFields(profile),
|
|
526
665
|
};
|
|
527
666
|
await saveAccount(creds);
|
|
528
667
|
return creds;
|
|
@@ -583,12 +722,10 @@ export async function addAccountFromKeychain(alias, target) {
|
|
|
583
722
|
if (!oauth?.accessToken || !oauth?.refreshToken) {
|
|
584
723
|
throw new KeychainImportError(`Keychain entry "${chosen.target}" is missing accessToken/refreshToken — re-authenticate Claude Code.`, 'empty');
|
|
585
724
|
}
|
|
586
|
-
// Same
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
accountUuid: randomUUID(),
|
|
591
|
-
};
|
|
725
|
+
// Same profile-and-policy as addAccountViaOAuth — see chooseClientIdentity.
|
|
726
|
+
const profile = await fetchOAuthProfile(oauth.accessToken);
|
|
727
|
+
const others = (await loadAllAccounts()).filter((a) => a.alias !== alias);
|
|
728
|
+
const identity = await chooseClientIdentity(others, profile);
|
|
592
729
|
const creds = {
|
|
593
730
|
alias,
|
|
594
731
|
accessToken: oauth.accessToken,
|
|
@@ -597,7 +734,9 @@ export async function addAccountFromKeychain(alias, target) {
|
|
|
597
734
|
scopes: oauth.scopes ?? ['user:inference'],
|
|
598
735
|
deviceId: identity.deviceId,
|
|
599
736
|
accountUuid: identity.accountUuid,
|
|
737
|
+
identityFrom: identity.identityFrom,
|
|
600
738
|
grantedAt: oauth.grantedAt,
|
|
739
|
+
...profileFields(profile),
|
|
601
740
|
};
|
|
602
741
|
await saveAccount(creds);
|
|
603
742
|
return creds;
|
|
@@ -645,10 +784,12 @@ export async function ensureLoginCredentialsInPool(alias = MIGRATED_LOGIN_ALIAS)
|
|
|
645
784
|
const tok = creds?.claudeAiOauth;
|
|
646
785
|
if (!tok?.accessToken || !tok?.refreshToken)
|
|
647
786
|
return null;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
787
|
+
// The login seat IS the machine's Claude Code account, so its identity is
|
|
788
|
+
// the local one when there is one — the policy in chooseClientIdentity
|
|
789
|
+
// exists for the OTHER aliases.
|
|
790
|
+
const cc = await detectClaudeIdentity();
|
|
791
|
+
const identity = cc ?? { deviceId: randomUUID(), accountUuid: randomUUID() };
|
|
792
|
+
const profile = await fetchOAuthProfile(tok.accessToken);
|
|
652
793
|
await saveAccount({
|
|
653
794
|
alias,
|
|
654
795
|
accessToken: tok.accessToken,
|
|
@@ -657,7 +798,9 @@ export async function ensureLoginCredentialsInPool(alias = MIGRATED_LOGIN_ALIAS)
|
|
|
657
798
|
scopes: tok.scopes ?? [],
|
|
658
799
|
deviceId: identity.deviceId,
|
|
659
800
|
accountUuid: identity.accountUuid,
|
|
801
|
+
identityFrom: cc ? 'claude-code' : 'generated',
|
|
660
802
|
grantedAt: tok.grantedAt,
|
|
803
|
+
...profileFields(profile),
|
|
661
804
|
});
|
|
662
805
|
return alias;
|
|
663
806
|
}
|
package/dist/admin-api.d.ts
CHANGED
|
@@ -115,8 +115,21 @@ export interface AdminAccountLive {
|
|
|
115
115
|
lastRejectedAt: number | null;
|
|
116
116
|
/** Organization observed on this seat's responses, or `null` if none yet (dario#1244). */
|
|
117
117
|
organizationId: string | null;
|
|
118
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Other aliases that are the SAME Anthropic account (same OAuth account
|
|
120
|
+
* uuid) — one subscription under several aliases (dario#1244). Kept under
|
|
121
|
+
* its original name for readers of the older payload; `sameAccountAs` is the
|
|
122
|
+
* same list. Until 6.0.38 this was inferred from a shared reset second,
|
|
123
|
+
* which Anthropic's 20-minute reset grid makes meaningless (dario#1263).
|
|
124
|
+
*/
|
|
119
125
|
sharesWindowWith: string[];
|
|
126
|
+
sameAccountAs?: string[];
|
|
127
|
+
/** OAuth account uuid behind the seat's token, or null until identified. */
|
|
128
|
+
accountId?: string | null;
|
|
129
|
+
/** Masked email on that account, or null. */
|
|
130
|
+
accountEmail?: string | null;
|
|
131
|
+
rateLimitTier?: string | null;
|
|
132
|
+
seatTier?: string | null;
|
|
120
133
|
/** Peer instance whose reading this seat currently carries (shared pool state), or `null` for this instance's own. */
|
|
121
134
|
readingFrom: string | null;
|
|
122
135
|
/**
|
package/dist/admin-api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { maskEmail } from './pool.js';
|
|
1
2
|
import { timingSafeEqual } from 'node:crypto';
|
|
2
3
|
import { startAddAccount, completeAddAccount, removeAccount, listAccountAliases, loadAccount, } from './accounts.js';
|
|
3
4
|
import { parseManualPaste } from './oauth.js';
|
|
@@ -362,6 +363,10 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
362
363
|
// From the record (written with the seat's last token refresh), so
|
|
363
364
|
// it is known without a live pool entry.
|
|
364
365
|
organization_id: r.organizationId ?? null,
|
|
366
|
+
// Identity from the record (grant-time OAuth profile); the live
|
|
367
|
+
// block below repeats it when the seat is in the running pool.
|
|
368
|
+
account_id: r.accountId ?? null,
|
|
369
|
+
account_email: maskEmail(r.accountEmail),
|
|
365
370
|
// Inline the running pool's live status when this account is in it.
|
|
366
371
|
...(l ? {
|
|
367
372
|
util5h: l.util5h,
|
|
@@ -375,6 +380,11 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
375
380
|
reset_in_ms: l.resetInMs ?? null,
|
|
376
381
|
...(l.organizationId ? { organization_id: l.organizationId } : {}),
|
|
377
382
|
shares_window_with: l.sharesWindowWith ?? [],
|
|
383
|
+
same_account_as: l.sameAccountAs ?? l.sharesWindowWith ?? [],
|
|
384
|
+
...(l.accountId ? { account_id: l.accountId } : {}),
|
|
385
|
+
...(l.accountEmail ? { account_email: l.accountEmail } : {}),
|
|
386
|
+
...(l.rateLimitTier ? { rate_limit_tier: l.rateLimitTier } : {}),
|
|
387
|
+
...(l.seatTier ? { seat_tier: l.seatTier } : {}),
|
|
378
388
|
reading_from: l.readingFrom ?? null,
|
|
379
389
|
claim: l.claim,
|
|
380
390
|
status: l.status,
|
|
@@ -221,7 +221,14 @@ export type ResponsesToolChoice = 'auto' | 'none' | 'required' | {
|
|
|
221
221
|
name: string;
|
|
222
222
|
};
|
|
223
223
|
export interface ResponsesReasoningConfig {
|
|
224
|
-
|
|
224
|
+
/**
|
|
225
|
+
* The backend accepts more levels than the thinking-budget mapping can
|
|
226
|
+
* produce: probed 2026-08-29 against a live account, `none, minimal, low,
|
|
227
|
+
* medium, high, xhigh, max` are all valid and `ultra` 400s. The wider set
|
|
228
|
+
* is reachable only when a caller names an effort outright (dario#1260);
|
|
229
|
+
* `thinkingToReasoningEffort` still only ever yields low/medium/high.
|
|
230
|
+
*/
|
|
231
|
+
effort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
225
232
|
summary?: 'auto' | 'concise' | 'detailed';
|
|
226
233
|
}
|
|
227
234
|
export interface ResponsesRequest {
|
|
@@ -338,9 +345,13 @@ export interface ResponsesResponse {
|
|
|
338
345
|
* (max_output_tokens caps reasoning + output combined on the Responses API).
|
|
339
346
|
*/
|
|
340
347
|
export declare const REASONING_HEADROOM: {
|
|
348
|
+
readonly none: 0;
|
|
349
|
+
readonly minimal: 6000;
|
|
341
350
|
readonly low: 12000;
|
|
342
351
|
readonly medium: 25000;
|
|
343
352
|
readonly high: 50000;
|
|
353
|
+
readonly xhigh: 80000;
|
|
354
|
+
readonly max: 120000;
|
|
344
355
|
};
|
|
345
356
|
/** gpt-5.x / o-series output ceiling (tokens). */
|
|
346
357
|
export declare const RESPONSES_MAX_OUTPUT_CAP = 128000;
|
|
@@ -352,6 +363,14 @@ export interface AnthropicToResponsesOptions {
|
|
|
352
363
|
* Pass `null` to omit `summary` entirely.
|
|
353
364
|
*/
|
|
354
365
|
reasoningSummary?: 'auto' | 'concise' | 'detailed' | null;
|
|
366
|
+
/**
|
|
367
|
+
* Force `reasoning.effort`, overriding whatever `thinking.budget_tokens`
|
|
368
|
+
* would have implied. Set from a model-name effort suffix on the Codex
|
|
369
|
+
* route (dario#1260, `gpt-5.6-terra:high`), which is the only way an
|
|
370
|
+
* Anthropic-shape caller can choose an effort the budget thresholds cannot
|
|
371
|
+
* express. Unset means the existing thinking-derived behaviour, unchanged.
|
|
372
|
+
*/
|
|
373
|
+
effort?: ResponsesReasoningConfig['effort'];
|
|
355
374
|
/**
|
|
356
375
|
* `store`. Default false — dario is stateless and keeps no server-side
|
|
357
376
|
* conversation. Set true only if a caller wants OpenAI-side retention.
|
|
@@ -312,7 +312,17 @@ function translateToolChoice(choice) {
|
|
|
312
312
|
* client's intended visible-output budget survives on a reasoning model
|
|
313
313
|
* (max_output_tokens caps reasoning + output combined on the Responses API).
|
|
314
314
|
*/
|
|
315
|
-
export const REASONING_HEADROOM = {
|
|
315
|
+
export const REASONING_HEADROOM = {
|
|
316
|
+
none: 0, minimal: 6000, low: 12000, medium: 25000, high: 50000,
|
|
317
|
+
// dario#1260 widened the reachable levels beyond what the thinking-budget
|
|
318
|
+
// mapping produces. xhigh and max reason MUCH harder — measured through
|
|
319
|
+
// dario on one identical prompt, same model, only the level changing:
|
|
320
|
+
// low 1,982 output tokens, high 3,947, max 11,160. Reserve above the trend
|
|
321
|
+
// rather than on it: under-reserving is the failure this table exists to
|
|
322
|
+
// prevent (reasoning eats the whole budget, status incomplete, empty turn),
|
|
323
|
+
// and both values are clamped by RESPONSES_MAX_OUTPUT_CAP anyway.
|
|
324
|
+
xhigh: 80000, max: 120000,
|
|
325
|
+
};
|
|
316
326
|
/** gpt-5.x / o-series output ceiling (tokens). */
|
|
317
327
|
export const RESPONSES_MAX_OUTPUT_CAP = 128000;
|
|
318
328
|
/**
|
|
@@ -376,7 +386,10 @@ export function anthropicToResponsesRequest(body, targetModel, options = {}) {
|
|
|
376
386
|
if (body.tool_choice?.disable_parallel_tool_use === true && out.tools) {
|
|
377
387
|
out.parallel_tool_calls = false;
|
|
378
388
|
}
|
|
379
|
-
|
|
389
|
+
// An explicitly named effort wins over the thinking-budget mapping: the
|
|
390
|
+
// caller asked for a level, not a budget, and the thresholds cannot express
|
|
391
|
+
// xhigh or max at all.
|
|
392
|
+
const effort = options.effort ?? thinkingToReasoningEffort(body.thinking);
|
|
380
393
|
if (effort) {
|
|
381
394
|
out.reasoning = { effort };
|
|
382
395
|
const summary = options.reasoningSummary === undefined ? 'auto' : options.reasoningSummary;
|
package/dist/cli.d.ts
CHANGED
|
@@ -130,6 +130,9 @@ export interface LiveSeat {
|
|
|
130
130
|
organizationId?: string | null;
|
|
131
131
|
sharesWindowWith?: string[];
|
|
132
132
|
grantedAt?: number | null;
|
|
133
|
+
sameAccountAs?: string[];
|
|
134
|
+
accountId?: string | null;
|
|
135
|
+
accountEmail?: string | null;
|
|
133
136
|
}
|
|
134
137
|
export interface LivePayload {
|
|
135
138
|
mode?: string;
|
|
@@ -165,3 +168,11 @@ export declare function formatLiveAccountsListing(payload: LivePayload, port: nu
|
|
|
165
168
|
* guard from #137 (v3.31.15).
|
|
166
169
|
*/
|
|
167
170
|
export declare function isMainEntry(argv1: string | undefined | null, moduleHref: string, realpath?: (p: string) => string): boolean;
|
|
171
|
+
/**
|
|
172
|
+
* `dario accounts identity` — which client identity each seat presents in
|
|
173
|
+
* `metadata.user_id`, where it came from, and which seats share one across
|
|
174
|
+
* DIFFERENT accounts (dario#1244, 2026-09-08). `--fresh <alias>...` / `--all`
|
|
175
|
+
* rewrites the named seats' identity; the running proxy presents the new one
|
|
176
|
+
* on each seat's next request, no restart.
|
|
177
|
+
*/
|
|
178
|
+
export declare function runAccountsIdentity(args: string[]): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
// just want `parsePositiveIntEnv`) doesn't trigger a Bun relaunch or any
|
|
18
18
|
// other startup side effect.
|
|
19
19
|
import { unlink } from 'node:fs/promises';
|
|
20
|
+
import { loadAllAccounts as loadAllAccountsForIdentity, regenerateClientIdentity } from './accounts.js';
|
|
21
|
+
import { maskEmail } from './pool.js';
|
|
20
22
|
import { realpathSync, readFileSync } from 'node:fs';
|
|
21
23
|
import { join } from 'node:path';
|
|
22
24
|
import { homedir } from 'node:os';
|
|
@@ -915,7 +917,7 @@ export function formatLiveAccountsListing(payload, port, now) {
|
|
|
915
917
|
lines.push(' ────────────────');
|
|
916
918
|
lines.push('');
|
|
917
919
|
const windows = typeof payload.distinctWindows === 'number' ? payload.distinctWindows : seats.length;
|
|
918
|
-
lines.push(` Pool of ${seats.length} (${seats.length === 1 ? '1 seat' : seats.length + ' seats'} on ${windows} distinct
|
|
920
|
+
lines.push(` Pool of ${seats.length} (${seats.length === 1 ? '1 seat' : seats.length + ' seats'} on ${windows} distinct account${windows === 1 ? '' : 's'})`);
|
|
919
921
|
lines.push('');
|
|
920
922
|
for (const s of seats) {
|
|
921
923
|
const alias = typeof s.alias === 'string' ? s.alias : '(unnamed)';
|
|
@@ -932,7 +934,11 @@ export function formatLiveAccountsListing(payload, port, now) {
|
|
|
932
934
|
`429s ${num(s.rejectedCount)}`,
|
|
933
935
|
...(next ? [next] : []),
|
|
934
936
|
typeof s.organizationId === 'string' && s.organizationId ? `org ${s.organizationId.slice(0, 8)}…` : 'org not yet observed',
|
|
935
|
-
...(
|
|
937
|
+
...(typeof s.accountEmail === 'string' && s.accountEmail ? [`account ${s.accountEmail}`] : []),
|
|
938
|
+
...((() => {
|
|
939
|
+
const same = Array.isArray(s.sameAccountAs) ? s.sameAccountAs : Array.isArray(s.sharesWindowWith) ? s.sharesWindowWith : [];
|
|
940
|
+
return same.length > 0 ? [`same account as ${same.join(', ')}`] : [];
|
|
941
|
+
})()),
|
|
936
942
|
];
|
|
937
943
|
lines.push(` ${''.padEnd(20)} ${facts.join(' · ')}`);
|
|
938
944
|
lines.push(` ${''.padEnd(20)} ${describeGrantAge(grantAge(typeof s.grantedAt === 'number' ? s.grantedAt : undefined, now))}`);
|
|
@@ -1232,8 +1238,12 @@ async function accounts() {
|
|
|
1232
1238
|
}
|
|
1233
1239
|
return;
|
|
1234
1240
|
}
|
|
1241
|
+
if (sub === 'identity') {
|
|
1242
|
+
await runAccountsIdentity(args);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1235
1245
|
console.error(`[dario] Unknown accounts subcommand: ${sub}`);
|
|
1236
|
-
console.error('Usage: dario accounts [list|add <alias>|check <alias>|remove <alias
|
|
1246
|
+
console.error('Usage: dario accounts [list|add <alias>|check <alias>|remove <alias>|identity [--fresh <alias>...|--all]]');
|
|
1237
1247
|
process.exit(1);
|
|
1238
1248
|
}
|
|
1239
1249
|
/**
|
|
@@ -2571,3 +2581,70 @@ if (isDirectEntry) {
|
|
|
2571
2581
|
process.exit(1);
|
|
2572
2582
|
});
|
|
2573
2583
|
}
|
|
2584
|
+
/**
|
|
2585
|
+
* `dario accounts identity` — which client identity each seat presents in
|
|
2586
|
+
* `metadata.user_id`, where it came from, and which seats share one across
|
|
2587
|
+
* DIFFERENT accounts (dario#1244, 2026-09-08). `--fresh <alias>...` / `--all`
|
|
2588
|
+
* rewrites the named seats' identity; the running proxy presents the new one
|
|
2589
|
+
* on each seat's next request, no restart.
|
|
2590
|
+
*/
|
|
2591
|
+
export async function runAccountsIdentity(args) {
|
|
2592
|
+
const fresh = args.includes('--fresh');
|
|
2593
|
+
const all = args.includes('--all');
|
|
2594
|
+
const named = args.slice(2).filter((a) => !a.startsWith('--'));
|
|
2595
|
+
const accounts = await loadAllAccountsForIdentity();
|
|
2596
|
+
if (fresh) {
|
|
2597
|
+
const targets = all ? accounts.map((a) => a.alias) : named;
|
|
2598
|
+
if (targets.length === 0) {
|
|
2599
|
+
console.error('');
|
|
2600
|
+
console.error(' Usage: dario accounts identity --fresh <alias> [<alias>...] or --fresh --all');
|
|
2601
|
+
console.error('');
|
|
2602
|
+
process.exit(1);
|
|
2603
|
+
}
|
|
2604
|
+
const done = await regenerateClientIdentity(targets);
|
|
2605
|
+
for (const alias of done)
|
|
2606
|
+
console.log(`[dario] "${alias}": fresh client identity written — the running proxy presents it on the seat's next request.`);
|
|
2607
|
+
const missing = targets.filter((t) => !done.includes(t));
|
|
2608
|
+
for (const alias of missing)
|
|
2609
|
+
console.error(`[dario] No account "${alias}" found.`);
|
|
2610
|
+
if (missing.length > 0)
|
|
2611
|
+
process.exit(1);
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
console.log('');
|
|
2615
|
+
console.log(' dario — Client identity per seat');
|
|
2616
|
+
console.log(' ────────────────');
|
|
2617
|
+
console.log('');
|
|
2618
|
+
const byIdentity = new Map();
|
|
2619
|
+
for (const a of accounts) {
|
|
2620
|
+
const k = `${a.deviceId}|${a.accountUuid}`;
|
|
2621
|
+
const list = byIdentity.get(k);
|
|
2622
|
+
if (list)
|
|
2623
|
+
list.push(a.alias);
|
|
2624
|
+
else
|
|
2625
|
+
byIdentity.set(k, [a.alias]);
|
|
2626
|
+
}
|
|
2627
|
+
for (const a of accounts) {
|
|
2628
|
+
const source = a.identityFrom ?? (a.deviceId ? 'unrecorded' : 'none');
|
|
2629
|
+
const who = a.accountEmail ? maskEmail(a.accountEmail) : a.accountId ? `${a.accountId.slice(0, 8)}…` : 'not yet identified';
|
|
2630
|
+
const shared = (byIdentity.get(`${a.deviceId}|${a.accountUuid}`) ?? []).filter((x) => x !== a.alias);
|
|
2631
|
+
const sharedNote = shared.length > 0 ? ` · same client identity as ${shared.join(', ')}` : '';
|
|
2632
|
+
console.log(` ${a.alias.padEnd(20)} identity ${source.padEnd(11)} device ${(a.deviceId || '(empty)').slice(0, 8)}… account ${who}${sharedNote}`);
|
|
2633
|
+
}
|
|
2634
|
+
const spanning = [...byIdentity.values()].filter((aliases) => {
|
|
2635
|
+
if (aliases.length < 2)
|
|
2636
|
+
return false;
|
|
2637
|
+
const ids = new Set(aliases.map((al) => accounts.find((a) => a.alias === al)?.accountId));
|
|
2638
|
+
return ids.size > 1 || ids.has(undefined);
|
|
2639
|
+
});
|
|
2640
|
+
console.log('');
|
|
2641
|
+
if (spanning.length > 0) {
|
|
2642
|
+
console.log(` ⚠ ${spanning.length} client identit${spanning.length === 1 ? 'y is' : 'ies are'} shared across different (or unidentified) accounts.`);
|
|
2643
|
+
console.log(' Anthropic ties usage and limits to the identity it sees. Give each seat its own:');
|
|
2644
|
+
console.log(` dario accounts identity --fresh ${spanning.flat().join(' ')}`);
|
|
2645
|
+
}
|
|
2646
|
+
else {
|
|
2647
|
+
console.log(' OK every seat presents its own client identity, or shares one only with the same account.');
|
|
2648
|
+
}
|
|
2649
|
+
console.log('');
|
|
2650
|
+
}
|
package/dist/codex-backend.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import type { CodexAccountCredentials } from './codex-accounts.js';
|
|
3
|
-
import { type ResponsesUsage } from './anthropic-responses-translate.js';
|
|
3
|
+
import { type ResponsesReasoningConfig, type ResponsesUsage } from './anthropic-responses-translate.js';
|
|
4
4
|
import { type ModelResolver, type ClaudeTarget } from './claude-model.js';
|
|
5
|
+
import { type EffortValue } from './effort.js';
|
|
5
6
|
export declare const CODEX_BACKEND_BASE_URL: string;
|
|
6
7
|
/**
|
|
7
8
|
* Client version sent on the model-discovery call. The backend REQUIRES the
|
|
@@ -72,7 +73,13 @@ export declare function isCodexModel(model: string, slugs: readonly string[]): b
|
|
|
72
73
|
* own. A single-entry chain keeps the pre-6.0 meaning exactly, so configs
|
|
73
74
|
* written before this release behave identically.
|
|
74
75
|
*/
|
|
75
|
-
export
|
|
76
|
+
export interface CodexTarget {
|
|
77
|
+
/** The slug as the account lists it — what goes in the outbound body. */
|
|
78
|
+
model: string;
|
|
79
|
+
/** Effort the entry declared through a `:high`-style suffix, if any. */
|
|
80
|
+
effort?: EffortValue;
|
|
81
|
+
}
|
|
82
|
+
export declare function pickCodexFallback(models: readonly string[], slugs: readonly string[]): CodexTarget | null;
|
|
76
83
|
export declare function pickClaudeFallback(models: readonly string[], slugs: readonly string[], bases?: readonly string[], resolve?: ModelResolver): string | null;
|
|
77
84
|
/**
|
|
78
85
|
* {@link pickClaudeFallback} with the effort the winning entry asked for.
|
|
@@ -276,4 +283,10 @@ export declare function buildCodexHeaders(creds: CodexAccountCredentials): Recor
|
|
|
276
283
|
* testable without network (test/codex-backend.mjs), matching the pattern
|
|
277
284
|
* test/codex-oauth.mjs already uses.
|
|
278
285
|
*/
|
|
279
|
-
export declare function forwardToCodex(req: IncomingMessage, res: ServerResponse, body: Buffer, creds: CodexAccountCredentials, corsOrigin: string, securityHeaders: Record<string, string>, upstreamTimeoutMs: number, verbose: boolean, shape?: CodexRequestShape, fetchImpl?: typeof fetch, deferOnUnavailable?: boolean, onDone?: (outcome: CodexForwardOutcome) => void, onDecline?: (info: CodexDecline) => void
|
|
286
|
+
export declare function forwardToCodex(req: IncomingMessage, res: ServerResponse, body: Buffer, creds: CodexAccountCredentials, corsOrigin: string, securityHeaders: Record<string, string>, upstreamTimeoutMs: number, verbose: boolean, shape?: CodexRequestShape, fetchImpl?: typeof fetch, deferOnUnavailable?: boolean, onDone?: (outcome: CodexForwardOutcome) => void, onDecline?: (info: CodexDecline) => void,
|
|
287
|
+
/**
|
|
288
|
+
* Effort named by a model-name suffix (dario#1260). Anthropic-shape only:
|
|
289
|
+
* a chat/completions caller sets `reasoning_effort` itself and that already
|
|
290
|
+
* translates. Undefined leaves the request exactly as it was.
|
|
291
|
+
*/
|
|
292
|
+
effort?: ResponsesReasoningConfig['effort']): Promise<boolean>;
|