@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.
@@ -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
- // Prefer CC identity if installed; otherwise generate fresh IDs.
377
- const identity = (await detectClaudeIdentity()) ?? {
378
- deviceId: randomUUID(),
379
- accountUuid: randomUUID(),
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 identity = (await detectClaudeIdentity()) ?? {
514
- deviceId: randomUUID(),
515
- accountUuid: randomUUID(),
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 identity preference as addAccountViaOAuth — prefer CC identity if
587
- // installed; otherwise generate fresh IDs.
588
- const identity = (await detectClaudeIdentity()) ?? {
589
- deviceId: randomUUID(),
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
- const identity = (await detectClaudeIdentity()) ?? {
649
- deviceId: randomUUID(),
650
- accountUuid: randomUUID(),
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
  }
@@ -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
- /** Other aliases whose last reading names the same live window — one subscription under several aliases. */
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
- effort?: 'low' | 'medium' | 'high';
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 = { low: 12000, medium: 25000, high: 50000 };
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
- const effort = thinkingToReasoningEffort(body.thinking);
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 window${windows === 1 ? '' : 's'})`);
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
- ...(Array.isArray(s.sharesWindowWith) && s.sharesWindowWith.length > 0 ? [`shares its window with ${s.sharesWindowWith.join(', ')}`] : []),
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
+ }
@@ -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 declare function pickCodexFallback(models: readonly string[], slugs: readonly string[]): string | null;
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): Promise<boolean>;
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>;