@cortexkit/anthropic-auth-core 1.15.0 → 1.16.0

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/dump.d.ts CHANGED
@@ -13,6 +13,25 @@ export declare function isDumpEnabled(): boolean;
13
13
  export declare function setDumpEnabled(enabled: boolean): void;
14
14
  export declare function resetDumpState(): void;
15
15
  export declare function getDumpDirectory(): string;
16
+ export declare function writeDumpFile(path: string, contents: string): Promise<boolean>;
17
+ /**
18
+ * Caps completed dump artifacts in the configured directory. A zero-byte cap
19
+ * disables sweeping. Custom directories may contain unrelated files: only
20
+ * recognized final artifact names and stale partials are eligible. Symlinks,
21
+ * fresh partial writes, and files younger than the newness floor remain
22
+ * protected.
23
+ */
24
+ export declare function sweepDumpDirectory(options: {
25
+ dumpDir?: string;
26
+ maxBytes?: number;
27
+ protectedPaths?: readonly string[];
28
+ now?: number;
29
+ minAgeMs?: number;
30
+ partialStaleMs?: number;
31
+ }): Promise<{
32
+ removed: number;
33
+ freedBytes: number;
34
+ }>;
16
35
  export declare function parseDumpCommandAction(argumentsText: string): DumpCommandAction;
