@cortexkit/anthropic-auth-core 1.19.1 → 1.21.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/quotas.d.ts CHANGED
@@ -11,13 +11,13 @@ export type QuotaAccountSummary = {
11
11
  };
12
12
  export declare function formatQuotaMoney(money: QuotaMoney): string;
13
13
  export declare function buildFallbackQuotaSummaries(storage: AccountStorage | null, errors?: ReadonlyMap<string, string>): {
14
- error?: string | undefined;
15
- lastRefreshedAt: number | undefined;
16
- tierLabel?: string | undefined;
17
14
  name: string;
18
- role: "fallback";
15
+ role: 'fallback';
19
16
  enabled: boolean;
20
17
  quota: OAuthQuotaSnapshot | undefined;
18
+ tierLabel?: string | undefined;
19
+ lastRefreshedAt: number | undefined;
20
+ error?: string | undefined;
21
21
  }[];
22
22
  export declare function buildClaudeQuotaSummary(input: {
23
23
  accounts: QuotaAccountSummary[];
package/dist/quotas.js CHANGED
@@ -1,5 +1,5 @@
1
- import { isOAuthAccount } from "./accounts.js";
2
- import { formatOAuthAccountTier } from "./oauth-profile.js";
1
+ import { isOAuthAccount } from './accounts.js';
2
+ import { formatOAuthAccountTier } from './oauth-profile.js';
3
3
  export const CLAUDE_QUOTAS_COMMAND_NAME = 'claude-quota';
4
4
  const WINDOW_LABELS = {
5
5
  five_hour: '5h',
package/dist/relay.d.ts CHANGED
@@ -31,6 +31,8 @@ export declare function sendViaRelay(options: {
31
31
  * within an attempt are ignored.
32
32
  */
33
33
  onResponseHeaders?: (headers: Headers) => void;
34
+ dumpTag?: import('./dump.ts').DumpTag;
35
+ onDumpCreated?: (handle: import('./dump.ts').DumpHandle) => void;
34
36
  setTimeoutImpl?: typeof globalThis.setTimeout;
35
37
  clearTimeoutImpl?: typeof globalThis.clearTimeout;
36
38
  }): Promise<Response>;
package/dist/relay.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { createHash, randomBytes } from 'node:crypto';
3
- import { dumpRelayRequest } from "./dump.js";
4
- import { relayLog } from "./logger.js";
3
+ import { dumpRelayRequest } from './dump.js';
4
+ import { relayLog } from './logger.js';
5
5
  class RelayStateMismatchError extends Error {
6
6
  }
7
7
  class RelayWebSocketConnectionResetError extends Error {
@@ -752,7 +752,7 @@ function getPersistentRelaySession(config, affinity, timers) {
752
752
  return session;
753
753
  }
754
754
  export async function sendViaRelay(options) {
755
- const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, onResponseHeaders, setTimeoutImpl = globalThis.setTimeout, clearTimeoutImpl = globalThis.clearTimeout, } = options;
755
+ const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, onResponseHeaders, dumpTag, onDumpCreated, setTimeoutImpl = globalThis.setTimeout, clearTimeoutImpl = globalThis.clearTimeout, } = options;
756
756
  if (!config || !isRelayableAnthropicRequest(input, body))
757
757
  return fallback();
758
758
  const affinity = explicitAffinity ||
@@ -848,7 +848,7 @@ export async function sendViaRelay(options) {
848
848
  });
849
849
  relayLog(`used relay transport=${result.transport} protocol=${result.protocol} mode=${result.payload.mode} status=${result.response.status} session=${shortAffinity(affinity)} bodyBytes=${bodyText.length} relayBytes=${actualPayloadBytes}`);
850
850
  const dumpStart = perfNowMs();
851
- await dumpRelayRequest({
851
+ const dumpHandle = await dumpRelayRequest({
852
852
  affinity,
853
853
  transport: result.transport,
854
854
  protocol: result.protocol,
@@ -858,7 +858,13 @@ export async function sendViaRelay(options) {
858
858
  previousBodyText: previous?.body,
859
859
  payload: result.payload,
860
860
  relayBytes: actualPayloadBytes,
861
+ tag: dumpTag,
861
862
  });
863
+ try {
864
+ if (dumpHandle)
865
+ onDumpCreated?.(dumpHandle);
866
+ }
867
+ catch { }
862
868
  relayPerfLog('dump', {
863
869
  session: shortAffinity(affinity),
864
870
  ms: formatMs(perfNowMs() - dumpStart),
package/dist/routing.d.ts CHANGED
@@ -1,6 +1,6 @@
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", "sticky-balanced"];
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';
package/dist/routing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createEmptyStorage, getAccountStoragePath, loadAccounts, saveAccounts, } from "./accounts.js";
1
+ import { createEmptyStorage, getAccountStoragePath, loadAccounts, saveAccounts, } from './accounts.js';
2
2
  export const CLAUDE_ROUTING_COMMAND_NAME = 'claude-routing';
3
3
  export const ROUTING_MODES = [
4
4
  'main-first',
@@ -0,0 +1,13 @@
1
+ export declare const CLAUDE_START_COMMAND_NAME = "claude-start";
2
+ export type LaneStartCommandAction = {
3
+ type: 'fire';
4
+ } | {
5
+ type: 'usage';
6
+ };
7
+ export declare function parseLaneStartCommandAction(input: string): LaneStartCommandAction;
8
+ export declare function executeLaneStartCommand(input: {
9
+ argumentsText: string;
10
+ }): {
11
+ action: LaneStartCommandAction;
12
+ text: string;
13
+ };
package/dist/start.js ADDED
@@ -0,0 +1,29 @@
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
+ }
@@ -3,9 +3,21 @@ export declare const STICKY_ROUTING_MAIN_ACCOUNT_ID = "main";
3
3
  export declare const STICKY_ROUTING_SHORT_RESET_GRACE_MS: number;
4
4
  export declare const STICKY_ROUTING_ASSIGNMENT_TTL_MS: number;
5
5
  export type StickyRouteFamily = 'fable' | 'opus' | 'general';
6
- export type StickyRouteCandidate = {
6
+ export type IdentityState = {
7
+ kind: 'known';
7
8
  accountId: string;
9
+ } | {
10
+ kind: 'unknown';
11
+ };
12
+ export type QuotaState = {
13
+ kind: 'known';
8
14
  quota: OAuthQuotaSnapshot;
15
+ } | {
16
+ kind: 'unknown';
17
+ };
18
+ export type StickyRouteCandidate = {
19
+ accountId: string;
20
+ quota: OAuthQuotaSnapshot | QuotaState;
9
21
  order: number;
10
22
  };
11
23
  export type StickyRouteAssignment = {
@@ -40,6 +52,7 @@ export type StickyQuotaFailureDecision = {
40
52
  */
41
53
  export declare function createStickyNoRouteResponse(input: {
42
54
  mainRefreshError?: AccountOperationError;
55
+ fallbackReauthLabels?: readonly string[];
43
56
  routeQuotas: readonly OAuthQuotaSnapshot[];
44
57
  modelId?: string;
45
58
  now?: number;
@@ -55,7 +68,7 @@ export declare function stickyRouteCandidateWeight(input: {
55
68
  }): number;
56
69
  export declare function stickyRouteKnownFableExhausted(candidate: StickyRouteCandidate, storage: AccountStorage | null, now?: number): boolean;
57
70
  export declare function decideStickyQuotaFailure(input: {
58
- quota: OAuthQuotaSnapshot | undefined;
71
+ quota: OAuthQuotaSnapshot | QuotaState | undefined;
59
72
  modelId?: string;
60
73
  now?: number;
61
74
  }): StickyQuotaFailureDecision;
@@ -1,8 +1,8 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
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";
4
+ import { acquireRefreshFileLock, getKillswitchThresholdsForAccount, getQuotaCheckIntervalMs, getQuotaMinimumRemainingThresholds, getScopedQuotaWindowForModel, isKillswitchEnabled, isPermanentRefreshError, killswitchRetryAfterSeconds, quotaSnapshotPassesModelScope, } from './accounts.js';
5
+ import { isClaudeFableOrMythos5Model } from './models.js';
6
6
  export const STICKY_ROUTING_MAIN_ACCOUNT_ID = 'main';
7
7
  export const STICKY_ROUTING_SHORT_RESET_GRACE_MS = 15 * 60_000;
8
8
  export const STICKY_ROUTING_ASSIGNMENT_TTL_MS = 7 * 24 * 60 * 60_000;
@@ -24,13 +24,23 @@ export function createStickyNoRouteResponse(input) {
24
24
  .map((quota) => getScopedQuotaWindowForModel(quota, modelId))
25
25
  .find(Boolean)?.modelName
26
26
  : undefined;
27
+ let authenticationMessage;
27
28
  if (isPermanentRefreshError(input.mainRefreshError)) {
28
- const message = modelName
29
+ authenticationMessage = modelName
29
30
  ? `Main Claude OAuth account requires re-login, and no fallback OAuth account is currently routable for ${modelName}.`
30
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) {
31
38
  return new Response(JSON.stringify({
32
39
  type: 'error',
33
- error: { type: 'authentication_error', message },
40
+ error: {
41
+ type: 'authentication_error',
42
+ message: authenticationMessage,
43
+ },
34
44
  }), {
35
45
  status: 401,
36
46
  headers: { 'content-type': 'application/json' },
@@ -126,6 +136,13 @@ export function stickyQuotaSnapshotIsFresh(quota, storage, now = Date.now(), mod
126
136
  function snapshotCheckedAt(quota) {
127
137
  return Math.max(quota.checkedAt ?? 0, quota.five_hour?.checkedAt ?? 0, quota.seven_day?.checkedAt ?? 0, ...(quota.scoped ?? []).map((window) => window.checkedAt ?? 0));
128
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
+ }
129
146
  function remainingThreshold(storage, accountId, key) {
130
147
  const quotaMinimum = getQuotaMinimumRemainingThresholds(storage);
131
148
  const quotaThreshold = key === 'five_hour'
@@ -160,18 +177,21 @@ export function stickyRouteFamilyForModel(model) {
160
177
  export function stickyRouteCandidateWeight(input) {
161
178
  const now = input.now ?? Date.now();
162
179
  const { candidate, storage } = input;
163
- if (!stickyQuotaSnapshotIsFresh(candidate.quota, storage, now, input.modelId)) {
180
+ const quota = knownQuota(candidate.quota);
181
+ if (!quota)
182
+ return 0;
183
+ if (!stickyQuotaSnapshotIsFresh(quota, storage, now, input.modelId)) {
164
184
  return 0;
165
185
  }
166
186
  const weights = [];
167
187
  for (const key of ['five_hour', 'seven_day']) {
168
- const window = candidate.quota[key];
188
+ const window = quota[key];
169
189
  if (!window || !Number.isFinite(window.remainingPercent))
170
190
  return 0;
171
191
  weights.push(sustainableWindowWeight(window.remainingPercent, remainingThreshold(storage, candidate.accountId, key), window.resetsAt, now));
172
192
  }
173
193
  if (input.family === 'fable') {
174
- const window = getScopedQuotaWindowForModel(candidate.quota, input.modelId);
194
+ const window = getScopedQuotaWindowForModel(quota, input.modelId);
175
195
  if (window && Number.isFinite(window.remainingPercent)) {
176
196
  weights.push(sustainableWindowWeight(window.remainingPercent, remainingThreshold(storage, candidate.accountId, 'scoped'), window.resetsAt, now));
177
197
  }
@@ -179,29 +199,31 @@ export function stickyRouteCandidateWeight(input) {
179
199
  return Math.min(...weights);
180
200
  }
181
201
  export function stickyRouteKnownFableExhausted(candidate, storage, now = Date.now()) {
182
- const window = getScopedQuotaWindowForModel(candidate.quota, 'claude-fable-5');
202
+ const quota = knownQuota(candidate.quota);
203
+ const window = getScopedQuotaWindowForModel(quota, 'claude-fable-5');
183
204
  return Boolean(window &&
184
- stickyQuotaSnapshotIsFresh(candidate.quota, storage, now, 'claude-fable-5') &&
205
+ stickyQuotaSnapshotIsFresh(quota, storage, now, 'claude-fable-5') &&
185
206
  Number.isFinite(window.remainingPercent) &&
186
207
  window.remainingPercent <= 0);
187
208
  }
188
209
  export function decideStickyQuotaFailure(input) {
189
210
  const now = input.now ?? Date.now();
190
- if (!input.quota)
211
+ const quota = knownQuota(input.quota);
212
+ if (!quota)
191
213
  return { action: 'retain', reason: 'unknown' };
192
- const scoped = getScopedQuotaWindowForModel(input.quota, input.modelId);
214
+ const scoped = getScopedQuotaWindowForModel(quota, input.modelId);
193
215
  if (scoped &&
194
216
  Number.isFinite(scoped.remainingPercent) &&
195
217
  scoped.remainingPercent <= 0) {
196
218
  return { action: 'migrate', reason: 'model-scoped' };
197
219
  }
198
- const sevenDay = input.quota.seven_day;
220
+ const sevenDay = quota.seven_day;
199
221
  if (sevenDay &&
200
222
  Number.isFinite(sevenDay.remainingPercent) &&
201
223
  sevenDay.remainingPercent <= 0) {
202
224
  return { action: 'migrate', reason: 'seven-day' };
203
225
  }
204
- const fiveHour = input.quota.five_hour;
226
+ const fiveHour = quota.five_hour;
205
227
  if (fiveHour &&
206
228
  Number.isFinite(fiveHour.remainingPercent) &&
207
229
  fiveHour.remainingPercent <= 0) {
@@ -376,14 +398,18 @@ export class StickySessionRouter {
376
398
  });
377
399
  return weight > 0 ? [{ candidate, weight }] : [];
378
400
  });
379
- if (weighted.length === 0)
380
- return undefined;
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
+ }
381
406
  const pendingBytes = new Map();
382
407
  for (const assignment of Object.values(input.state.assignments)) {
383
408
  const candidate = weighted.find((entry) => entry.candidate.accountId === assignment.accountId)?.candidate;
384
409
  if (!candidate)
385
410
  continue;
386
- if (assignment.quotaCheckedAt !== snapshotCheckedAt(candidate.quota))
411
+ const quota = knownQuota(candidate.quota);
412
+ if (quota && assignment.quotaCheckedAt !== snapshotCheckedAt(quota))
387
413
  continue;
388
414
  pendingBytes.set(assignment.accountId, (pendingBytes.get(assignment.accountId) ?? 0) +
389
415
  assignment.initialInputBytes);
@@ -456,13 +482,14 @@ export class StickySessionRouter {
456
482
  return null;
457
483
  }
458
484
  const now = this.now();
485
+ const selectedQuota = knownQuota(selected.quota);
459
486
  const assignment = {
460
487
  accountId: selected.accountId,
461
488
  family: input.family,
462
489
  assignedAt: now,
463
490
  lastSeenAt: now,
464
491
  initialInputBytes: Math.max(1, input.inputBytes),
465
- quotaCheckedAt: snapshotCheckedAt(selected.quota),
492
+ quotaCheckedAt: selectedQuota ? snapshotCheckedAt(selectedQuota) : 0,
466
493
  };
467
494
  state.assignments[key] = assignment;
468
495
  await this.writeState(state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.19.1",
3
+ "version": "1.21.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",