@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/codex-backend.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { createHash } from 'node:crypto';
|
|
25
25
|
import { anthropicToResponsesRequest, anthropicUsageFromResponses, createResponsesSSEParser, formatResponsesAnthropicSSE, createAnthropicMessageAssembler, responsesStreamToAnthropicSSE, } from './anthropic-responses-translate.js';
|
|
26
26
|
import { resolveClaudeTarget } from './claude-model.js';
|
|
27
|
+
import { parseEffortSuffix } from './effort.js';
|
|
27
28
|
import { BAKED_BASE_MODELS } from './model-catalog.js';
|
|
28
29
|
import { parseRetryAfterMs } from './provider-cooldown.js';
|
|
29
30
|
export const CODEX_BACKEND_BASE_URL = process.env.DARIO_CODEX_BASE_URL || 'https://chatgpt.com/backend-api/codex';
|
|
@@ -121,20 +122,28 @@ export function isCodexModel(model, slugs) {
|
|
|
121
122
|
const m = model.toLowerCase();
|
|
122
123
|
return slugs.some(s => s.toLowerCase() === m);
|
|
123
124
|
}
|
|
124
|
-
/**
|
|
125
|
-
* A pool-fallback value may name a CHAIN — `gpt-5.6-sol,claude-sonnet-5` — and
|
|
126
|
-
* each provider takes the first entry it can actually serve. These two pickers
|
|
127
|
-
* are the whole selection rule, kept pure so it is testable without a socket.
|
|
128
|
-
*
|
|
129
|
-
* Reading the chain from both ends is what makes failover SYMMETRIC in v6.0.0:
|
|
130
|
-
* `pickCodexFallback` catches a drained Claude pool, `pickClaudeFallback`
|
|
131
|
-
* catches a ChatGPT subscription that is rate-limited or down. Neither
|
|
132
|
-
* subscription hitting its ceiling can take the whole deployment dark on its
|
|
133
|
-
* own. A single-entry chain keeps the pre-6.0 meaning exactly, so configs
|
|
134
|
-
* written before this release behave identically.
|
|
135
|
-
*/
|
|
136
125
|
export function pickCodexFallback(models, slugs) {
|
|
137
|
-
|
|
126
|
+
for (const m of models) {
|
|
127
|
+
// The name AS WRITTEN wins, and only a name that matches no slug at all is
|
|
128
|
+
// re-read as `model:effort` — the same two-pass rule resolveClaudeTarget
|
|
129
|
+
// uses for chain entries (dario#1161), so a slug that genuinely ends in an
|
|
130
|
+
// effort word keeps priority over the suffix reading.
|
|
131
|
+
//
|
|
132
|
+
// Without the second pass a codex chain entry carrying the very suffix the
|
|
133
|
+
// Claude half accepts (`gpt-5.6-terra:high`) matched nothing and was
|
|
134
|
+
// silently SKIPPED: no error, no log, just a failover the operator
|
|
135
|
+
// configured that never fired (dario#1260).
|
|
136
|
+
//
|
|
137
|
+
// Per entry rather than two passes over the whole list, because the chain is
|
|
138
|
+
// priority-ordered and a full as-written sweep would let a later entry
|
|
139
|
+
// overtake an earlier one purely for being spelled without a suffix.
|
|
140
|
+
if (isCodexModel(m, slugs))
|
|
141
|
+
return { model: m };
|
|
142
|
+
const eff = parseEffortSuffix(m);
|
|
143
|
+
if (eff.effort && isCodexModel(eff.model, slugs))
|
|
144
|
+
return { model: eff.model, effort: eff.effort };
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
138
147
|
}
|
|
139
148
|
export function pickClaudeFallback(models, slugs, bases = BAKED_BASE_MODELS, resolve) {
|
|
140
149
|
// "Not a codex slug" is NOT the same as "the Claude pool can serve it". A
|
|
@@ -746,7 +755,13 @@ export function buildCodexHeaders(creds) {
|
|
|
746
755
|
* testable without network (test/codex-backend.mjs), matching the pattern
|
|
747
756
|
* test/codex-oauth.mjs already uses.
|
|
748
757
|
*/
|
|
749
|
-
export async function forwardToCodex(req, res, body, creds, corsOrigin, securityHeaders, upstreamTimeoutMs, verbose, shape = 'openai', fetchImpl = fetch, deferOnUnavailable = false, onDone, onDecline
|
|
758
|
+
export async function forwardToCodex(req, res, body, creds, corsOrigin, securityHeaders, upstreamTimeoutMs, verbose, shape = 'openai', fetchImpl = fetch, deferOnUnavailable = false, onDone, onDecline,
|
|
759
|
+
/**
|
|
760
|
+
* Effort named by a model-name suffix (dario#1260). Anthropic-shape only:
|
|
761
|
+
* a chat/completions caller sets `reasoning_effort` itself and that already
|
|
762
|
+
* translates. Undefined leaves the request exactly as it was.
|
|
763
|
+
*/
|
|
764
|
+
effort) {
|
|
750
765
|
void req;
|
|
751
766
|
const isAnthropic = shape === 'anthropic';
|
|
752
767
|
// Reported exactly once, on every exit that answered the client. Without
|
|
@@ -789,7 +804,7 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
|
|
|
789
804
|
const model = String(parsed.model ?? '');
|
|
790
805
|
// stream is forced: the backend is always streamed and collapsed here.
|
|
791
806
|
const upstreamBody = isAnthropic
|
|
792
|
-
? { ...anthropicToResponsesRequest(parsed, model), stream: true }
|
|
807
|
+
? { ...anthropicToResponsesRequest(parsed, model, effort ? { effort } : {}), stream: true }
|
|
793
808
|
: chatCompletionsToResponses(parsed);
|
|
794
809
|
const scrubbed = toCodexSupportedBody({
|
|
795
810
|
...upstreamBody,
|
package/dist/doctor-core.d.ts
CHANGED
|
@@ -156,6 +156,40 @@ export interface OrganizationsInput {
|
|
|
156
156
|
* until at least two seats have been observed.
|
|
157
157
|
*/
|
|
158
158
|
export declare function checkOrganizations(input: OrganizationsInput): Check[];
|
|
159
|
+
export interface AccountIdentityInput {
|
|
160
|
+
accounts: Array<{
|
|
161
|
+
alias: string;
|
|
162
|
+
accountId?: string;
|
|
163
|
+
accountEmail?: string;
|
|
164
|
+
}>;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The Accounts doctor row (dario#1244, #1263): how many distinct Anthropic
|
|
168
|
+
* accounts the pool's seats are, from the OAuth account uuid each record
|
|
169
|
+
* carries — the fact, not an inference from reset seconds. Two seats with one
|
|
170
|
+
* uuid ARE one subscription under two aliases; a seat without one is not yet
|
|
171
|
+
* identified (its next token refresh fills it in) and is never guessed at.
|
|
172
|
+
*/
|
|
173
|
+
export declare function checkAccountIdentity(input: AccountIdentityInput): Check[];
|
|
174
|
+
export interface SharedClientIdentityInput {
|
|
175
|
+
accounts: Array<{
|
|
176
|
+
alias: string;
|
|
177
|
+
deviceId: string;
|
|
178
|
+
accountUuid: string;
|
|
179
|
+
accountId?: string;
|
|
180
|
+
identityFrom?: string;
|
|
181
|
+
}>;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The Client identity doctor row (dario#1244, 2026-09-08). Every `accounts add`
|
|
185
|
+
* used to copy the machine's Claude Code identity into the new alias, so a
|
|
186
|
+
* pool of colleagues' tokens on one machine presented ONE `device_id` /
|
|
187
|
+
* `account_uuid` in `metadata.user_id` across every OAuth account it held.
|
|
188
|
+
* Anthropic ties what it sees to that identity (see checkIdentityDrift).
|
|
189
|
+
* Seats that are genuinely the same account may share it; seats that are
|
|
190
|
+
* different accounts, or not yet identified, must not.
|
|
191
|
+
*/
|
|
192
|
+
export declare function checkSharedClientIdentity(input: SharedClientIdentityInput): Check[];
|
|
159
193
|
export declare function checkIdentityDrift(input: IdentityDriftInput): Check[];
|
|
160
194
|
export declare function probeNpmLatestCC(): string | null;
|
|
161
195
|
/**
|
package/dist/doctor-core.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* system surfaces as `fail` instead of crashing the CLI.
|
|
12
12
|
*/
|
|
13
13
|
import { grantAge, grantThresholds, worstGrantLevel, describeGrantAge } from './refresh-grant.js';
|
|
14
|
+
import { maskEmail } from './pool.js';
|
|
14
15
|
import { readFileSync } from 'node:fs';
|
|
15
16
|
import { join, dirname } from 'node:path';
|
|
16
17
|
import { fileURLToPath } from 'node:url';
|
|
@@ -221,7 +222,89 @@ export function checkOrganizations(input) {
|
|
|
221
222
|
return [{
|
|
222
223
|
status: 'info',
|
|
223
224
|
label: 'Organizations',
|
|
224
|
-
detail: `${head} — ${pairs}. Seats on one organization
|
|
225
|
+
detail: `${head} — ${pairs}. Seats on one organization are not one subscription: the Accounts row says which seats are the same account, from each token's OAuth profile.`,
|
|
226
|
+
}];
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The Accounts doctor row (dario#1244, #1263): how many distinct Anthropic
|
|
230
|
+
* accounts the pool's seats are, from the OAuth account uuid each record
|
|
231
|
+
* carries — the fact, not an inference from reset seconds. Two seats with one
|
|
232
|
+
* uuid ARE one subscription under two aliases; a seat without one is not yet
|
|
233
|
+
* identified (its next token refresh fills it in) and is never guessed at.
|
|
234
|
+
*/
|
|
235
|
+
export function checkAccountIdentity(input) {
|
|
236
|
+
if (input.accounts.length < 2)
|
|
237
|
+
return [];
|
|
238
|
+
const byId = new Map();
|
|
239
|
+
let unidentified = 0;
|
|
240
|
+
for (const a of input.accounts) {
|
|
241
|
+
if (!a.accountId) {
|
|
242
|
+
unidentified++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const g = byId.get(a.accountId);
|
|
246
|
+
if (g) {
|
|
247
|
+
g.aliases.push(a.alias);
|
|
248
|
+
if (!g.email && a.accountEmail)
|
|
249
|
+
g.email = a.accountEmail;
|
|
250
|
+
}
|
|
251
|
+
else
|
|
252
|
+
byId.set(a.accountId, { aliases: [a.alias], email: a.accountEmail });
|
|
253
|
+
}
|
|
254
|
+
const distinct = byId.size + unidentified;
|
|
255
|
+
const head = `${input.accounts.length} seats, ${distinct} distinct account${distinct === 1 ? '' : 's'}`
|
|
256
|
+
+ (unidentified > 0 ? ` (${unidentified} not yet identified — filled in by the next token refresh)` : '');
|
|
257
|
+
const dups = [...byId.values()].filter((g) => g.aliases.length > 1);
|
|
258
|
+
if (dups.length === 0) {
|
|
259
|
+
return [{ status: 'ok', label: 'Accounts', detail: `${head} — no alias is a duplicate of another` }];
|
|
260
|
+
}
|
|
261
|
+
const named = dups.map((g) => `${g.aliases.join(' + ')} are the same account${g.email ? ` (${maskEmail(g.email)})` : ''}`).join('; ');
|
|
262
|
+
return [{
|
|
263
|
+
status: 'info',
|
|
264
|
+
label: 'Accounts',
|
|
265
|
+
detail: `${head} — ${named}. Duplicates share one window and one set of limits; the pool counts them once.`,
|
|
266
|
+
}];
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The Client identity doctor row (dario#1244, 2026-09-08). Every `accounts add`
|
|
270
|
+
* used to copy the machine's Claude Code identity into the new alias, so a
|
|
271
|
+
* pool of colleagues' tokens on one machine presented ONE `device_id` /
|
|
272
|
+
* `account_uuid` in `metadata.user_id` across every OAuth account it held.
|
|
273
|
+
* Anthropic ties what it sees to that identity (see checkIdentityDrift).
|
|
274
|
+
* Seats that are genuinely the same account may share it; seats that are
|
|
275
|
+
* different accounts, or not yet identified, must not.
|
|
276
|
+
*/
|
|
277
|
+
export function checkSharedClientIdentity(input) {
|
|
278
|
+
if (input.accounts.length < 2)
|
|
279
|
+
return [];
|
|
280
|
+
const byIdentity = new Map();
|
|
281
|
+
for (const a of input.accounts) {
|
|
282
|
+
if (!a.deviceId && !a.accountUuid)
|
|
283
|
+
continue;
|
|
284
|
+
const k = `${a.deviceId}|${a.accountUuid}`;
|
|
285
|
+
const list = byIdentity.get(k);
|
|
286
|
+
if (list)
|
|
287
|
+
list.push(a);
|
|
288
|
+
else
|
|
289
|
+
byIdentity.set(k, [a]);
|
|
290
|
+
}
|
|
291
|
+
const offending = [...byIdentity.entries()].filter(([, seats]) => {
|
|
292
|
+
if (seats.length < 2)
|
|
293
|
+
return false;
|
|
294
|
+
const ids = new Set(seats.map((s) => s.accountId));
|
|
295
|
+
return ids.size > 1 || ids.has(undefined);
|
|
296
|
+
});
|
|
297
|
+
if (offending.length === 0) {
|
|
298
|
+
return [{ status: 'ok', label: 'Client identity', detail: 'every seat presents its own client identity (or shares one only with the same account)' }];
|
|
299
|
+
}
|
|
300
|
+
const parts = offending.map(([k, seats]) => {
|
|
301
|
+
const accounts = new Set(seats.map((s) => s.accountId ?? `?${s.alias}`)).size;
|
|
302
|
+
return `${seats.length} seats present ONE client identity (device ${(k.split('|')[0] || '(empty)').slice(0, 8)}…) across ${accounts} account${accounts === 1 ? '' : 's'}: ${seats.map((s) => s.alias).join(', ')}`;
|
|
303
|
+
});
|
|
304
|
+
return [{
|
|
305
|
+
status: 'warn',
|
|
306
|
+
label: 'Client identity',
|
|
307
|
+
detail: `${parts.join('; ')}. Every alias added on a machine with Claude Code installed copied its identity, and Anthropic ties usage and limits to the identity it sees. Give each seat its own: \`dario accounts identity --fresh <alias>\` (or \`--all\`); the running proxy presents the new one on the seat's next request.`,
|
|
225
308
|
}];
|
|
226
309
|
}
|
|
227
310
|
const REGRANT_FIX = " — re-grant with `dario accounts add <alias>` (or `dario login --force-reauth` for the login seat); the new grant restarts the clock";
|
|
@@ -811,8 +894,8 @@ export async function runChecks(opts = {}) {
|
|
|
811
894
|
: '';
|
|
812
895
|
checks.push({
|
|
813
896
|
status: util >= 0.90 ? 'warn' : 'ok',
|
|
814
|
-
label: `Usage 7d (${family} only)`,
|
|
815
|
-
detail: `${pct(util)} used${marker}`,
|
|
897
|
+
label: family === 'oi' ? 'Included overage credit (7d, oi)' : `Usage 7d (${family} only)`,
|
|
898
|
+
detail: `${pct(util)} used${marker}${family === 'oi' ? " — the plan's included-overage credit; binds Fable's weekly allowance (#1262)" : ''}`,
|
|
816
899
|
});
|
|
817
900
|
}
|
|
818
901
|
if (firstOk.overageUtil > 0) {
|
|
@@ -969,6 +1052,7 @@ export async function runChecks(opts = {}) {
|
|
|
969
1052
|
});
|
|
970
1053
|
checks.push(...checkRefreshGrant({ accounts: loaded.map((a) => ({ alias: a.alias, grantedAt: a.grantedAt })), now }));
|
|
971
1054
|
checks.push(...checkOrganizations({ accounts: loaded.map((a) => ({ alias: a.alias, organizationId: a.organizationId })) }));
|
|
1055
|
+
checks.push(...checkAccountIdentity({ accounts: loaded.map((a) => ({ alias: a.alias, accountId: a.accountId, accountEmail: a.accountEmail })) }));
|
|
972
1056
|
// Next-account-in-rotation surfacing. The proxy's per-request
|
|
973
1057
|
// selector picks by max headroom (with 7d_<family> per-model
|
|
974
1058
|
// bucket considered when a request's model family is known);
|
|
@@ -1047,6 +1131,10 @@ export async function runChecks(opts = {}) {
|
|
|
1047
1131
|
});
|
|
1048
1132
|
for (const c of driftChecks)
|
|
1049
1133
|
checks.push(c);
|
|
1134
|
+
for (const c of checkSharedClientIdentity({
|
|
1135
|
+
accounts: loaded.map((a) => ({ alias: a.alias, deviceId: a.deviceId, accountUuid: a.accountUuid, accountId: a.accountId, identityFrom: a.identityFrom })),
|
|
1136
|
+
}))
|
|
1137
|
+
checks.push(c);
|
|
1050
1138
|
}
|
|
1051
1139
|
catch (err) {
|
|
1052
1140
|
checks.push({ status: 'warn', label: 'Identity', detail: `check failed: ${err.message}` });
|
package/dist/effort.d.ts
CHANGED
|
@@ -18,6 +18,20 @@
|
|
|
18
18
|
/** Valid values for the `--effort` flag. Mirrors CC's effort set (`low|medium|high|xhigh|max`) plus CC's `ultracode` mode and dario's pseudo-value `'client'` for passthrough. `'ultracode'` is CC's xhigh-plus-dynamic-workflow-orchestration mode (CC 2.1.154); the Messages API accepts only low|medium|high|xhigh|max, so dario normalizes ultracode → 'xhigh' on the wire (see normalizeEffortForWire). `'client'` passes through the client's own `output_config.effort` (falling back to `'xhigh'`). dario#87, `'max'` added in dario#190, `'ultracode'` added 2026-05-28. */
|
|
19
19
|
export type EffortValue = 'low' | 'medium' | 'high' | 'xhigh' | 'ultracode' | 'max' | 'client';
|
|
20
20
|
export declare const VALID_EFFORT_VALUES: ReadonlyArray<EffortValue>;
|
|
21
|
+
/**
|
|
22
|
+
* dario's effort tiers onto the levels the Codex Responses backend accepts
|
|
23
|
+
* (dario#1260). Probed 2026-08-29: `none, minimal, low, medium, high, xhigh,
|
|
24
|
+
* max` are valid upstream; `ultra` 400s.
|
|
25
|
+
*
|
|
26
|
+
* - low/medium/high/xhigh/max pass through — same name on both sides.
|
|
27
|
+
* - `ultracode` is dario's own tier with no upstream equivalent. It maps to
|
|
28
|
+
* `max`, the nearest thing the backend has, rather than being dropped:
|
|
29
|
+
* a caller who asked for the most effort available should not silently
|
|
30
|
+
* get the default.
|
|
31
|
+
* - `client` means "whatever the client asked for", so it forces nothing
|
|
32
|
+
* and the request's own thinking budget decides, as before.
|
|
33
|
+
*/
|
|
34
|
+
export declare function effortForCodex(effort: EffortValue | undefined): 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined;
|
|
21
35
|
export declare function parseEffortSuffix(model: string): {
|
|
22
36
|
model: string;
|
|
23
37
|
effort?: EffortValue;
|
package/dist/effort.js
CHANGED
|
@@ -26,6 +26,32 @@ export const VALID_EFFORT_VALUES = ['low', 'medium', 'high', 'xhigh', 'ultracode
|
|
|
26
26
|
* suffix removed plus the parsed effort (undefined when none). Exported for tests.
|
|
27
27
|
*/
|
|
28
28
|
const SUFFIX_EFFORTS = ['ultracode', 'medium', 'xhigh', 'high', 'low', 'max'];
|
|
29
|
+
/**
|
|
30
|
+
* dario's effort tiers onto the levels the Codex Responses backend accepts
|
|
31
|
+
* (dario#1260). Probed 2026-08-29: `none, minimal, low, medium, high, xhigh,
|
|
32
|
+
* max` are valid upstream; `ultra` 400s.
|
|
33
|
+
*
|
|
34
|
+
* - low/medium/high/xhigh/max pass through — same name on both sides.
|
|
35
|
+
* - `ultracode` is dario's own tier with no upstream equivalent. It maps to
|
|
36
|
+
* `max`, the nearest thing the backend has, rather than being dropped:
|
|
37
|
+
* a caller who asked for the most effort available should not silently
|
|
38
|
+
* get the default.
|
|
39
|
+
* - `client` means "whatever the client asked for", so it forces nothing
|
|
40
|
+
* and the request's own thinking budget decides, as before.
|
|
41
|
+
*/
|
|
42
|
+
export function effortForCodex(effort) {
|
|
43
|
+
switch (effort) {
|
|
44
|
+
case 'low':
|
|
45
|
+
case 'medium':
|
|
46
|
+
case 'high':
|
|
47
|
+
case 'xhigh':
|
|
48
|
+
case 'max': return effort;
|
|
49
|
+
case 'ultracode': return 'max';
|
|
50
|
+
case 'client':
|
|
51
|
+
case undefined: return undefined;
|
|
52
|
+
default: return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
29
55
|
export function parseEffortSuffix(model) {
|
|
30
56
|
for (const e of SUFFIX_EFFORTS) {
|
|
31
57
|
for (const sep of [':', '-']) {
|
package/dist/pool.d.ts
CHANGED
|
@@ -38,6 +38,34 @@ export interface RateLimitSnapshot {
|
|
|
38
38
|
reset: number;
|
|
39
39
|
fallbackPct: number;
|
|
40
40
|
updatedAt: number;
|
|
41
|
+
/**
|
|
42
|
+
* `retry-after` on the response, in ms, or null/undefined when absent. Read
|
|
43
|
+
* for a 429 that names no exhausted window (see `exhausted`): it is the only
|
|
44
|
+
* duration the upstream actually stated for that case.
|
|
45
|
+
*/
|
|
46
|
+
retryAfterMs?: number | null;
|
|
47
|
+
/**
|
|
48
|
+
* Set by `markRejected`. True when the 429 itself showed a window was over —
|
|
49
|
+
* a utilization at or past the 1.0 threshold on some window or bucket — so the
|
|
50
|
+
* seat is parked until `reset`. False when the 429 said no such thing
|
|
51
|
+
* (dario#1244 follow-up, 2026-09-08): a seat on the fleet box was parked for
|
|
52
|
+
* 546 HOURS on a 429 reading `5h 0%, 7d 0%, claim unknown`, because the
|
|
53
|
+
* status code alone used to decide and the stated reset was honoured
|
|
54
|
+
* blindly. Such a rejection cools for `retryAfterMs` (or a minute) and stays
|
|
55
|
+
* probeable. Undefined on snapshots that predate the field, which behave as
|
|
56
|
+
* before (exhausted).
|
|
57
|
+
*/
|
|
58
|
+
exhausted?: boolean;
|
|
59
|
+
/** Epoch ms a non-exhausted rejection stops being ineligible. */
|
|
60
|
+
cooldownUntil?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Which per-model buckets the upstream has shown to bind which model
|
|
63
|
+
* family on THIS account, learned from responses: `{ fable: ['oi'] }`. Merged
|
|
64
|
+
* across readings by the pool, so a binding learned on one response holds
|
|
65
|
+
* for the seat's later headroom decisions. See `WIRE_BUCKET_BINDINGS` for the
|
|
66
|
+
* static seed and the evidence.
|
|
67
|
+
*/
|
|
68
|
+
boundBuckets?: Record<string, string[]>;
|
|
41
69
|
}
|
|
42
70
|
export declare const EMPTY_SNAPSHOT: RateLimitSnapshot;
|
|
43
71
|
/** Freshness of an account's utilisation reading — see `utilFreshness`. */
|
|
@@ -94,25 +122,35 @@ export declare function rateLimitWindow(rl: RateLimitSnapshot, now: number): Rat
|
|
|
94
122
|
*/
|
|
95
123
|
export declare function describeRateLimitSnapshot(rl: RateLimitSnapshot, now?: number): string;
|
|
96
124
|
/**
|
|
97
|
-
* The
|
|
98
|
-
*
|
|
99
|
-
*
|
|
125
|
+
* The log line for a 429 as `markRejected` classified it: an exhausted window
|
|
126
|
+
* parks the seat until its reset; anything else cools briefly and says which
|
|
127
|
+
* stated reset was NOT honoured, so the operator can see the reading dario
|
|
128
|
+
* declined to act on.
|
|
129
|
+
*/
|
|
130
|
+
/** `th***@example.com` — enough to recognise an account by eye, never the whole address on a listing or a log. */
|
|
131
|
+
export declare function maskEmail(email: string | null | undefined): string | null;
|
|
132
|
+
export declare function describeRejection(rl: RateLimitSnapshot, now?: number): string;
|
|
133
|
+
/**
|
|
134
|
+
* For every seat, the other aliases that are the SAME Anthropic account —
|
|
135
|
+
* one subscription under several aliases (dario#1244).
|
|
100
136
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* is
|
|
137
|
+
* Identity, not inference. The first version of this keyed seats on
|
|
138
|
+
* `claim@reset` — "two independent windows all but never share a reset
|
|
139
|
+
* second". They do: Anthropic aligns the five-hour reset to a 20-minute grid
|
|
140
|
+
* (dario#1263 — two demonstrably different accounts, both resetting at
|
|
141
|
+
* exactly :40:00), so a five-hour window has 15 possible reset seconds and a
|
|
142
|
+
* pool of 18 seats is GUARANTEED collisions. That heuristic told an operator
|
|
143
|
+
* seven independent colleagues were one subscription. `accountId` is the
|
|
144
|
+
* account uuid the OAuth grant belongs to (accounts.ts, `fetchOAuthProfile`);
|
|
145
|
+
* two seats with the same one are the same account, full stop, and two
|
|
146
|
+
* seats without one are simply not yet identified — never guessed.
|
|
107
147
|
*/
|
|
108
|
-
export declare function
|
|
109
|
-
/** For every seat, the other aliases whose last reading names the same live window. */
|
|
110
|
-
export declare function windowPeers(accounts: readonly PoolAccount[], now: number): Map<string, string[]>;
|
|
148
|
+
export declare function accountPeers(accounts: readonly PoolAccount[]): Map<string, string[]>;
|
|
111
149
|
/**
|
|
112
|
-
* How many
|
|
113
|
-
* each seat
|
|
150
|
+
* How many accounts the pool really has: each identified account once, and
|
|
151
|
+
* each seat not yet identified as its own (nothing says otherwise).
|
|
114
152
|
*/
|
|
115
|
-
export declare function
|
|
153
|
+
export declare function distinctAccounts(accounts: readonly PoolAccount[]): number;
|
|
116
154
|
export interface PoolAccount {
|
|
117
155
|
alias: string;
|
|
118
156
|
accessToken: string;
|
|
@@ -131,6 +169,19 @@ export interface PoolAccount {
|
|
|
131
169
|
rejectedCount: number;
|
|
132
170
|
/** Epoch ms of the most recent 429 on this account; undefined if never. */
|
|
133
171
|
lastRejectedAt?: number;
|
|
172
|
+
/**
|
|
173
|
+
* The Anthropic account uuid behind this seat's token, from the OAuth
|
|
174
|
+
* profile at grant time (accounts.ts). The one fact that says whether two
|
|
175
|
+
* aliases are one subscription (dario#1244, #1263). Undefined until the
|
|
176
|
+
* seat's record carries it — a record from before this field is filled in
|
|
177
|
+
* by its next token refresh.
|
|
178
|
+
*/
|
|
179
|
+
accountId?: string;
|
|
180
|
+
/** Email on that account, for the operator's eye; masked on every listing. */
|
|
181
|
+
accountEmail?: string;
|
|
182
|
+
/** `organization.rate_limit_tier` / `seat_tier` from the same profile, when stated. */
|
|
183
|
+
rateLimitTier?: string;
|
|
184
|
+
seatTier?: string;
|
|
134
185
|
/**
|
|
135
186
|
* The Anthropic organization behind this seat's token, from the
|
|
136
187
|
* `anthropic-organization-id` response header: learned on the first
|
|
@@ -238,6 +289,32 @@ export declare function isAccountEligible(account: PoolAccount, now?: number): b
|
|
|
238
289
|
* back, so it stays probeable (dario#1244).
|
|
239
290
|
*/
|
|
240
291
|
export declare function isParkedInLiveWindow(account: PoolAccount, now?: number): boolean;
|
|
292
|
+
/**
|
|
293
|
+
* A seat rejected by a 429 that named no exhausted window, still inside the
|
|
294
|
+
* cool-down that rejection earned (see `RateLimitSnapshot.exhausted`). It is
|
|
295
|
+
* deliberately NOT "parked in a live window" — the reset it stated was never
|
|
296
|
+
* this seat's window — but it is equally not askable yet: the upstream said
|
|
297
|
+
* retry after N, and asking sooner is the retry storm the cool-down exists to
|
|
298
|
+
* prevent (dario#1264 review).
|
|
299
|
+
*/
|
|
300
|
+
export declare function isCoolingAfterRejection(account: PoolAccount, now?: number): boolean;
|
|
301
|
+
/**
|
|
302
|
+
* Whether the router may send this seat a request RIGHT NOW, for reasons that
|
|
303
|
+
* expire on their own: an auth cool-down, a live rate-limit window, or the
|
|
304
|
+
* cool-down a non-window 429 earned.
|
|
305
|
+
*
|
|
306
|
+
* ONE predicate, filtered on by both fallback paths (the all-exhausted branch
|
|
307
|
+
* in `select()` and the mid-flight `selectExcluding()`). They carried the
|
|
308
|
+
* condition inline and independently, so `isCoolingAfterRejection` was added to
|
|
309
|
+
* neither — a seat that had just answered `retry-after: 17` could be retried
|
|
310
|
+
* inside the same client request. Caught in review on #1264; the shape of the
|
|
311
|
+
* bug is why this is a named predicate rather than two copies of a filter.
|
|
312
|
+
*
|
|
313
|
+
* Note this is NOT `isAccountEligible`: eligibility also refuses an expired
|
|
314
|
+
* token, which no amount of waiting fixes and which these paths handle
|
|
315
|
+
* separately.
|
|
316
|
+
*/
|
|
317
|
+
export declare function isProbeable(account: PoolAccount, now?: number): boolean;
|
|
241
318
|
/**
|
|
242
319
|
* The operator's next step for one seat, next to `status` on both listings
|
|
243
320
|
* (dario#1244 — "do I have to re-login?" should not need the docs table).
|
|
@@ -280,8 +357,50 @@ export type PoolStrategy = 'headroom' | 'fill-first';
|
|
|
280
357
|
* in this codebase (see resolveSessionRotationConfig).
|
|
281
358
|
*/
|
|
282
359
|
export declare function resolvePoolStrategy(explicit?: string | null, env?: NodeJS.ProcessEnv): PoolStrategy;
|
|
360
|
+
/**
|
|
361
|
+
* Which wire buckets bind which model family, when the wire does not say it
|
|
362
|
+
* by name. `7d_sonnet` names its family; `7d_oi` does not — it is the plan's
|
|
363
|
+
* INCLUDED-OVERAGE credit, and it is what Fable's weekly allowance is metered
|
|
364
|
+
* on (dario#1262):
|
|
365
|
+
*
|
|
366
|
+
* - 2026-07-05, live Max account: Fable drew `representative-claim:
|
|
367
|
+
* seven_day_overage_included` at 7d 82% with `7d_oi` at 99%, served at $0;
|
|
368
|
+
* at `7d_oi` ≥ 1.0 Fable answered a hard 429 (`7d_oi-status: rejected`,
|
|
369
|
+
* `7d_oi-surpassed-threshold: 1.0`) while Opus kept serving.
|
|
370
|
+
* - 2026-09-08 (#1262): a Fable response at `7d 0.63, 7d_oi 0.98` — headroom
|
|
371
|
+
* read 0.37 against a seat two points from refusal.
|
|
372
|
+
*
|
|
373
|
+
* So for Fable, `oi` IS the binding weekly bucket, under a name that is not
|
|
374
|
+
* "fable". This seed makes that true from the first response. Bindings for
|
|
375
|
+
* other families are LEARNED per account from the wire (see
|
|
376
|
+
* `parseRateLimits`): a response whose claim is `*_overage_included`, or a 429
|
|
377
|
+
* whose `7d_<bucket>-status` is `rejected`, proves that bucket binds the family
|
|
378
|
+
* that request was for. Nothing here maps a family to `oi` by assumption:
|
|
379
|
+
* a seat drawing on included overage for Opus is bound by `oi` for Opus
|
|
380
|
+
* exactly when its responses say so.
|
|
381
|
+
*/
|
|
382
|
+
export declare const WIRE_BUCKET_BINDINGS: Readonly<Record<string, readonly string[]>>;
|
|
383
|
+
/** Parse `retry-after`: delta-seconds or an HTTP date; null when absent or unreadable. */
|
|
384
|
+
export declare function parseRetryAfterMs(value: string | null, now?: number): number | null;
|
|
385
|
+
/** Union of two readings' learned bindings, per family, order-preserving. */
|
|
386
|
+
export declare function mergeBoundBuckets(prev: Record<string, string[]> | undefined, next: Record<string, string[]> | undefined): Record<string, string[]> | undefined;
|
|
387
|
+
/** `next` with everything the account had already learned carried forward. */
|
|
388
|
+
export declare function withBoundBuckets(prev: RateLimitSnapshot, next: RateLimitSnapshot): RateLimitSnapshot;
|
|
283
389
|
/** Parse an Anthropic response's rate-limit headers into a snapshot. */
|
|
284
|
-
export declare function parseRateLimits(headers: Headers): RateLimitSnapshot;
|
|
390
|
+
export declare function parseRateLimits(headers: Headers, family?: string | null): RateLimitSnapshot;
|
|
391
|
+
/**
|
|
392
|
+
* Does this 429 say a rate-limit WINDOW is over? True only when the reading
|
|
393
|
+
* itself shows one: a utilization at or past the 1.0 threshold on any window
|
|
394
|
+
* or bucket (the unified headers are a ratio against
|
|
395
|
+
* `surpassed-threshold: 1.0`, so `1.02` is 102%; every live rejection observed
|
|
396
|
+
* has read 1.00–1.06). A 429 with a claim but 30% used, or with no claim and
|
|
397
|
+
* 0% used, is a refusal of some other kind — concurrency, an account-level
|
|
398
|
+
* lock, a monthly credit — and its stated `reset` is not the moment this
|
|
399
|
+
* seat's window rolls. 0.99 rather than 1 absorbs float formatting.
|
|
400
|
+
*/
|
|
401
|
+
export declare function isWindowRejection(rl: RateLimitSnapshot): boolean;
|
|
402
|
+
/** Cool-down for a 429 that named no exhausted window, when it stated no `retry-after`. */
|
|
403
|
+
export declare const NON_WINDOW_REJECTION_COOLDOWN_MS = 60000;
|
|
285
404
|
/**
|
|
286
405
|
* Extract the model family (`opus` / `sonnet` / `haiku` / `fable`) from a
|
|
287
406
|
* request's model id. Used to look up the per-model 7d bucket in
|
|
@@ -307,8 +426,14 @@ export declare function modelFamily(modelId: string | null | undefined): string
|
|
|
307
426
|
* isn't represented in the snapshot (e.g. account hasn't seen a Sonnet
|
|
308
427
|
* request yet so `7d_sonnet` is unknown), headroom is computed from the
|
|
309
428
|
* unified buckets only — best-effort, populated on the next response.
|
|
429
|
+
*
|
|
430
|
+
* A bucket counts for a family when it names the family (`7d_sonnet`), when
|
|
431
|
+
* `WIRE_BUCKET_BINDINGS` says it binds the family (`7d_oi` → fable, dario#1262),
|
|
432
|
+
* or when this account's responses have shown it does (`boundBuckets`).
|
|
310
433
|
*/
|
|
311
434
|
export declare function computeHeadroom(snapshot: RateLimitSnapshot, family?: string | null): number;
|
|
435
|
+
/** Every bucket name that binds `family` for this reading — by name, by seed, or as learned. */
|
|
436
|
+
export declare function bucketsBindingFamily(snapshot: RateLimitSnapshot, family: string): string[];
|
|
312
437
|
export declare class AccountPool {
|
|
313
438
|
private readonly strategy;
|
|
314
439
|
private accounts;
|
|
@@ -327,6 +452,10 @@ export declare class AccountPool {
|
|
|
327
452
|
accountUuid: string;
|
|
328
453
|
grantedAt?: number;
|
|
329
454
|
organizationId?: string;
|
|
455
|
+
accountId?: string;
|
|
456
|
+
accountEmail?: string;
|
|
457
|
+
rateLimitTier?: string;
|
|
458
|
+
seatTier?: string;
|
|
330
459
|
}): void;
|
|
331
460
|
remove(alias: string): boolean;
|
|
332
461
|
get size(): number;
|
|
@@ -367,7 +496,7 @@ export declare class AccountPool {
|
|
|
367
496
|
* review on dario#1254 that caught the mixed case).
|
|
368
497
|
*/
|
|
369
498
|
parkedUntil(now?: number): number | null;
|
|
370
|
-
/** Seats currently
|
|
499
|
+
/** Seats a rate limit is currently keeping out of rotation (dario#1244, #1264). */
|
|
371
500
|
parkedCount(now?: number): number;
|
|
372
501
|
/**
|
|
373
502
|
* Select with session stickiness. If `stickyKey` is already bound to a
|