@cortexkit/anthropic-auth-core 1.22.0 → 2.0.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.
Files changed (133) hide show
  1. package/README.md +2 -0
  2. package/dist/accounts.d.ts +52 -8
  3. package/dist/cachekeep.d.ts +27 -1
  4. package/dist/cch.d.ts +11 -1
  5. package/dist/claude-code.d.ts +23 -1
  6. package/dist/claustrum-enrollment.d.ts +72 -0
  7. package/dist/claustrum-scoped-roster.d.ts +27 -0
  8. package/dist/claustrum-scoped-runtime.d.ts +31 -0
  9. package/dist/claustrum-scoped.d.ts +53 -0
  10. package/dist/claustrum.d.ts +16 -82
  11. package/dist/commands/account.d.ts +38 -11
  12. package/dist/constants.d.ts +3 -2
  13. package/dist/custom-headers.d.ts +3 -0
  14. package/dist/index.d.ts +50 -0
  15. package/dist/index.js +29700 -31
  16. package/dist/internal-types/LICENSE +21 -0
  17. package/dist/internal-types/commands/builtins.d.ts +71 -0
  18. package/dist/internal-types/commands/index.d.ts +10 -0
  19. package/dist/internal-types/commands/menu.d.ts +61 -0
  20. package/dist/internal-types/commands/model.d.ts +238 -0
  21. package/dist/internal-types/commands/pi.d.ts +23 -0
  22. package/dist/internal-types/commands/seam.d.ts +72 -0
  23. package/dist/internal-types/fs/refresh-file-lock.d.ts +33 -0
  24. package/dist/internal-types/logger/capture-sink.d.ts +14 -0
  25. package/dist/internal-types/logger/engine.d.ts +100 -0
  26. package/dist/internal-types/logger/index.d.ts +6 -0
  27. package/dist/internal-types/logger/redact.d.ts +11 -0
  28. package/dist/internal-types/pi-slot/index.d.ts +3 -0
  29. package/dist/internal-types/pi-slot/slot.d.ts +49 -0
  30. package/dist/internal-types/quota/codec.d.ts +11 -0
  31. package/dist/internal-types/quota/format.d.ts +46 -0
  32. package/dist/internal-types/quota/index.d.ts +10 -0
  33. package/dist/internal-types/quota/map.d.ts +63 -0
  34. package/dist/internal-types/quota/merge.d.ts +48 -0
  35. package/dist/internal-types/quota/projection.d.ts +64 -0
  36. package/dist/internal-types/store/attribution.d.ts +28 -0
  37. package/dist/internal-types/store/errors.d.ts +55 -0
  38. package/dist/internal-types/store/hooks.d.ts +12 -0
  39. package/dist/internal-types/store/identity.d.ts +39 -0
  40. package/dist/internal-types/store/index.d.ts +21 -0
  41. package/dist/internal-types/store/mutate.d.ts +189 -0
  42. package/dist/internal-types/store/pool.d.ts +196 -0
  43. package/dist/internal-types/store/provider-state.d.ts +131 -0
  44. package/dist/internal-types/store/publication.d.ts +47 -0
  45. package/dist/internal-types/store/pull.d.ts +36 -0
  46. package/dist/internal-types/store/refresh-lock.d.ts +63 -0
  47. package/dist/internal-types/store/refresh.d.ts +72 -0
  48. package/dist/internal-types/store/rows.d.ts +313 -0
  49. package/dist/internal-types/store/runtime.d.ts +36 -0
  50. package/dist/internal-types/store/schema.d.ts +443 -0
  51. package/dist/internal-types/store/settings.d.ts +63 -0
  52. package/dist/internal-types/store/torn.d.ts +78 -0
  53. package/dist/migration-source.d.ts +33 -0
  54. package/dist/model-remap.d.ts +27 -0
  55. package/dist/models.d.ts +60 -0
  56. package/dist/native-account-runtime.d.ts +121 -0
  57. package/dist/native-account-view.d.ts +126 -0
  58. package/dist/native-command-effects.d.ts +59 -0
  59. package/dist/native-credential-validation.d.ts +42 -0
  60. package/dist/native-custody-activation.d.ts +45 -0
  61. package/dist/native-custody.d.ts +114 -0
  62. package/dist/native-enrollment-authority.d.ts +2 -0
  63. package/dist/native-host-auth-read.d.ts +3 -0
  64. package/dist/native-host-auth-write.d.ts +51 -0
  65. package/dist/native-host-auth.d.ts +12 -0
  66. package/dist/native-local-credential-service.d.ts +76 -0
  67. package/dist/native-local-runtime-readers.d.ts +22 -0
  68. package/dist/native-lock-observer.d.ts +12 -0
  69. package/dist/native-menu-executor.d.ts +243 -0
  70. package/dist/native-menu-model.d.ts +142 -0
  71. package/dist/native-migration-proof.d.ts +28 -0
  72. package/dist/native-migration-source.d.ts +47 -0
  73. package/dist/native-migration.d.ts +66 -0
  74. package/dist/native-quota-codec.d.ts +32 -0
  75. package/dist/native-refresh-coordinator.d.ts +240 -0
  76. package/dist/native-roster-seed.d.ts +115 -0
  77. package/dist/native-runtime-registry.d.ts +19 -0
  78. package/dist/native-runtime.d.ts +204 -0
  79. package/dist/native-transient-account.d.ts +38 -0
  80. package/dist/native-ui.d.ts +45 -0
  81. package/dist/native-usage-publication.d.ts +11 -0
  82. package/dist/native-vault-admission.d.ts +8 -0
  83. package/dist/native-vault-connection.d.ts +33 -0
  84. package/dist/native-vault-paths.d.ts +13 -0
  85. package/dist/native-vault-runtime.d.ts +102 -0
  86. package/dist/native-vault-seed.d.ts +16 -0
  87. package/dist/oauth-profile.d.ts +2 -1
  88. package/dist/pool-authority.d.ts +159 -0
  89. package/dist/pool-binding.d.ts +32 -0
  90. package/dist/pool-paths.d.ts +21 -0
  91. package/dist/pool-store.d.ts +32 -0
  92. package/dist/prime.d.ts +11 -3
  93. package/dist/provider-error.d.ts +139 -0
  94. package/dist/quota-header-feed.d.ts +13 -0
  95. package/dist/quota-manager.d.ts +30 -5
  96. package/dist/relay.d.ts +22 -1
  97. package/dist/request-history.d.ts +34 -0
  98. package/dist/sse.d.ts +81 -0
  99. package/dist/sticky-routing.d.ts +29 -0
  100. package/package.json +9 -3
  101. package/dist/accounts.js +0 -3330
  102. package/dist/auth.js +0 -182
  103. package/dist/cache1h.js +0 -97
  104. package/dist/cachekeep-registry.js +0 -124
  105. package/dist/cachekeep.js +0 -578
  106. package/dist/cch.js +0 -127
  107. package/dist/claude-code.js +0 -388
  108. package/dist/claustrum.js +0 -529
  109. package/dist/commands/account.js +0 -239
  110. package/dist/constants.js +0 -82
  111. package/dist/dump.js +0 -616
  112. package/dist/fast.js +0 -66
  113. package/dist/json.js +0 -10
  114. package/dist/killswitch.js +0 -132
  115. package/dist/logger.js +0 -286
  116. package/dist/logging.js +0 -59
  117. package/dist/mid-conversation-output-config.js +0 -89
  118. package/dist/models.js +0 -141
  119. package/dist/network-errors.js +0 -27
  120. package/dist/oauth-profile.js +0 -54
  121. package/dist/pkce.js +0 -17
  122. package/dist/prime.js +0 -823
  123. package/dist/provider.js +0 -0
  124. package/dist/quota-header-feed.js +0 -279
  125. package/dist/quota-headers.js +0 -121
  126. package/dist/quota-manager.js +0 -805
  127. package/dist/quotas.js +0 -141
  128. package/dist/relay.js +0 -1243
  129. package/dist/routing.js +0 -94
  130. package/dist/start.js +0 -29
  131. package/dist/sticky-routing.js +0 -523
  132. package/dist/thinking-binding.js +0 -54
  133. package/dist/token-fingerprint.js +0 -8