17
36
  export declare function buildDumpStatusSummary(input?: {
18
37
  enabled?: boolean;
package/dist/dump.js CHANGED
@@ -1,9 +1,9 @@
1
- import { createHash } from 'node:crypto';
2
- import { mkdir, writeFile } from 'node:fs/promises';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { lstat, mkdir, readdir, rename, unlink, writeFile, } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { join, resolve } from 'node:path';
5
5
  import { extractBillingHeaderCCH } from "./cch.js";
6
- import { isSecretKey, relayLog } from "./logger.js";
6
+ import { isSecretKey, logger, relayLog } from "./logger.js";
7
7
  export const CLAUDE_DUMP_COMMAND_NAME = 'claude-dump';
8
8
  const DUMP_STATUS_TITLE = '## Claude Dump Status';
9
9
  const DUMP_ENABLED_TITLE = '## Claude Dump Enabled';
@@ -11,9 +11,21 @@ const DUMP_DISABLED_TITLE = '## Claude Dump Disabled';
11
11
  const DUMP_USAGE_TITLE = '## Claude Dump Usage';
12
12
  const DUMP_USAGE = 'Usage: `/claude-dump`, `/claude-dump on`, or `/claude-dump off`.';
13
13
  const DUMP_DIR_ENV = 'OPENCODE_ANTHROPIC_AUTH_DUMP_DIR';
14
+ const DUMP_MAX_BYTES_ENV = 'OPENCODE_ANTHROPIC_AUTH_DUMP_MAX_BYTES';
14
15
  const DEFAULT_DUMP_DIR = join(tmpdir(), 'opencode-anthropic-auth-dumps');
16
+ const DEFAULT_DUMP_MAX_BYTES = 512 * 1024 * 1024;
17
+ const DUMP_SWEEP_INTERVAL_MS = 5 * 60 * 1000;
18
+ const DUMP_SWEEP_NEWNESS_FLOOR_MS = 60 * 1000;
19
+ const DUMP_PARTIAL_STALE_MS = 10 * 60 * 1000;
20
+ const DUMP_ARTIFACT_SUFFIX_PATTERN = /\.(body|meta|relay|request)\.json$/;
21
+ // Earlier builds emitted five-digit counters; current counters grow without truncation.
22
+ const DUMP_ARTIFACT_ID_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-\d{5,}-(.+)$/;
23
+ const DUMP_SEGMENT_PATTERN = '[a-zA-Z0-9._-]';
24
+ const DIRECT_DUMP_PATTERN = new RegExp(`^${DUMP_SEGMENT_PATTERN}{1,80}-direct(?:-${DUMP_SEGMENT_PATTERN}{1,80})?$`);
25
+ const RELAY_DUMP_PATTERN = new RegExp(`^${DUMP_SEGMENT_PATTERN}{1,80}-(?:http|websocket)-p[12]-(?:full_sync|patch)$`);
15
26
  let dumpEnabled = false;
16
27
  let nextDumpId = 0;
28
+ let lastDumpSweepAt = 0;
17
29
  const DIRECT_DUMP_PREVIOUS_BODY_LIMIT = 100;
18
30
  const directDumpPreviousBodies = new Map();
19
31
  export function isDumpEnabled() {
@@ -25,11 +37,150 @@ export function setDumpEnabled(enabled) {
25
37
  export function resetDumpState() {
26
38
  dumpEnabled = false;
27
39
  nextDumpId = 0;
40
+ lastDumpSweepAt = 0;
28
41
  directDumpPreviousBodies.clear();
29
42
  }
30
43
  export function getDumpDirectory() {
31
44
  return process.env[DUMP_DIR_ENV] || DEFAULT_DUMP_DIR;
32
45
  }
46
+ function getDumpMaxBytes() {
47
+ const configured = Number(process.env[DUMP_MAX_BYTES_ENV]);
48
+ return Number.isFinite(configured) && configured >= 0
49
+ ? Math.floor(configured)
50
+ : DEFAULT_DUMP_MAX_BYTES;
51
+ }
52
+ function isConfiguredDumpDirectory(path) {
53
+ return resolve(path) === resolve(getDumpDirectory());
54
+ }
55
+ function isDumpArtifactFileName(name) {
56
+ const stem = name.replace(DUMP_ARTIFACT_SUFFIX_PATTERN, '');
57
+ if (stem === name)
58
+ return false;
59
+ const match = DUMP_ARTIFACT_ID_PATTERN.exec(stem);
60
+ const requestPath = match?.[1];
61
+ if (!requestPath)
62
+ return false;
63
+ return (DIRECT_DUMP_PATTERN.test(requestPath) ||
64
+ RELAY_DUMP_PATTERN.test(requestPath));
65
+ }
66
+ function isDumpPartialFileName(name) {
67
+ if (!name.endsWith('.partial'))
68
+ return false;
69
+ const partialStem = name.slice(0, -8);
70
+ if (isDumpArtifactFileName(partialStem))
71
+ return true;
72
+ const separator = partialStem.lastIndexOf('.');
73
+ if (separator < 0)
74
+ return false;
75
+ const nonce = partialStem.slice(separator + 1);
76
+ return (/^[a-f0-9]{24}$/.test(nonce) &&
77
+ isDumpArtifactFileName(partialStem.slice(0, separator)));
78
+ }
79
+ export async function writeDumpFile(path, contents) {
80
+ for (let attempt = 0; attempt < 2; attempt += 1) {
81
+ const nonce = randomBytes(12).toString('hex');
82
+ const partialPath = `${path}.${nonce}.partial`;
83
+ let created = false;
84
+ try {
85
+ await writeFile(partialPath, contents, { encoding: 'utf8', flag: 'wx' });
86
+ created = true;
87
+ await rename(partialPath, path);
88
+ return true;
89
+ }
90
+ catch (error) {
91
+ if (error.code === 'EEXIST')
92
+ continue;
93
+ if (created)
94
+ await unlink(partialPath).catch(() => { });
95
+ throw error;
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+ /**
101
+ * Caps completed dump artifacts in the configured directory. A zero-byte cap
102
+ * disables sweeping. Custom directories may contain unrelated files: only
103
+ * recognized final artifact names and stale partials are eligible. Symlinks,
104
+ * fresh partial writes, and files younger than the newness floor remain
105
+ * protected.
106
+ */
107
+ export async function sweepDumpDirectory(options) {
108
+ const dumpDir = options.dumpDir ?? getDumpDirectory();
109
+ const maxBytes = options.maxBytes ?? getDumpMaxBytes();
110
+ const now = options.now ?? Date.now();
111
+ const minAgeMs = options.minAgeMs ?? DUMP_SWEEP_NEWNESS_FLOOR_MS;
112
+ const partialStaleMs = options.partialStaleMs ?? DUMP_PARTIAL_STALE_MS;
113
+ const emptyResult = { removed: 0, freedBytes: 0 };
114
+ if (!isConfiguredDumpDirectory(dumpDir) || maxBytes <= 0)
115
+ return emptyResult;
116
+ try {
117
+ const dumpDirStats = await lstat(dumpDir);
118
+ if (dumpDirStats.isSymbolicLink())
119
+ return emptyResult;
120
+ const protectedPaths = new Set((options.protectedPaths ?? []).map((path) => resolve(path)));
121
+ const entries = await readdir(dumpDir, { withFileTypes: true });
122
+ const files = [];
123
+ await Promise.all(entries.map(async (entry) => {
124
+ if (!entry.isFile())
125
+ return;
126
+ const partial = isDumpPartialFileName(entry.name);
127
+ if (!partial && !isDumpArtifactFileName(entry.name))
128
+ return;
129
+ const path = join(dumpDir, entry.name);
130
+ try {
131
+ const stats = await lstat(path);
132
+ if (!stats.isFile() || stats.isSymbolicLink())
133
+ return;
134
+ files.push({
135
+ path,
136
+ size: stats.size,
137
+ mtimeMs: stats.mtimeMs,
138
+ partial,
139
+ });
140
+ }
141
+ catch {
142
+ // Files can disappear while concurrent dump requests finish.
143
+ }
144
+ }));
145
+ let totalBytes = files.reduce((total, file) => total + file.size, 0);
146
+ files.sort((left, right) => left.mtimeMs - right.mtimeMs || left.path.localeCompare(right.path));
147
+ let removed = 0;
148
+ let freedBytes = 0;
149
+ for (const file of files) {
150
+ if (protectedPaths.has(resolve(file.path)))
151
+ continue;
152
+ const ageMs = now - file.mtimeMs;
153
+ const stalePartial = file.partial && ageMs >= partialStaleMs;
154
+ if (!stalePartial && totalBytes <= maxBytes)
155
+ continue;
156
+ if (file.partial ? !stalePartial : ageMs < minAgeMs)
157
+ continue;
158
+ try {
159
+ await unlink(file.path);
160
+ totalBytes -= file.size;
161
+ freedBytes += file.size;
162
+ removed += 1;
163
+ }
164
+ catch {
165
+ // Dump cleanup is best-effort and must not affect request handling.
166
+ }
167
+ }
168
+ if (removed > 0) {
169
+ logger.debug('dump', 'removed old dump files', { removed, freedBytes });
170
+ }
171
+ return { removed, freedBytes };
172
+ }
173
+ catch {
174
+ return emptyResult;
175
+ }
176
+ }
177
+ function scheduleDumpSweep(dumpDir, protectedPaths) {
178
+ const now = Date.now();
179
+ if (now - lastDumpSweepAt < DUMP_SWEEP_INTERVAL_MS)
180
+ return;
181
+ lastDumpSweepAt = now;
182
+ void sweepDumpDirectory({ dumpDir, protectedPaths }).catch(() => { });
183
+ }
33
184
  export function parseDumpCommandAction(argumentsText) {
34
185
  const normalized = argumentsText.trim().split(/\s+/).filter(Boolean);
35
186
  if (normalized.length === 0)
@@ -228,7 +379,7 @@ async function dumpRequest(input) {
228
379
  return;
229
380
  nextDumpId += 1;
230
381
  const affinity = input.affinity?.trim() || 'session-unknown';
231
- const id = `${new Date().toISOString().replace(/[:.]/g, '-')}-${String(nextDumpId).padStart(5, '0')}-${dumpFileSessionSegment(affinity)}-${dumpRequestSegment(input)}`;
382
+ const id = `${new Date().toISOString().replace(/[:.]/g, '-')}-${String(nextDumpId).padStart(6, '0')}-${dumpFileSessionSegment(affinity)}-${dumpRequestSegment(input)}`;
232
383
  const dumpDir = getDumpDirectory();
233
384
  const prefix = join(dumpDir, id);
234
385
  const files = {
@@ -259,19 +410,20 @@ async function dumpRequest(input) {
259
410
  files,
260
411
  };
261
412
  const writes = [
262
- writeFile(files.body, input.bodyText, 'utf8'),
263
- writeFile(files.metadata, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8'),
413
+ writeDumpFile(files.body, input.bodyText),
414
+ writeDumpFile(files.metadata, `${JSON.stringify(metadata, null, 2)}\n`),
264
415
  ];
265
416
  if (input.payload !== undefined && files.relay) {
266
- writes.push(writeFile(files.relay, `${JSON.stringify(redactForDump(input.payload), null, 2)}\n`, 'utf8'));
417
+ writes.push(writeDumpFile(files.relay, `${JSON.stringify(redactForDump(input.payload), null, 2)}\n`));
267
418
  }
268
419
  if (input.request !== undefined && files.request) {
269
- writes.push(writeFile(files.request, `${JSON.stringify(redactForDump({
420
+ writes.push(writeDumpFile(files.request, `${JSON.stringify(redactForDump({
270
421
  ...input.request,
271
422
  headers: headersToRecord(input.request.headers),
272
- }), null, 2)}\n`, 'utf8'));
423
+ }), null, 2)}\n`));
273
424
  }
274
425
  await Promise.all(writes);
426
+ scheduleDumpSweep(dumpDir, Object.values(files));
275
427
  relayLog(`dumped request id=${id} session=${shortAffinity(affinity)} body=${files.body} meta=${files.metadata}`);
276
428
  }
277
429
  catch (error) {
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './accounts.ts';
2
2
  export * from './auth.ts';
3
3
  export * from './cache1h.ts';
4
4
  export * from './cachekeep.ts';
5
+ export * from './cachekeep-registry.ts';
5
6
  export * from './cch.ts';
6
7
  export * from './claude-code.ts';
7
8
  export * from './commands/account.ts';
@@ -18,3 +19,4 @@ export * from './quota-manager.ts';
18
19
  export * from './quotas.ts';
19
20
  export * from './relay.ts';
20
21
  export * from './routing.ts';
22
+ export * from './sticky-routing.ts';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export * from "./accounts.js";
2
2
  export * from "./auth.js";
3
3
  export * from "./cache1h.js";
4
4
  export * from "./cachekeep.js";
5
+ export * from "./cachekeep-registry.js";
5
6
  export * from "./cch.js";
6
7
  export * from "./claude-code.js";
7
8
  export * from "./commands/account.js";
@@ -18,3 +19,4 @@ export * from "./quota-manager.js";
18
19
  export * from "./quotas.js";
19
20
  export * from "./relay.js";
20
21
  export * from "./routing.js";
22
+ export * from "./sticky-routing.js";
@@ -67,14 +67,15 @@ export declare class QuotaManager {
67
67
  * Fire-and-forget refresh. Does not await, swallows errors.
68
68
  */
69
69
  refreshMainInBackground(accessToken: string): void;
70
- isMainStale(): boolean;
71
- isFallbackStale(accountId: string, accessToken?: string): boolean;
70
+ private scopedWindowIsStale;
71
+ isMainStale(modelId?: string): boolean;
72
+ isFallbackStale(accountId: string, accessToken?: string, modelId?: string): boolean;
72
73
  shouldRefreshOnRequestCount(requestCount: number): boolean;
73
74
  /**
74
75
  * Combined check: should a refresh happen right now?
75
76
  * True if main is stale by time OR triggered by request count.
76
77
  */
77
- needsRefresh(requestCount: number): boolean;
78
+ needsRefresh(requestCount: number, modelId?: string): boolean;
78
79
  updateStorage(storage: AccountStorage | null): void;
79
80
  /**
80
81
  * Seed/update the main quota cache from persisted state. This is deliberately
@@ -6,7 +6,7 @@
6
6
  * Handles deduplication, rate-limiting (429 backoff), and staleness.
7
7
  */
8
8
  import { createHash } from 'node:crypto';
9
- import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, isQuotaPolicyAuthError, quotaBackoffActive, } from "./accounts.js";
9
+ import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, getScopedQuotaWindowForModel, isQuotaPolicyAuthError, quotaBackoffActive, } from "./accounts.js";
10
10
  // Capture real setTimeout before tests can mock globalThis.setTimeout
11
11
  const nativeSetTimeout = globalThis.setTimeout;
12
12
  /**
@@ -190,17 +190,24 @@ export class QuotaManager {
190
190
  // =========================================================================
191
191
  // Staleness queries
192
192
  // =========================================================================
193
- isMainStale() {
193
+ scopedWindowIsStale(entry, modelId) {
194
+ const scoped = getScopedQuotaWindowForModel(entry.quota, modelId);
195
+ return Boolean(scoped &&
196
+ this.now() - scoped.checkedAt >= getQuotaCheckIntervalMs(this.storage));
197
+ }
198
+ isMainStale(modelId) {
194
199
  if (!this.main)
195
200
  return true;
196
- return this.now() >= this.main.refreshAfter;
201
+ return (this.now() >= this.main.refreshAfter ||
202
+ this.scopedWindowIsStale(this.main, modelId));
197
203
  }
198
- isFallbackStale(accountId, accessToken) {
204
+ isFallbackStale(accountId, accessToken, modelId) {
199
205
  // Token-aware: a credential change invalidates the entry (treated as stale).
200
206
  const entry = this.getFallback(accountId, accessToken);
201
207
  if (!entry)
202
208
  return true;
203
- return this.now() >= entry.refreshAfter;
209
+ return (this.now() >= entry.refreshAfter ||
210
+ this.scopedWindowIsStale(entry, modelId));
204
211
  }
205
212
  shouldRefreshOnRequestCount(requestCount) {
206
213
  const everyN = getQuotaRefreshEveryNRequests(this.storage);
@@ -212,8 +219,9 @@ export class QuotaManager {
212
219
  * Combined check: should a refresh happen right now?
213
220
  * True if main is stale by time OR triggered by request count.
214
221
  */
215
- needsRefresh(requestCount) {
216
- return this.isMainStale() || this.shouldRefreshOnRequestCount(requestCount);
222
+ needsRefresh(requestCount, modelId) {
223
+ return (this.isMainStale(modelId) ||
224
+ this.shouldRefreshOnRequestCount(requestCount));
217
225
  }
218
226
  // =========================================================================
219
227
  // Config
package/dist/routing.d.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { type AccountStorage, type RoutingMode } from './accounts.ts';
2
2
  export declare const CLAUDE_ROUTING_COMMAND_NAME = "claude-routing";
3
- export declare const ROUTING_MODES: readonly ["main-first", "fallback-first"];
3
+ export declare const ROUTING_MODES: readonly ["main-first", "fallback-first", "sticky-balanced"];
4
4
  export declare const DEFAULT_ROUTING_MODE: RoutingMode;
5
5
  type RoutingCommandAction = {
6
6
  type: 'status';
7
7
  } | {
8
8
  type: 'mode';
9
9
  mode: RoutingMode;
10
+ } | {
11
+ type: 'reset';
10
12
  } | {
11
13
  type: 'usage';
12
14
  };
package/dist/routing.js CHANGED
@@ -1,8 +1,12 @@
1
1
  import { createEmptyStorage, getAccountStoragePath, loadAccounts, saveAccounts, } from "./accounts.js";
2
2
  export const CLAUDE_ROUTING_COMMAND_NAME = 'claude-routing';
3
- export const ROUTING_MODES = ['main-first', 'fallback-first'];
3
+ export const ROUTING_MODES = [
4
+ 'main-first',
5
+ 'fallback-first',
6
+ 'sticky-balanced',
7
+ ];
4
8
  export const DEFAULT_ROUTING_MODE = 'main-first';
5
- const ROUTING_USAGE = 'Usage: `/claude-routing`, `/claude-routing main-first`, or `/claude-routing fallback-first`.';
9
+ const ROUTING_USAGE = 'Usage: `/claude-routing`, `/claude-routing main-first`, `/claude-routing fallback-first`, `/claude-routing sticky-balanced`, or `/claude-routing reset`.';
6
10
  function normalizeRoutingMode(value) {
7
11
  return typeof value === 'string' &&
8
12
  ROUTING_MODES.includes(value)
@@ -25,6 +29,8 @@ export function parseRoutingCommandAction(argumentsText) {
25
29
  const tokens = argumentsText.trim().toLowerCase().split(/\s+/).filter(Boolean);
26
30
  if (tokens.length === 0)
27
31
  return { type: 'status' };
32
+ if (tokens.length === 1 && tokens[0] === 'reset')
33
+ return { type: 'reset' };
28
34
  if (tokens.length === 1 && ROUTING_MODES.includes(tokens[0])) {
29
35
  return { type: 'mode', mode: tokens[0] };
30
36
  }
@@ -39,6 +45,9 @@ function routingDescription(mode) {
39
45
  if (mode === 'fallback-first') {
40
46
  return 'Try usable fallback accounts before the main account. If no fallback succeeds, try the main account.';
41
47
  }
48
+ if (mode === 'sticky-balanced') {
49
+ return 'Assign each session to a quota-weighted OAuth account, keep it sticky across transient failures, and migrate only for confirmed long-lived exhaustion or permanent account failure.';
50
+ }
42
51
  return 'Try the main account first. Use fallback accounts only when quota policy or fallback errors require it.';
43
52
  }
44
53
  export function buildRoutingStatusSummary(input) {
@@ -66,6 +75,15 @@ export function executeRoutingCommand(input) {
66
75
  ...buildRoutingStatusSummary({ mode: action.mode }).split('\n').slice(2),
67
76
  ].join('\n');
68
77
  }
78
+ if (action.type === 'reset') {
79
+ return [
80
+ '## Claude Routing Assignment Reset',
81
+ '',
82
+ 'The current session will be assigned again on its next request.',
83
+ '',
84
+ ...buildRoutingStatusSummary({ mode }).split('\n').slice(2),
85
+ ].join('\n');
86
+ }
69
87
  return [
70
88
  '## Claude Routing Usage',
71
89
  '',
@@ -0,0 +1,86 @@
1
+ import { type AccountStorage, type OAuthQuotaSnapshot } from './accounts.ts';
2
+ export declare const STICKY_ROUTING_MAIN_ACCOUNT_ID = "main";
3
+ export declare const STICKY_ROUTING_SHORT_RESET_GRACE_MS: number;
4
+ export declare const STICKY_ROUTING_ASSIGNMENT_TTL_MS: number;
5
+ export type StickyRouteFamily = 'fable' | 'opus' | 'general';
6
+ export type StickyRouteCandidate = {
7
+ accountId: string;
8
+ quota: OAuthQuotaSnapshot;
9
+ order: number;
10
+ };
11
+ export type StickyRouteAssignment = {
12
+ accountId: string;
13
+ family: StickyRouteFamily;
14
+ assignedAt: number;
15
+ lastSeenAt: number;
16
+ initialInputBytes: number;
17
+ quotaCheckedAt: number;
18
+ };
19
+ export type StickyRouteResolution = {
20
+ accountId: string;
21
+ assignment: StickyRouteAssignment;
22
+ created: boolean;
23
+ migrated: boolean;
24
+ };
25
+ export type StickyQuotaFailureDecision = {
26
+ action: 'retain';
27
+ reason: 'not-exhausted' | 'unknown';
28
+ } | {
29
+ action: 'hold';
30
+ reason: 'five-hour-short-reset';
31
+ retryAfterSeconds: number;
32
+ } | {
33
+ action: 'migrate';
34
+ reason: 'five-hour' | 'seven-day' | 'model-scoped';
35
+ };
36
+ export declare function stickyQuotaSnapshotIsFresh(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, now?: number, modelId?: string): boolean;
37
+ export declare function stickyRouteFamilyForModel(model: unknown): StickyRouteFamily;
38
+ export declare function stickyRouteCandidateWeight(input: {
39
+ candidate: StickyRouteCandidate;
40
+ family: StickyRouteFamily;
41
+ modelId?: string;
42
+ storage: AccountStorage | null;
43
+ now?: number;
44
+ }): number;
45
+ export declare function stickyRouteKnownFableExhausted(candidate: StickyRouteCandidate, storage: AccountStorage | null, now?: number): boolean;
46
+ export declare function decideStickyQuotaFailure(input: {
47
+ quota: OAuthQuotaSnapshot | undefined;
48
+ modelId?: string;
49
+ now?: number;
50
+ }): StickyQuotaFailureDecision;
51
+ export declare function getStickyRoutingStatePath(accountStoragePath: string): string;
52
+ export declare function stickyRetryAfterWithJitter(sessionId: string, retryAfterSeconds: number): number;
53
+ export declare class StickySessionRouter {
54
+ private readonly options;
55
+ private loaded;
56
+ private state;
57
+ private stateMtimeNs;
58
+ private touchChain;
59
+ constructor(options: {
60
+ path: string;
61
+ now?: () => number;
62
+ assignmentTtlMs?: number;
63
+ });
64
+ private now;
65
+ private readState;
66
+ private readStateMtimeNs;
67
+ private refreshStateIfChanged;
68
+ private acquireLock;
69
+ private assignmentIsActive;
70
+ private prune;
71
+ private writeState;
72
+ private scheduleTouch;
73
+ private selectCandidate;
74
+ resolve(input: {
75
+ sessionId: string;
76
+ family: StickyRouteFamily;
77
+ modelId?: string;
78
+ candidates: readonly StickyRouteCandidate[];
79
+ retainAccountIds: ReadonlySet<string>;
80
+ storage: AccountStorage | null;
81
+ inputBytes: number;
82
+ preferredAccountId?: string;
83
+ excludeAccountIds?: ReadonlySet<string>;
84
+ }): Promise<StickyRouteResolution | null>;
85
+ clear(sessionId: string): Promise<void>;
86
+ }