package/dist/routing.js DELETED
@@ -1,94 +0,0 @@
1
- import { createEmptyStorage, getAccountStoragePath, loadAccounts, saveAccounts, } from './accounts.js';
2
- export const CLAUDE_ROUTING_COMMAND_NAME = 'claude-routing';
3
- export const ROUTING_MODES = [
4
- 'main-first',
5
- 'fallback-first',
6
- 'sticky-balanced',
7
- ];
8
- export const DEFAULT_ROUTING_MODE = 'main-first';
9
- const ROUTING_USAGE = 'Usage: `/claude-routing`, `/claude-routing main-first`, `/claude-routing fallback-first`, `/claude-routing sticky-balanced`, or `/claude-routing reset`.';
10
- function normalizeRoutingMode(value) {
11
- return typeof value === 'string' &&
12
- ROUTING_MODES.includes(value)
13
- ? value
14
- : DEFAULT_ROUTING_MODE;
15
- }
16
- export function getRoutingMode(storage) {
17
- return normalizeRoutingMode(storage?.routing?.mode);
18
- }
19
- export async function setRoutingMode(mode, path = getAccountStoragePath()) {
20
- const storage = (await loadAccounts(path)) ?? createEmptyStorage();
21
- storage.routing = {
22
- ...(storage.routing ?? {}),
23
- mode,
24
- };
25
- await saveAccounts(storage, path);
26
- return storage;
27
- }
28
- export function parseRoutingCommandAction(argumentsText) {
29
- const tokens = argumentsText.trim().toLowerCase().split(/\s+/).filter(Boolean);
30
- if (tokens.length === 0)
31
- return { type: 'status' };
32
- if (tokens.length === 1 && tokens[0] === 'reset')
33
- return { type: 'reset' };
34
- if (tokens.length === 1 && ROUTING_MODES.includes(tokens[0])) {
35
- return { type: 'mode', mode: tokens[0] };
36
- }
37
- if (tokens.length === 2 &&
38
- tokens[0] === 'mode' &&
39
- ROUTING_MODES.includes(tokens[1])) {
40
- return { type: 'mode', mode: tokens[1] };
41
- }
42
- return { type: 'usage' };
43
- }
44
- function routingDescription(mode) {
45
- if (mode === 'fallback-first') {
46
- return 'Try usable fallback accounts before the main account. If no fallback succeeds, try the main account.';
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
- }
51
- return 'Try the main account first. Use fallback accounts only when quota policy or fallback errors require it.';
52
- }
53
- export function buildRoutingStatusSummary(input) {
54
- const mode = input?.mode ?? DEFAULT_ROUTING_MODE;
55
- return [
56
- '## Claude Routing Status',
57
- '',
58
- `- Mode: \`${mode}\``,
59
- `- Behavior: ${routingDescription(mode)}`,
60
- '',
61
- ROUTING_USAGE,
62
- ].join('\n');
63
- }
64
- export function executeRoutingCommand(input) {
65
- const action = parseRoutingCommandAction(input.argumentsText);
66
- const mode = input.mode ?? DEFAULT_ROUTING_MODE;
67
- if (action.type === 'status')
68
- return buildRoutingStatusSummary({ mode });
69
- if (action.type === 'mode') {
70
- return [
71
- '## Claude Routing Updated',
72
- '',
73
- `Mode updated to \`${action.mode}\`.`,
74
- '',
75
- ...buildRoutingStatusSummary({ mode: action.mode }).split('\n').slice(2),
76
- ].join('\n');
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
- }
87
- return [
88
- '## Claude Routing Usage',
89
- '',
90
- ROUTING_USAGE,
91
- '',
92
- ...buildRoutingStatusSummary({ mode }).split('\n').slice(2),
93
- ].join('\n');
94
- }
package/dist/start.js DELETED
@@ -1,29 +0,0 @@
1
- export const CLAUDE_START_COMMAND_NAME = 'claude-start';
2
- const START_STATUS_TITLE = '## Claude Lane Start Status';
3
- const START_QUEUED_TITLE = '## Claude Lane Start Queued';
4
- const START_USAGE_TITLE = '## Claude Lane Start Usage';
5
- const START_USAGE = 'Usage: `/claude-start`.';
6
- export function parseLaneStartCommandAction(input) {
7
- const normalized = input.trim().split(/\s+/).filter(Boolean);
8
- if (normalized.length === 0)
9
- return { type: 'fire' };
10
- return { type: 'usage' };
11
- }
12
- export function executeLaneStartCommand(input) {
13
- const action = parseLaneStartCommandAction(input.argumentsText);
14
- if (action.type === 'fire') {
15
- return {
16
- action,
17
- text: [
18
- START_QUEUED_TITLE,
19
- '',
20
- '- Queued an explicit lane-start request.',
21
- '- This is a billed request: the one-token limit does not remove prompt-cache read or write charges.',
22
- ].join('\n'),
23
- };
24
- }
25
- return {
26
- action,
27
- text: [START_USAGE_TITLE, '', START_USAGE, '', START_STATUS_TITLE].join('\n'),
28
- };
29
- }
@@ -1,523 +0,0 @@
1
- import { createHash, randomUUID } from 'node:crypto';
2
- import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
- import { basename, dirname, join } from 'node:path';
4
- import { acquireRefreshFileLock, getKillswitchThresholdsForAccount, getQuotaCheckIntervalMs, getQuotaMinimumRemainingThresholds, getScopedQuotaWindowForModel, isKillswitchEnabled, isPermanentRefreshError, killswitchRetryAfterSeconds, quotaSnapshotPassesModelScope, } from './accounts.js';
5
- import { isClaudeFableOrMythos5Model } from './models.js';
6
- export const STICKY_ROUTING_MAIN_ACCOUNT_ID = 'main';
7
- export const STICKY_ROUTING_SHORT_RESET_GRACE_MS = 15 * 60_000;
8
- export const STICKY_ROUTING_ASSIGNMENT_TTL_MS = 7 * 24 * 60 * 60_000;
9
- const STICKY_ROUTING_TOUCH_INTERVAL_MS = 60 * 60_000;
10
- const STICKY_ROUTING_LOCK_TTL_MS = 10_000;
11
- const STICKY_ROUTING_LOCK_WAIT_MS = 5_000;
12
- const STICKY_ROUTING_LOCK_POLL_MS = 10;
13
- const STICKY_ROUTING_MAX_ASSIGNMENTS = 4_096;
14
- const MIN_WEIGHT = 0.000_001;
15
- /**
16
- * Build the terminal response for a complete sticky quota pool that has no
17
- * eligible route. This prevents callers from silently falling through to the
18
- * main account after the router deliberately excluded it.
19
- */
20
- export function createStickyNoRouteResponse(input) {
21
- const modelId = input.modelId;
22
- const modelName = modelId
23
- ? input.routeQuotas
24
- .map((quota) => getScopedQuotaWindowForModel(quota, modelId))
25
- .find(Boolean)?.modelName
26
- : undefined;
27
- let authenticationMessage;
28
- if (isPermanentRefreshError(input.mainRefreshError)) {
29
- authenticationMessage = modelName
30
- ? `Main Claude OAuth account requires re-login, and no fallback OAuth account is currently routable for ${modelName}.`
31
- : 'Main Claude OAuth account requires re-login, and no fallback OAuth account is currently routable.';
32
- }
33
- else if (input.fallbackReauthLabels?.length) {
34
- const scope = modelName ? ` for ${modelName}` : '';
35
- authenticationMessage = `No OAuth account is currently routable${scope}. Fallback Claude OAuth accounts require re-login: ${input.fallbackReauthLabels.join(', ')}.`;
36
- }
37
- if (authenticationMessage) {
38
- return new Response(JSON.stringify({
39
- type: 'error',
40
- error: {
41
- type: 'authentication_error',
42
- message: authenticationMessage,
43
- },
44
- }), {
45
- status: 401,
46
- headers: { 'content-type': 'application/json' },
47
- });
48
- }
49
- const scopedBlocked = Boolean(modelId &&
50
- input.routeQuotas.length > 0 &&
51
- input.routeQuotas.every((quota) => !quotaSnapshotPassesModelScope(quota, modelId)));
52
- const [firstQuota, ...remainingQuotas] = input.routeQuotas;
53
- const retryAfter = killswitchRetryAfterSeconds(firstQuota, remainingQuotas.map((quota) => ({ quota })), input.now ?? Date.now(), scopedBlocked ? modelId : undefined);
54
- const minutes = Math.floor(retryAfter / 60);
55
- const seconds = retryAfter % 60;
56
- const reason = scopedBlocked && modelName
57
- ? `${modelName} weekly limit reached, no routable OAuth accounts.`
58
- : 'No OAuth account currently satisfies sticky-balanced quota policy.';
59
- const message = `${reason} Retry in ${minutes}m ${seconds}s.`;
60
- return new Response(JSON.stringify({
61
- type: 'error',
62
- error: { type: 'rate_limit_error', message },
63
- }), {
64
- status: 429,
65
- headers: {
66
- 'content-type': 'application/json',
67
- 'retry-after': String(retryAfter),
68
- },
69
- });
70
- }
71
- function isRecord(value) {
72
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
73
- }
74
- function finiteNumber(value) {
75
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
76
- }
77
- function normalizeAssignment(value) {
78
- if (!isRecord(value))
79
- return;
80
- if (typeof value.accountId !== 'string' ||
81
- !['fable', 'opus', 'general'].includes(String(value.family))) {
82
- return;
83
- }
84
- const assignedAt = finiteNumber(value.assignedAt);
85
- const lastSeenAt = finiteNumber(value.lastSeenAt);
86
- const initialInputBytes = finiteNumber(value.initialInputBytes);
87
- const quotaCheckedAt = finiteNumber(value.quotaCheckedAt);
88
- if (assignedAt === undefined ||
89
- lastSeenAt === undefined ||
90
- initialInputBytes === undefined ||
91
- quotaCheckedAt === undefined) {
92
- return;
93
- }
94
- return {
95
- accountId: value.accountId,
96
- family: value.family,
97
- assignedAt,
98
- lastSeenAt,
99
- initialInputBytes: Math.max(0, initialInputBytes),
100
- quotaCheckedAt,
101
- };
102
- }
103
- function normalizeState(value) {
104
- if (!isRecord(value) || value.version !== 1 || !isRecord(value.assignments)) {
105
- return;
106
- }
107
- const assignments = {};
108
- for (const [key, assignment] of Object.entries(value.assignments)) {
109
- const normalized = normalizeAssignment(assignment);
110
- if (normalized)
111
- assignments[key] = normalized;
112
- }
113
- return {
114
- version: 1,
115
- updatedAt: finiteNumber(value.updatedAt) ?? 0,
116
- assignments,
117
- };
118
- }
119
- function emptyState(now) {
120
- return { version: 1, updatedAt: now, assignments: {} };
121
- }
122
- function sessionKey(sessionId) {
123
- return createHash('sha256').update(sessionId).digest('hex');
124
- }
125
- export function stickyQuotaSnapshotIsFresh(quota, storage, now = Date.now(), modelId) {
126
- const maxAge = getQuotaCheckIntervalMs(storage);
127
- const standardFresh = ['five_hour', 'seven_day'].every((key) => {
128
- const window = quota?.[key];
129
- return Boolean(window && now - window.checkedAt < maxAge);
130
- });
131
- if (!standardFresh)
132
- return false;
133
- const scoped = getScopedQuotaWindowForModel(quota, modelId);
134
- return !scoped || now - scoped.checkedAt < maxAge;
135
- }
136
- function snapshotCheckedAt(quota) {
137
- return Math.max(quota.checkedAt ?? 0, quota.five_hour?.checkedAt ?? 0, quota.seven_day?.checkedAt ?? 0, ...(quota.scoped ?? []).map((window) => window.checkedAt ?? 0));
138
- }
139
- function knownQuota(quota) {
140
- if (!quota)
141
- return undefined;
142
- if ('kind' in quota)
143
- return quota.kind === 'known' ? quota.quota : undefined;
144
- return quota;
145
- }
146
- function remainingThreshold(storage, accountId, key) {
147
- const quotaMinimum = getQuotaMinimumRemainingThresholds(storage);
148
- const quotaThreshold = key === 'five_hour'
149
- ? quotaMinimum.five_hour
150
- : key === 'seven_day'
151
- ? quotaMinimum.seven_day
152
- : 0;
153
- if (!isKillswitchEnabled(storage))
154
- return quotaThreshold;
155
- const killswitch = getKillswitchThresholdsForAccount(storage, accountId === STICKY_ROUTING_MAIN_ACCOUNT_ID ? undefined : accountId);
156
- return Math.max(quotaThreshold, killswitch[key]);
157
- }
158
- function sustainableWindowWeight(remainingPercent, reservePercent, resetsAt, now) {
159
- const spendable = Math.max(0, remainingPercent - reservePercent);
160
- if (spendable <= 0)
161
- return 0;
162
- if (!resetsAt)
163
- return spendable;
164
- const resetAt = Date.parse(resetsAt);
165
- if (!Number.isFinite(resetAt) || resetAt <= now)
166
- return spendable;
167
- const hoursUntilReset = Math.max((resetAt - now) / 3_600_000, 1 / 60);
168
- return spendable / hoursUntilReset;
169
- }
170
- export function stickyRouteFamilyForModel(model) {
171
- if (isClaudeFableOrMythos5Model(model))
172
- return 'fable';
173
- return typeof model === 'string' && model.toLowerCase().includes('opus')
174
- ? 'opus'
175
- : 'general';
176
- }
177
- export function stickyRouteCandidateWeight(input) {
178
- const now = input.now ?? Date.now();
179
- const { candidate, storage } = input;
180
- const quota = knownQuota(candidate.quota);
181
- if (!quota)
182
- return 0;
183
- if (!stickyQuotaSnapshotIsFresh(quota, storage, now, input.modelId)) {
184
- return 0;
185
- }
186
- const weights = [];
187
- for (const key of ['five_hour', 'seven_day']) {
188
- const window = quota[key];
189
- if (!window || !Number.isFinite(window.remainingPercent))
190
- return 0;
191
- weights.push(sustainableWindowWeight(window.remainingPercent, remainingThreshold(storage, candidate.accountId, key), window.resetsAt, now));
192
- }
193
- if (input.family === 'fable') {
194
- const window = getScopedQuotaWindowForModel(quota, input.modelId);
195
- if (window && Number.isFinite(window.remainingPercent)) {
196
- weights.push(sustainableWindowWeight(window.remainingPercent, remainingThreshold(storage, candidate.accountId, 'scoped'), window.resetsAt, now));
197
- }
198
- }
199
- return Math.min(...weights);
200
- }
201
- export function stickyRouteKnownFableExhausted(candidate, storage, now = Date.now()) {
202
- const quota = knownQuota(candidate.quota);
203
- const window = getScopedQuotaWindowForModel(quota, 'claude-fable-5');
204
- return Boolean(window &&
205
- stickyQuotaSnapshotIsFresh(quota, storage, now, 'claude-fable-5') &&
206
- Number.isFinite(window.remainingPercent) &&
207
- window.remainingPercent <= 0);
208
- }
209
- export function decideStickyQuotaFailure(input) {
210
- const now = input.now ?? Date.now();
211
- const quota = knownQuota(input.quota);
212
- if (!quota)
213
- return { action: 'retain', reason: 'unknown' };
214
- const scoped = getScopedQuotaWindowForModel(quota, input.modelId);
215
- if (scoped &&
216
- Number.isFinite(scoped.remainingPercent) &&
217
- scoped.remainingPercent <= 0) {
218
- return { action: 'migrate', reason: 'model-scoped' };
219
- }
220
- const sevenDay = quota.seven_day;
221
- if (sevenDay &&
222
- Number.isFinite(sevenDay.remainingPercent) &&
223
- sevenDay.remainingPercent <= 0) {
224
- return { action: 'migrate', reason: 'seven-day' };
225
- }
226
- const fiveHour = quota.five_hour;
227
- if (fiveHour &&
228
- Number.isFinite(fiveHour.remainingPercent) &&
229
- fiveHour.remainingPercent <= 0) {
230
- const resetAt = fiveHour.resetsAt ? Date.parse(fiveHour.resetsAt) : NaN;
231
- if (Number.isFinite(resetAt) &&
232
- resetAt > now &&
233
- resetAt - now <= STICKY_ROUTING_SHORT_RESET_GRACE_MS) {
234
- return {
235
- action: 'hold',
236
- reason: 'five-hour-short-reset',
237
- retryAfterSeconds: Math.max(1, Math.ceil((resetAt - now) / 1000)),
238
- };
239
- }
240
- return { action: 'migrate', reason: 'five-hour' };
241
- }
242
- return { action: 'retain', reason: 'not-exhausted' };
243
- }
244
- export function getStickyRoutingStatePath(accountStoragePath) {
245
- const name = basename(accountStoragePath);
246
- const stem = name.endsWith('.json') ? name.slice(0, -5) : name;
247
- return join(dirname(accountStoragePath), `${stem}-routing-state.json`);
248
- }
249
- export function stickyRetryAfterWithJitter(sessionId, retryAfterSeconds) {
250
- const hash = createHash('sha256').update(sessionId).digest().readUInt32BE(0);
251
- return Math.max(1, Math.ceil(retryAfterSeconds)) + (hash % 21);
252
- }
253
- export class StickySessionRouter {
254
- options;
255
- loaded = false;
256
- state;
257
- stateMtimeNs;
258
- touchChain = Promise.resolve();
259
- constructor(options) {
260
- this.options = options;
261
- this.state = emptyState(this.now());
262
- }
263
- now() {
264
- return this.options.now?.() ?? Date.now();
265
- }
266
- async readState() {
267
- try {
268
- const raw = await readFile(this.options.path, 'utf8');
269
- const normalized = normalizeState(JSON.parse(raw));
270
- if (!normalized) {
271
- throw new Error(`Invalid sticky routing state at ${this.options.path}`);
272
- }
273
- return normalized;
274
- }
275
- catch (error) {
276
- if (error.code === 'ENOENT') {
277
- return emptyState(this.now());
278
- }
279
- throw error;
280
- }
281
- }
282
- async readStateMtimeNs() {
283
- try {
284
- return (await stat(this.options.path, { bigint: true })).mtimeNs;
285
- }
286
- catch (error) {
287
- if (error.code === 'ENOENT')
288
- return 0n;
289
- throw error;
290
- }
291
- }
292
- async refreshStateIfChanged() {
293
- for (let attempt = 0; attempt < 3; attempt++) {
294
- const before = await this.readStateMtimeNs();
295
- if (this.loaded && before === this.stateMtimeNs)
296
- return;
297
- const state = await this.readState();
298
- const after = await this.readStateMtimeNs();
299
- if (before !== after)
300
- continue;
301
- this.state = state;
302
- this.stateMtimeNs = after;
303
- this.loaded = true;
304
- return;
305
- }
306
- this.state = await this.readState();
307
- this.stateMtimeNs = await this.readStateMtimeNs();
308
- this.loaded = true;
309
- }
310
- async acquireLock() {
311
- await mkdir(dirname(this.options.path), { recursive: true, mode: 0o700 });
312
- const deadline = this.now() + STICKY_ROUTING_LOCK_WAIT_MS;
313
- while (true) {
314
- const lock = await acquireRefreshFileLock({
315
- name: 'write',
316
- path: this.options.path,
317
- ttlMs: STICKY_ROUTING_LOCK_TTL_MS,
318
- renew: true,
319
- });
320
- if (lock)
321
- return lock;
322
- if (this.now() >= deadline) {
323
- throw new Error('Timed out acquiring sticky routing state lock');
324
- }
325
- await new Promise((resolve) => setTimeout(resolve, STICKY_ROUTING_LOCK_POLL_MS));
326
- }
327
- }
328
- assignmentIsActive(assignment) {
329
- return (assignment.lastSeenAt >=
330
- this.now() -
331
- (this.options.assignmentTtlMs ?? STICKY_ROUTING_ASSIGNMENT_TTL_MS));
332
- }
333
- prune(state) {
334
- const cutoff = this.now() -
335
- (this.options.assignmentTtlMs ?? STICKY_ROUTING_ASSIGNMENT_TTL_MS);
336
- for (const [key, assignment] of Object.entries(state.assignments)) {
337
- if (assignment.lastSeenAt < cutoff)
338
- delete state.assignments[key];
339
- }
340
- const entries = Object.entries(state.assignments);
341
- if (entries.length <= STICKY_ROUTING_MAX_ASSIGNMENTS)
342
- return;
343
- entries
344
- .sort((left, right) => right[1].lastSeenAt - left[1].lastSeenAt)
345
- .slice(STICKY_ROUTING_MAX_ASSIGNMENTS)
346
- .forEach(([key]) => {
347
- delete state.assignments[key];
348
- });
349
- }
350
- async writeState(state) {
351
- this.prune(state);
352
- state.updatedAt = this.now();
353
- const tempPath = `${this.options.path}.${randomUUID()}.tmp`;
354
- try {
355
- await writeFile(tempPath, `${JSON.stringify(state)}\n`, { mode: 0o600 });
356
- await rename(tempPath, this.options.path);
357
- }
358
- finally {
359
- await rm(tempPath, { force: true }).catch(() => { });
360
- }
361
- this.state = state;
362
- this.stateMtimeNs = await this.readStateMtimeNs();
363
- this.loaded = true;
364
- }
365
- scheduleTouch(key) {
366
- this.touchChain = this.touchChain
367
- .catch(() => { })
368
- .then(async () => {
369
- const lock = await this.acquireLock();
370
- try {
371
- const state = await this.readState();
372
- const assignment = state.assignments[key];
373
- if (!assignment)
374
- return;
375
- assignment.lastSeenAt = this.now();
376
- await this.writeState(state);
377
- }
378
- finally {
379
- await lock.release();
380
- }
381
- })
382
- .catch(() => { });
383
- }
384
- selectCandidate(input) {
385
- let candidates = [...input.candidates];
386
- if (input.family === 'opus') {
387
- const depleted = candidates.filter((candidate) => stickyRouteKnownFableExhausted(candidate, input.storage, this.now()));
388
- if (depleted.length > 0)
389
- candidates = depleted;
390
- }
391
- const weighted = candidates.flatMap((candidate) => {
392
- const weight = stickyRouteCandidateWeight({
393
- candidate,
394
- family: input.family,
395
- modelId: input.modelId,
396
- storage: input.storage,
397
- now: this.now(),
398
- });
399
- return weight > 0 ? [{ candidate, weight }] : [];
400
- });
401
- if (weighted.length === 0 &&
402
- candidates.every((candidate) => !knownQuota(candidate.quota))) {
403
- return [...candidates].sort((left, right) => left.order - right.order ||
404
- left.accountId.localeCompare(right.accountId))[0];
405
- }
406
- const pendingBytes = new Map();
407
- for (const assignment of Object.values(input.state.assignments)) {
408
- const candidate = weighted.find((entry) => entry.candidate.accountId === assignment.accountId)?.candidate;
409
- if (!candidate)
410
- continue;
411
- const quota = knownQuota(candidate.quota);
412
- if (quota && assignment.quotaCheckedAt !== snapshotCheckedAt(quota))
413
- continue;
414
- pendingBytes.set(assignment.accountId, (pendingBytes.get(assignment.accountId) ?? 0) +
415
- assignment.initialInputBytes);
416
- }
417
- return weighted
418
- .map(({ candidate, weight }) => ({
419
- candidate,
420
- score: ((pendingBytes.get(candidate.accountId) ?? 0) + input.inputBytes) /
421
- Math.max(weight, MIN_WEIGHT),
422
- }))
423
- .sort((left, right) => left.score - right.score ||
424
- left.candidate.order - right.candidate.order ||
425
- left.candidate.accountId.localeCompare(right.candidate.accountId))[0]?.candidate;
426
- }
427
- async resolve(input) {
428
- if (!input.sessionId)
429
- return null;
430
- await this.refreshStateIfChanged();
431
- const key = sessionKey(input.sessionId);
432
- const cached = this.state.assignments[key];
433
- if (cached &&
434
- this.assignmentIsActive(cached) &&
435
- input.retainAccountIds.has(cached.accountId) &&
436
- !input.excludeAccountIds?.has(cached.accountId)) {
437
- if (this.now() - cached.lastSeenAt >= STICKY_ROUTING_TOUCH_INTERVAL_MS) {
438
- cached.lastSeenAt = this.now();
439
- this.scheduleTouch(key);
440
- }
441
- return {
442
- accountId: cached.accountId,
443
- assignment: { ...cached },
444
- created: false,
445
- migrated: false,
446
- };
447
- }
448
- const lock = await this.acquireLock();
449
- try {
450
- const state = await this.readState();
451
- this.prune(state);
452
- const current = state.assignments[key];
453
- if (current &&
454
- input.retainAccountIds.has(current.accountId) &&
455
- !input.excludeAccountIds?.has(current.accountId)) {
456
- current.lastSeenAt = this.now();
457
- await this.writeState(state);
458
- return {
459
- accountId: current.accountId,
460
- assignment: { ...current },
461
- created: false,
462
- migrated: false,
463
- };
464
- }
465
- const candidates = input.candidates.filter((candidate) => !input.excludeAccountIds?.has(candidate.accountId));
466
- const preferred = input.preferredAccountId
467
- ? candidates.find((candidate) => candidate.accountId === input.preferredAccountId &&
468
- stickyRouteCandidateWeight({
469
- candidate,
470
- family: input.family,
471
- modelId: input.modelId,
472
- storage: input.storage,
473
- now: this.now(),
474
- }) > 0)
475
- : undefined;
476
- const selected = preferred ?? this.selectCandidate({ ...input, candidates, state });
477
- if (!selected) {
478
- if (current) {
479
- delete state.assignments[key];
480
- await this.writeState(state);
481
- }
482
- return null;
483
- }
484
- const now = this.now();
485
- const selectedQuota = knownQuota(selected.quota);
486
- const assignment = {
487
- accountId: selected.accountId,
488
- family: input.family,
489
- assignedAt: now,
490
- lastSeenAt: now,
491
- initialInputBytes: Math.max(1, input.inputBytes),
492
- quotaCheckedAt: selectedQuota ? snapshotCheckedAt(selectedQuota) : 0,
493
- };
494
- state.assignments[key] = assignment;
495
- await this.writeState(state);
496
- return {
497
- accountId: selected.accountId,
498
- assignment: { ...assignment },
499
- created: !current,
500
- migrated: Boolean(current && current.accountId !== selected.accountId),
501
- };
502
- }
503
- finally {
504
- await lock.release();
505
- }
506
- }
507
- async clear(sessionId) {
508
- if (!sessionId)
509
- return;
510
- const key = sessionKey(sessionId);
511
- const lock = await this.acquireLock();
512
- try {
513
- const state = await this.readState();
514
- if (!(key in state.assignments))
515
- return;
516
- delete state.assignments[key];
517
- await this.writeState(state);
518
- }
519
- finally {
520
- await lock.release();
521
- }
522
- }
523
- }