@cortexkit/anthropic-auth-core 1.7.0 → 1.8.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.
@@ -1,22 +1,35 @@
1
1
  import { type Cache1hMode } from './constants.ts';
2
2
  export declare const ACCOUNT_FILE_NAME = "anthropic-auth.json";
3
+ export declare const ACCOUNT_STATE_FILE_NAME = "anthropic-auth-state.json";
3
4
  export declare const QUOTA_URL = "https://api.anthropic.com/api/oauth/usage";
4
5
  export type QuotaWindowName = 'five_hour' | 'seven_day';
5
- export type OAuthAccount = {
6
+ export type AccountBase = {
6
7
  id: string;
7
8
  label?: string;
9
+ enabled?: boolean;
10
+ addedAt?: number;
11
+ lastUsed?: number;
12
+ };
13
+ export type OAuthAccount = AccountBase & {
8
14
  type: 'oauth';
9
15
  access?: string;
10
16
  refresh: string;
11
17
  expires?: number;
12
- enabled?: boolean;
13
- addedAt?: number;
14
- lastUsed?: number;
15
18
  lastRefreshedAt?: number;
16
19
  lastRefreshError?: AccountOperationError;
17
20
  lastQuotaRefreshError?: AccountOperationError;
18
21
  quota?: Partial<Record<QuotaWindowName, AccountQuotaWindow>>;
19
22
  };
23
+ export type ApiKeyAccount = AccountBase & {
24
+ type: 'api';
25
+ apiKey?: string;
26
+ baseURL: string;
27
+ authHeader?: 'authorization-bearer' | 'x-api-key';
28
+ };
29
+ export type FallbackAccount = OAuthAccount | ApiKeyAccount;
30
+ export declare function isOAuthAccount(account: FallbackAccount): account is OAuthAccount;
31
+ export declare function isApiKeyAccount(account: FallbackAccount): account is ApiKeyAccount;
32
+ export declare function isValidApiBaseURL(value: string | undefined): boolean;
20
33
  export type AccountOperationError = {
21
34
  message: string;
22
35
  checkedAt: number;
@@ -94,7 +107,27 @@ export type AccountStorage = {
94
107
  transport?: 'http' | 'websocket';
95
108
  };
96
109
  killswitch?: KillswitchConfig;
97
- accounts: OAuthAccount[];
110
+ accounts: FallbackAccount[];
111
+ };
112
+ export type AccountRuntimeEntry = Partial<Pick<OAuthAccount, 'access' | 'refresh' | 'expires' | 'lastUsed' | 'lastRefreshedAt' | 'lastRefreshError' | 'lastQuotaRefreshError' | 'quota'> & Pick<ApiKeyAccount, 'apiKey' | 'lastUsed'>>;
113
+ export type AccountRuntimeState = {
114
+ version: 1;
115
+ main?: {
116
+ quota?: OAuthQuotaSnapshot;
117
+ quotaCheckedAt?: number;
118
+ quotaToken?: string;
119
+ lastQuotaApiError?: AccountOperationError;
120
+ lastRefreshError?: AccountOperationError;
121
+ refreshLeaseId?: string;
122
+ refreshLeaseUntil?: number;
123
+ refreshLeaseTokenHash?: string;
124
+ };
125
+ accounts?: Record<string, AccountRuntimeEntry>;
126
+ };
127
+ export type AccountStateSaveScope = {
128
+ mainQuota?: boolean;
129
+ mainRefresh?: boolean;
130
+ accounts?: true | string[];
98
131
  };
99
132
  export type OAuthQuotaSnapshot = Partial<Record<QuotaWindowName, AccountQuotaWindow>>;
100
133
  export type AccountManagerOptions = {
@@ -109,8 +142,10 @@ export type AccountRefreshError = {
109
142
  message: string;
110
143
  };
111
144
  export declare function getAccountStoragePath(): string;
145
+ export declare function getAccountStatePath(configPath?: string): string;
112
146
  export declare function loadAccounts(path?: string): Promise<AccountStorage | null>;
113
147
  export declare function saveAccounts(storage: AccountStorage, path?: string): Promise<void>;
148
+ export declare function saveAccountState(storage: AccountStorage, path?: string, scope?: AccountStateSaveScope): Promise<void>;
114
149
  export declare function acquireRefreshFileLock(options: {
115
150
  name: string;
116
151
  ttlMs: number;
@@ -202,11 +237,11 @@ export declare class FallbackAccountManager {
202
237
  */
203
238
  private seedFallbackQuota;
204
239
  load(): Promise<AccountStorage | null>;
205
- save(storage: AccountStorage): Promise<void>;
240
+ save(storage: AccountStorage, accountIds?: string[]): Promise<void>;
206
241
  startBackgroundRefresh(): void;
207
242
  stopBackgroundRefresh(): void;
208
243
  getUsableFallbackAccounts(existingStorage?: AccountStorage | null): Promise<OAuthAccount[]>;
209
- markUsed(account: OAuthAccount): Promise<void>;
244
+ markUsed(account: FallbackAccount): Promise<void>;
210
245
  accountPassesQuotaPolicy(account: OAuthAccount, storage: AccountStorage | null): boolean;
211
246
  /**
212
247
  * Return the account with its quota overlaid from the unified QuotaManager
package/dist/accounts.js CHANGED
@@ -8,7 +8,28 @@ import { log } from "./logger.js";
8
8
  const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
9
9
  const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
10
10
  export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
11
+ export const ACCOUNT_STATE_FILE_NAME = 'anthropic-auth-state.json';
11
12
  export const QUOTA_URL = 'https://api.anthropic.com/api/oauth/usage';
13
+ export function isOAuthAccount(account) {
14
+ return account.type === 'oauth';
15
+ }
16
+ export function isApiKeyAccount(account) {
17
+ return account.type === 'api';
18
+ }
19
+ export function isValidApiBaseURL(value) {
20
+ const raw = value?.trim();
21
+ if (!raw)
22
+ return false;
23
+ try {
24
+ const url = new URL(raw);
25
+ return ((url.protocol === 'http:' || url.protocol === 'https:') &&
26
+ !url.username &&
27
+ !url.password);
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
12
33
  const DEFAULT_FALLBACK_ON = [401, 403, 429];
13
34
  const MIN_REFRESH_BEFORE_EXPIRY_MINUTES = 240;
14
35
  const DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES = MIN_REFRESH_BEFORE_EXPIRY_MINUTES;
@@ -40,28 +61,55 @@ export function getAccountStoragePath() {
40
61
  return (process.env.OPENCODE_ANTHROPIC_AUTH_FILE?.trim() ||
41
62
  join(getConfigDir(), ACCOUNT_FILE_NAME));
42
63
  }
64
+ export function getAccountStatePath(configPath = getAccountStoragePath()) {
65
+ const explicit = process.env.OPENCODE_ANTHROPIC_AUTH_STATE_FILE?.trim();
66
+ if (explicit)
67
+ return explicit;
68
+ return configPath.endsWith(ACCOUNT_FILE_NAME)
69
+ ? join(dirname(configPath), ACCOUNT_STATE_FILE_NAME)
70
+ : `${configPath}.state.json`;
71
+ }
43
72
  function isRecord(value) {
44
73
  return value != null && typeof value === 'object' && !Array.isArray(value);
45
74
  }
75
+ function normalizeAccountBase(value) {
76
+ return {
77
+ id: typeof value.id === 'string' && value.id.trim()
78
+ ? value.id.trim()
79
+ : randomUUID(),
80
+ label: typeof value.label === 'string' ? value.label : undefined,
81
+ enabled: typeof value.enabled === 'boolean' ? value.enabled : undefined,
82
+ addedAt: typeof value.addedAt === 'number' ? value.addedAt : undefined,
83
+ lastUsed: typeof value.lastUsed === 'number' ? value.lastUsed : undefined,
84
+ };
85
+ }
46
86
  function normalizeAccount(value) {
47
87
  if (!isRecord(value))
48
88
  return null;
89
+ if (value.type === 'api') {
90
+ const baseURL = typeof value.baseURL === 'string' ? value.baseURL.trim() : '';
91
+ const apiKey = typeof value.apiKey === 'string' ? value.apiKey.trim() : '';
92
+ if (!isValidApiBaseURL(baseURL))
93
+ return null;
94
+ const authHeader = value.authHeader === 'x-api-key' ? 'x-api-key' : 'authorization-bearer';
95
+ return {
96
+ ...normalizeAccountBase(value),
97
+ type: 'api',
98
+ apiKey: apiKey || undefined,
99
+ baseURL,
100
+ authHeader,
101
+ };
102
+ }
49
103
  if (value.type !== 'oauth')
50
104
  return null;
51
105
  if (typeof value.refresh !== 'string' || !value.refresh.trim())
52
106
  return null;
53
107
  return {
54
- id: typeof value.id === 'string' && value.id.trim()
55
- ? value.id.trim()
56
- : randomUUID(),
57
- label: typeof value.label === 'string' ? value.label : undefined,
108
+ ...normalizeAccountBase(value),
58
109
  type: 'oauth',
59
110
  access: typeof value.access === 'string' ? value.access : undefined,
60
111
  refresh: value.refresh,
61
112
  expires: typeof value.expires === 'number' ? value.expires : undefined,
62
- enabled: typeof value.enabled === 'boolean' ? value.enabled : undefined,
63
- addedAt: typeof value.addedAt === 'number' ? value.addedAt : undefined,
64
- lastUsed: typeof value.lastUsed === 'number' ? value.lastUsed : undefined,
65
113
  lastRefreshedAt: typeof value.lastRefreshedAt === 'number'
66
114
  ? value.lastRefreshedAt
67
115
  : undefined,
@@ -136,41 +184,226 @@ function normalizeStorage(value) {
136
184
  .filter((account) => account != null),
137
185
  };
138
186
  }
139
- export async function loadAccounts(path = getAccountStoragePath()) {
187
+ async function readJsonIfPresent(path) {
140
188
  try {
141
- const raw = await readFile(path, 'utf8');
142
- return normalizeStorage(JSON.parse(raw));
189
+ return { exists: true, value: JSON.parse(await readFile(path, 'utf8')) };
143
190
  }
144
191
  catch (error) {
145
- if (error.code === 'ENOENT')
146
- return null;
147
- return null;
192
+ if (error.code === 'ENOENT') {
193
+ return { exists: false, value: null };
194
+ }
195
+ return { exists: false, value: null };
148
196
  }
149
197
  }
198
+ function objectWithDefinedEntries(value) {
199
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
200
+ }
201
+ function mergeConfigAndState(configValue, stateValue) {
202
+ if (!isRecord(configValue))
203
+ return configValue;
204
+ const state = isRecord(stateValue) ? stateValue : {};
205
+ const mainState = isRecord(state.main) ? state.main : undefined;
206
+ const stateAccounts = isRecord(state.accounts) ? state.accounts : {};
207
+ const quotaConfig = isRecord(configValue.quota) ? configValue.quota : {};
208
+ const refreshConfig = isRecord(configValue.refresh) ? configValue.refresh : {};
209
+ const mainQuotaSource = mainState ?? quotaConfig;
210
+ const mainRefreshSource = mainState ?? refreshConfig;
211
+ const accounts = Array.isArray(configValue.accounts)
212
+ ? configValue.accounts.map((account) => {
213
+ if (!isRecord(account))
214
+ return account;
215
+ const stateAccount = typeof account.id === 'string' && isRecord(stateAccounts[account.id])
216
+ ? stateAccounts[account.id]
217
+ : {};
218
+ return { ...account, ...stateAccount };
219
+ })
220
+ : [];
221
+ return {
222
+ ...configValue,
223
+ refresh: objectWithDefinedEntries({
224
+ ...refreshConfig,
225
+ mainLastRefreshError: mainRefreshSource.lastRefreshError,
226
+ mainRefreshLeaseId: mainRefreshSource.refreshLeaseId,
227
+ mainRefreshLeaseUntil: mainRefreshSource.refreshLeaseUntil,
228
+ mainRefreshLeaseTokenHash: mainRefreshSource.refreshLeaseTokenHash,
229
+ }),
230
+ quota: objectWithDefinedEntries({
231
+ ...quotaConfig,
232
+ mainQuota: mainQuotaSource.quota,
233
+ mainQuotaCheckedAt: mainQuotaSource.quotaCheckedAt,
234
+ mainQuotaToken: mainQuotaSource.quotaToken,
235
+ mainLastQuotaApiError: mainQuotaSource.lastQuotaApiError,
236
+ }),
237
+ accounts,
238
+ };
239
+ }
240
+ export async function loadAccounts(path = getAccountStoragePath()) {
241
+ const config = await readJsonIfPresent(path);
242
+ if (!config.exists)
243
+ return null;
244
+ const state = await readJsonIfPresent(getAccountStatePath(path));
245
+ return normalizeStorage(mergeConfigAndState(config.value, state.value));
246
+ }
150
247
  async function loadExistingTopLevelFields(path) {
151
- try {
152
- const raw = await readFile(path, 'utf8');
153
- const parsed = JSON.parse(raw);
154
- return isRecord(parsed) ? parsed : {};
155
- }
156
- catch {
157
- return {};
158
- }
248
+ const existing = await readJsonIfPresent(path);
249
+ return isRecord(existing.value) ? existing.value : {};
159
250
  }
160
251
  function omitUndefinedTopLevel(value) {
161
252
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
162
253
  }
163
- export async function saveAccounts(storage, path = getAccountStoragePath()) {
254
+ function accountConfig(account) {
255
+ return objectWithDefinedEntries({
256
+ id: account.id,
257
+ label: account.label,
258
+ type: account.type,
259
+ enabled: account.enabled,
260
+ addedAt: account.addedAt,
261
+ baseURL: account.type === 'api' ? account.baseURL : undefined,
262
+ authHeader: account.type === 'api' ? account.authHeader : undefined,
263
+ });
264
+ }
265
+ function accountRuntimeState(account) {
266
+ if (account.type === 'api') {
267
+ return objectWithDefinedEntries({
268
+ apiKey: account.apiKey,
269
+ lastUsed: account.lastUsed,
270
+ });
271
+ }
272
+ return objectWithDefinedEntries({
273
+ access: account.access,
274
+ refresh: account.refresh,
275
+ expires: account.expires,
276
+ lastUsed: account.lastUsed,
277
+ lastRefreshedAt: account.lastRefreshedAt,
278
+ lastRefreshError: account.lastRefreshError,
279
+ lastQuotaRefreshError: account.lastQuotaRefreshError,
280
+ quota: account.quota,
281
+ });
282
+ }
283
+ function configFromStorage(storage) {
284
+ const refresh = storage.refresh
285
+ ? objectWithDefinedEntries({
286
+ enabled: storage.refresh.enabled,
287
+ intervalMinutes: storage.refresh.intervalMinutes,
288
+ refreshBeforeExpiryMinutes: storage.refresh.refreshBeforeExpiryMinutes,
289
+ })
290
+ : undefined;
291
+ const quota = storage.quota
292
+ ? objectWithDefinedEntries({
293
+ enabled: storage.quota.enabled,
294
+ checkIntervalMinutes: storage.quota.checkIntervalMinutes,
295
+ refreshEveryNRequests: storage.quota.refreshEveryNRequests,
296
+ minimumRemaining: storage.quota.minimumRemaining,
297
+ failClosedOnUnknownQuota: storage.quota.failClosedOnUnknownQuota,
298
+ showToasts: storage.quota.showToasts,
299
+ })
300
+ : undefined;
301
+ return omitUndefinedTopLevel({
302
+ version: 1,
303
+ main: storage.main,
304
+ routing: storage.routing,
305
+ fallbackOn: storage.fallbackOn,
306
+ refresh,
307
+ quota,
308
+ claudeCache: storage.claudeCache,
309
+ dump: storage.dump,
310
+ claudeFast: storage.claudeFast,
311
+ cacheKeep: storage.cacheKeep,
312
+ relay: storage.relay,
313
+ killswitch: storage.killswitch,
314
+ accounts: storage.accounts.map(accountConfig),
315
+ });
316
+ }
317
+ function stateFromStorage(storage) {
318
+ const accounts = Object.fromEntries(storage.accounts.map((account) => [
319
+ account.id,
320
+ accountRuntimeState(account),
321
+ ]));
322
+ return {
323
+ version: 1,
324
+ main: objectWithDefinedEntries({
325
+ quota: storage.quota?.mainQuota,
326
+ quotaCheckedAt: storage.quota?.mainQuotaCheckedAt,
327
+ quotaToken: storage.quota?.mainQuotaToken,
328
+ lastQuotaApiError: storage.quota?.mainLastQuotaApiError,
329
+ lastRefreshError: storage.refresh?.mainLastRefreshError,
330
+ refreshLeaseId: storage.refresh?.mainRefreshLeaseId,
331
+ refreshLeaseUntil: storage.refresh?.mainRefreshLeaseUntil,
332
+ refreshLeaseTokenHash: storage.refresh?.mainRefreshLeaseTokenHash,
333
+ }),
334
+ accounts,
335
+ };
336
+ }
337
+ async function writeJsonAtomic(path, value) {
164
338
  await mkdir(dirname(path), { recursive: true });
165
- const existing = await loadExistingTopLevelFields(path);
166
- const next = { ...existing, ...omitUndefinedTopLevel(storage) };
167
339
  const tempPath = `${path}.${randomUUID()}.tmp`;
168
- await writeFile(tempPath, `${JSON.stringify(next, null, 2)}\n`, {
340
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
169
341
  encoding: 'utf8',
170
342
  mode: 0o600,
171
343
  });
172
344
  await rename(tempPath, path);
173
345
  }
346
+ export async function saveAccounts(storage, path = getAccountStoragePath()) {
347
+ const existing = await loadExistingTopLevelFields(path);
348
+ const nextConfig = { ...existing, ...configFromStorage(storage) };
349
+ await writeJsonAtomic(path, nextConfig);
350
+ await writeJsonAtomic(getAccountStatePath(path), stateFromStorage(storage));
351
+ }
352
+ function applyMainQuotaStatePatch(state, storage) {
353
+ state.main = state.main ?? {};
354
+ state.main.quota = storage.quota?.mainQuota;
355
+ state.main.quotaCheckedAt = storage.quota?.mainQuotaCheckedAt;
356
+ state.main.quotaToken = storage.quota?.mainQuotaToken;
357
+ state.main.lastQuotaApiError = storage.quota?.mainLastQuotaApiError;
358
+ }
359
+ function applyMainRefreshStatePatch(state, storage) {
360
+ state.main = state.main ?? {};
361
+ state.main.lastRefreshError = storage.refresh?.mainLastRefreshError;
362
+ state.main.refreshLeaseId = storage.refresh?.mainRefreshLeaseId;
363
+ state.main.refreshLeaseUntil = storage.refresh?.mainRefreshLeaseUntil;
364
+ state.main.refreshLeaseTokenHash = storage.refresh?.mainRefreshLeaseTokenHash;
365
+ }
366
+ function pruneUndefined(value) {
367
+ if (Array.isArray(value))
368
+ return value.map(pruneUndefined);
369
+ if (!isRecord(value))
370
+ return value;
371
+ return Object.fromEntries(Object.entries(value)
372
+ .filter(([, entry]) => entry !== undefined)
373
+ .map(([key, entry]) => [key, pruneUndefined(entry)]));
374
+ }
375
+ export async function saveAccountState(storage, path = getAccountStoragePath(), scope = {
376
+ mainQuota: true,
377
+ mainRefresh: true,
378
+ accounts: true,
379
+ }) {
380
+ const statePath = getAccountStatePath(path);
381
+ const existing = (await readJsonIfPresent(statePath)).value;
382
+ const next = isRecord(existing)
383
+ ? { ...existing, version: 1 }
384
+ : { version: 1 };
385
+ if (scope.mainQuota)
386
+ applyMainQuotaStatePatch(next, storage);
387
+ if (scope.mainRefresh)
388
+ applyMainRefreshStatePatch(next, storage);
389
+ if (scope.accounts) {
390
+ const ids = scope.accounts === true ? null : new Set(scope.accounts);
391
+ next.accounts = { ...(isRecord(next.accounts) ? next.accounts : {}) };
392
+ for (const account of storage.accounts) {
393
+ if (ids && !ids.has(account.id))
394
+ continue;
395
+ next.accounts[account.id] = accountRuntimeState(account);
396
+ }
397
+ if (ids) {
398
+ for (const id of ids) {
399
+ if (!storage.accounts.some((account) => account.id === id)) {
400
+ delete next.accounts[id];
401
+ }
402
+ }
403
+ }
404
+ }
405
+ await writeJsonAtomic(statePath, pruneUndefined(next));
406
+ }
174
407
  export async function acquireRefreshFileLock(options) {
175
408
  const accountPath = options.path ?? getAccountStoragePath();
176
409
  const lockPath = `${accountPath}.${options.name}.lock`;
@@ -786,8 +1019,10 @@ export class FallbackAccountManager {
786
1019
  async load() {
787
1020
  return loadAccounts(this.configPath);
788
1021
  }
789
- async save(storage) {
790
- await saveAccounts(storage, this.configPath);
1022
+ async save(storage, accountIds) {
1023
+ await saveAccountState(storage, this.configPath, {
1024
+ accounts: accountIds ?? true,
1025
+ });
791
1026
  }
792
1027
  startBackgroundRefresh() {
793
1028
  const run = async () => {
@@ -818,7 +1053,7 @@ export class FallbackAccountManager {
818
1053
  const usable = [];
819
1054
  let changed = false;
820
1055
  for (const account of storage.accounts) {
821
- if (account.enabled === false)
1056
+ if (account.enabled === false || !isOAuthAccount(account))
822
1057
  continue;
823
1058
  try {
824
1059
  let next = account;
@@ -898,7 +1133,7 @@ export class FallbackAccountManager {
898
1133
  return;
899
1134
  let changed = false;
900
1135
  for (const account of storage.accounts) {
901
- if (account.enabled === false)
1136
+ if (account.enabled === false || !isOAuthAccount(account))
902
1137
  continue;
903
1138
  if (!tokenNeedsRefresh(account, storage, this.now()))
904
1139
  continue;
@@ -940,7 +1175,7 @@ export class FallbackAccountManager {
940
1175
  return;
941
1176
  let changed = false;
942
1177
  for (const account of storage.accounts) {
943
- if (account.enabled === false)
1178
+ if (account.enabled === false || !isOAuthAccount(account))
944
1179
  continue;
945
1180
  let next = account;
946
1181
  try {
@@ -985,7 +1220,7 @@ export class FallbackAccountManager {
985
1220
  const force = options.force ?? false;
986
1221
  let changed = false;
987
1222
  for (const account of storage.accounts) {
988
- if (account.enabled === false)
1223
+ if (account.enabled === false || !isOAuthAccount(account))
989
1224
  continue;
990
1225
  let next = account;
991
1226
  try {
@@ -1046,7 +1281,7 @@ export class FallbackAccountManager {
1046
1281
  while (Date.now() < deadline) {
1047
1282
  await new Promise((resolve) => setTimeout(resolve, FALLBACK_REFRESH_JOIN_POLL_MS));
1048
1283
  const latestStorage = await this.load();
1049
- const latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
1284
+ const latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id && isOAuthAccount(candidate));
1050
1285
  if (!latestAccount)
1051
1286
  continue;
1052
1287
  const changed = latestAccount.access !== previous.access ||
@@ -1075,7 +1310,7 @@ export class FallbackAccountManager {
1075
1310
  }
1076
1311
  async refreshAccountNow(account, storage, options) {
1077
1312
  let latestStorage = await this.load();
1078
- let latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
1313
+ let latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id && isOAuthAccount(candidate));
1079
1314
  if (latestAccount &&
1080
1315
  !options.force &&
1081
1316
  !tokenNeedsRefresh(latestAccount, latestStorage, this.now())) {
@@ -1101,7 +1336,7 @@ export class FallbackAccountManager {
1101
1336
  }
1102
1337
  try {
1103
1338
  latestStorage = await this.load();
1104
- latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
1339
+ latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id && isOAuthAccount(candidate));
1105
1340
  if (latestAccount &&
1106
1341
  !options.force &&
1107
1342
  !tokenNeedsRefresh(latestAccount, latestStorage, this.now())) {
@@ -4,6 +4,9 @@ export declare const CACHE_KEEP_TTL_MS: number;
4
4
  export declare const CACHE_KEEP_PREWARM_LEAD_MS: number;
5
5
  export declare const CACHE_KEEP_TICK_MS = 60000;
6
6
  export declare const CACHE_KEEP_EXTENDED_TTL_BETA = "extended-cache-ttl-2025-04-11";
7
+ export declare const CACHE_KEEP_MAX_TARGETS = 32;
8
+ export declare const CACHE_KEEP_MAX_BODY_BYTES: number;
9
+ export declare const CACHE_KEEP_STALE_TARGET_MS: number;
7
10
  export type CacheKeepWindow = {
8
11
  startHour: number;
9
12
  endHour: number;
@@ -74,6 +77,9 @@ export declare class CacheKeepManager {
74
77
  nextPrewarmAt: number | undefined;
75
78
  };
76
79
  trackedCount(): number;
80
+ private totalBodyBytes;
81
+ private evictOldestTarget;
82
+ private pruneTargets;
77
83
  track(input: {
78
84
  sessionId?: string | null;
79
85
  url: string;
package/dist/cachekeep.js CHANGED
@@ -5,6 +5,9 @@ export const CACHE_KEEP_TTL_MS = 60 * 60_000;
5
5
  export const CACHE_KEEP_PREWARM_LEAD_MS = 5 * 60_000;
6
6
  export const CACHE_KEEP_TICK_MS = 60_000;
7
7
  export const CACHE_KEEP_EXTENDED_TTL_BETA = 'extended-cache-ttl-2025-04-11';
8
+ export const CACHE_KEEP_MAX_TARGETS = 32;
9
+ export const CACHE_KEEP_MAX_BODY_BYTES = 16 * 1024 * 1024;
10
+ export const CACHE_KEEP_STALE_TARGET_MS = 2 * CACHE_KEEP_TTL_MS;
8
11
  const STATUS_TITLE = '## Claude Cache Keep Status';
9
12
  const ENABLED_TITLE = '## Claude Cache Keep Enabled';
10
13
  const DISABLED_TITLE = '## Claude Cache Keep Disabled';
@@ -208,6 +211,33 @@ export class CacheKeepManager {
208
211
  trackedCount() {
209
212
  return this.targets.size;
210
213
  }
214
+ totalBodyBytes() {
215
+ return [...this.targets.values()].reduce((sum, target) => sum + target.bodyText.length, 0);
216
+ }
217
+ evictOldestTarget() {
218
+ const oldest = this.targets.keys().next().value;
219
+ if (oldest !== undefined)
220
+ this.targets.delete(oldest);
221
+ }
222
+ pruneTargets(now, today) {
223
+ for (const [id, target] of this.targets) {
224
+ if (today && target.dayKey !== today) {
225
+ this.targets.delete(id);
226
+ continue;
227
+ }
228
+ if (now - target.cacheExpiresAt > CACHE_KEEP_STALE_TARGET_MS) {
229
+ this.targets.delete(id);
230
+ }
231
+ }
232
+ while (this.targets.size > CACHE_KEEP_MAX_TARGETS)
233
+ this.evictOldestTarget();
234
+ while (this.totalBodyBytes() > CACHE_KEEP_MAX_BODY_BYTES) {
235
+ const before = this.targets.size;
236
+ this.evictOldestTarget();
237
+ if (this.targets.size === before)
238
+ break;
239
+ }
240
+ }
211
241
  track(input) {
212
242
  if (!input.sessionId)
213
243
  return { tracked: false, reason: 'missing session id' };
@@ -217,11 +247,18 @@ export class CacheKeepManager {
217
247
  if (input.cacheMode !== 'hybrid') {
218
248
  return { tracked: false, reason: 'cache mode is not hybrid' };
219
249
  }
250
+ if (input.bodyText.length > CACHE_KEEP_MAX_BODY_BYTES) {
251
+ return { tracked: false, reason: 'body exceeds cachekeep memory budget' };
252
+ }
220
253
  const now = this.options.now?.() ?? Date.now();
221
254
  const window = getCacheKeepWindow(input.storage);
222
255
  if (!isWithinCacheKeepWindow(window, new Date(now))) {
223
256
  return { tracked: false, reason: 'outside configured window' };
224
257
  }
258
+ const today = localWindowKey(window, new Date(now));
259
+ this.pruneTargets(now, today);
260
+ if (this.targets.has(input.sessionId))
261
+ this.targets.delete(input.sessionId);
225
262
  const headers = {};
226
263
  input.headers.forEach((value, key) => {
227
264
  headers[key] = value;
@@ -232,8 +269,9 @@ export class CacheKeepManager {
232
269
  headers,
233
270
  bodyText: input.bodyText,
234
271
  cacheExpiresAt: now + CACHE_KEEP_TTL_MS,
235
- dayKey: localWindowKey(window, new Date(now)),
272
+ dayKey: today,
236
273
  });
274
+ this.pruneTargets(now, today);
237
275
  this.start();
238
276
  return { tracked: true };
239
277
  }
@@ -242,10 +280,7 @@ export class CacheKeepManager {
242
280
  const window = getCacheKeepWindow(storage);
243
281
  const now = this.options.now?.() ?? Date.now();
244
282
  const today = localWindowKey(window, new Date(now));
245
- for (const [id, target] of this.targets) {
246
- if (target.dayKey !== today)
247
- this.targets.delete(id);
248
- }
283
+ this.pruneTargets(now, today);
249
284
  if (!this.targets.size) {
250
285
  this.stop();
251
286
  return;
@@ -22,7 +22,10 @@ export function getClaudeCodeIdentity(seed) {
22
22
  setBounded(identityCache, cacheKey, identity);
23
23
  return identity;
24
24
  }
25
+ const BOOTSTRAP_IDENTITY_CACHE_TTL_MS = 24 * 60 * 60_000;
26
+ const BOOTSTRAP_IDENTITY_NEGATIVE_TTL_MS = 5 * 60_000;
25
27
  const bootstrapFetches = new Map();
28
+ const bootstrapResults = new Map();
26
29
  async function fetchClaudeCodeAccountUuid(accessToken, model) {
27
30
  if (!accessToken.startsWith('sk-ant-oat'))
28
31
  return null;
@@ -61,16 +64,35 @@ export async function resolveClaudeCodeIdentity(accessToken, model) {
61
64
  const identity = getClaudeCodeIdentity(accessToken);
62
65
  if (!accessToken.startsWith('sk-ant-oat'))
63
66
  return identity;
67
+ const now = Date.now();
68
+ const cachedResult = bootstrapResults.get(accessToken);
69
+ if (cachedResult && cachedResult.expiresAt > now) {
70
+ if (!cachedResult.accountUuid)
71
+ return identity;
72
+ const accountCacheKey = `account:${cachedResult.accountUuid}`;
73
+ const accountIdentity = identityCache.get(accountCacheKey);
74
+ if (accountIdentity) {
75
+ setBounded(identityCache, accessToken, accountIdentity);
76
+ return accountIdentity;
77
+ }
78
+ }
64
79
  let fetchPromise = bootstrapFetches.get(accessToken);
65
80
  if (!fetchPromise) {
66
81
  fetchPromise = fetchClaudeCodeAccountUuid(accessToken, model);
67
82
  bootstrapFetches.set(accessToken, fetchPromise);
68
83
  }
69
- const accountUuid = await fetchPromise;
70
- if (!accountUuid) {
84
+ const accountUuid = await fetchPromise.finally(() => {
71
85
  bootstrapFetches.delete(accessToken);
86
+ });
87
+ setBounded(bootstrapResults, accessToken, {
88
+ accountUuid,
89
+ expiresAt: now +
90
+ (accountUuid
91
+ ? BOOTSTRAP_IDENTITY_CACHE_TTL_MS
92
+ : BOOTSTRAP_IDENTITY_NEGATIVE_TTL_MS),
93
+ });
94
+ if (!accountUuid)
72
95
  return identity;
73
- }
74
96
  const accountCacheKey = `account:${accountUuid}`;
75
97
  const accountIdentity = identityCache.get(accountCacheKey);
76
98
  if (accountIdentity) {
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export * from './dump.ts';
9
9
  export * from './fast.ts';
10
10
  export * from './killswitch.ts';
11
11
  export * from './logger.ts';
12
+ export * from './models.ts';
12
13
  export * from './pkce.ts';
13
14
  export * from './quota-manager.ts';
14
15
  export * from './quotas.ts';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export * from "./dump.js";
9
9
  export * from "./fast.js";
10
10
  export * from "./killswitch.js";
11
11
  export * from "./logger.js";
12
+ export * from "./models.js";
12
13
  export * from "./pkce.js";
13
14
  export * from "./quota-manager.js";
14
15
  export * from "./quotas.js";
@@ -0,0 +1,20 @@
1
+ export declare const CLAUDE_FABLE_5_MODEL_ID = "claude-fable-5";
2
+ export declare const CLAUDE_MYTHOS_5_MODEL_ID = "claude-mythos-5";
3
+ export declare const CLAUDE_FABLE_MYTHOS_5_MODEL_IDS: readonly ["claude-fable-5", "claude-mythos-5"];
4
+ export type ClaudeFableMythos5ModelId = (typeof CLAUDE_FABLE_MYTHOS_5_MODEL_IDS)[number];
5
+ export declare const CLAUDE_FABLE_MYTHOS_5_PRICING: {
6
+ readonly input: 10;
7
+ readonly output: 50;
8
+ readonly cacheRead: 1;
9
+ readonly cacheWrite5m: 12.5;
10
+ readonly cacheWrite1h: 20;
11
+ };
12
+ export declare const CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW = 1000000;
13
+ export declare const CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS = 128000;
14
+ export declare const CLAUDE_FABLE_MYTHOS_5_RELEASE_DATE = "2026-06-09";
15
+ export declare const CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS: Record<ClaudeFableMythos5ModelId, {
16
+ id: ClaudeFableMythos5ModelId;
17
+ name: string;
18
+ limited?: boolean;
19
+ }>;
20
+ export declare function isClaudeFableOrMythos5Model(model: unknown): boolean;
package/dist/models.js ADDED
@@ -0,0 +1,31 @@
1
+ export const CLAUDE_FABLE_5_MODEL_ID = 'claude-fable-5';
2
+ export const CLAUDE_MYTHOS_5_MODEL_ID = 'claude-mythos-5';
3
+ export const CLAUDE_FABLE_MYTHOS_5_MODEL_IDS = [
4
+ CLAUDE_FABLE_5_MODEL_ID,
5
+ CLAUDE_MYTHOS_5_MODEL_ID,
6
+ ];
7
+ export const CLAUDE_FABLE_MYTHOS_5_PRICING = {
8
+ input: 10,
9
+ output: 50,
10
+ cacheRead: 1,
11
+ cacheWrite5m: 12.5,
12
+ cacheWrite1h: 20,
13
+ };
14
+ export const CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW = 1_000_000;
15
+ export const CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS = 128_000;
16
+ export const CLAUDE_FABLE_MYTHOS_5_RELEASE_DATE = '2026-06-09';
17
+ export const CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS = {
18
+ [CLAUDE_FABLE_5_MODEL_ID]: {
19
+ id: CLAUDE_FABLE_5_MODEL_ID,
20
+ name: 'Claude Fable 5',
21
+ },
22
+ [CLAUDE_MYTHOS_5_MODEL_ID]: {
23
+ id: CLAUDE_MYTHOS_5_MODEL_ID,
24
+ name: 'Claude Mythos 5',
25
+ limited: true,
26
+ },
27
+ };
28
+ export function isClaudeFableOrMythos5Model(model) {
29
+ return (typeof model === 'string' &&
30
+ CLAUDE_FABLE_MYTHOS_5_MODEL_IDS.some((id) => model === id || model.startsWith(`${id}-`)));
31
+ }
@@ -94,6 +94,7 @@ export declare class QuotaManager {
94
94
  /** Minimum gap between consecutive quota API calls (ms). */
95
95
  private static readonly API_CALL_GAP_MS;
96
96
  private static fallbackInflightKey;
97
+ private static quotaLockName;
97
98
  /**
98
99
  * Serialize API calls through a shared gate so only one
99
100
  * quota API request runs at a time, with a minimum gap
@@ -284,6 +284,10 @@ export class QuotaManager {
284
284
  static fallbackInflightKey(accountId, accessToken) {
285
285
  return JSON.stringify([accountId, tokenFingerprint(accessToken)]);
286
286
  }
287
+ static quotaLockName(accountId) {
288
+ const safeId = accountId.replace(/[^a-zA-Z0-9._-]+/g, '-');
289
+ return `opencode-fallback-quota-refresh-${safeId || 'account'}`;
290
+ }
287
291
  /**
288
292
  * Serialize API calls through a shared gate so only one
289
293
  * quota API request runs at a time, with a minimum gap
@@ -367,20 +371,35 @@ export class QuotaManager {
367
371
  return cached.quota;
368
372
  throw new Error('Quota API rate-limited — try again later');
369
373
  }
370
- const quota = await fetchOAuthQuotaSnapshot({
371
- accessToken,
372
- fetchImpl: this.fetchImpl,
373
- now: this.now,
374
+ const fileLock = await acquireRefreshFileLock({
375
+ name: QuotaManager.quotaLockName(accountId),
376
+ ttlMs: 30_000,
374
377
  });
375
- const now = this.now();
376
- this.setFallback(accountId, {
377
- quota,
378
- refreshAfter: now + getQuotaCheckIntervalMs(this.storage),
379
- checkedAt: now,
380
- }, accessToken);
381
- this.fallbackApiErrors.delete(accountId);
382
- this.fallbackErrorTokenFps.delete(accountId);
383
- return quota;
378
+ if (!fileLock) {
379
+ const cached = this.getFallback(accountId, accessToken);
380
+ if (cached)
381
+ return cached.quota;
382
+ throw new Error('Quota refresh is already in progress');
383
+ }
384
+ try {
385
+ const quota = await fetchOAuthQuotaSnapshot({
386
+ accessToken,
387
+ fetchImpl: this.fetchImpl,
388
+ now: this.now,
389
+ });
390
+ const now = this.now();
391
+ this.setFallback(accountId, {
392
+ quota,
393
+ refreshAfter: now + getQuotaCheckIntervalMs(this.storage),
394
+ checkedAt: now,
395
+ }, accessToken);
396
+ this.fallbackApiErrors.delete(accountId);
397
+ this.fallbackErrorTokenFps.delete(accountId);
398
+ return quota;
399
+ }
400
+ finally {
401
+ await fileLock.release();
402
+ }
384
403
  }
385
404
  catch (error) {
386
405
  this._handleFallbackFetchError(accountId, accessToken, error);
package/dist/quotas.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isOAuthAccount } from "./accounts.js";
1
2
  export const CLAUDE_QUOTAS_COMMAND_NAME = 'claude-quota';
2
3
  const WINDOW_LABELS = {
3
4
  five_hour: '5h',
@@ -60,7 +61,7 @@ function accountStoredError(account) {
60
61
  export function buildFallbackQuotaSummaries(storage, errors = new Map()) {
61
62
  if (!storage?.accounts.length)
62
63
  return [];
63
- return storage.accounts.map((account) => {
64
+ return storage.accounts.filter(isOAuthAccount).map((account) => {
64
65
  const error = errors.get(account.id) ?? accountStoredError(account);
65
66
  return {
66
67
  name: accountName(account),
package/dist/relay.js CHANGED
@@ -21,6 +21,10 @@ class RelayWebSocketConnectionResetError extends Error {
21
21
  const sessionState = new Map();
22
22
  const websocketSessions = new Map();
23
23
  const loggedRelayConfigMessages = new Set();
24
+ const RELAY_SESSION_STATE_TTL_MS = 2 * 60 * 60_000;
25
+ const RELAY_WEBSOCKET_IDLE_TTL_MS = 30 * 60_000;
26
+ const MAX_RELAY_SESSION_STATES = 128;
27
+ const MAX_RELAY_WEBSOCKET_SESSIONS = 32;
24
28
  let nextRequestId = 0;
25
29
  function createRequestId() {
26
30
  nextRequestId += 1;
@@ -136,14 +140,23 @@ function jsonHeaders(headers) {
136
140
  }
137
141
  return result;
138
142
  }
139
- async function postRelay(config, payload) {
143
+ async function postRelay(config, payload, signal) {
144
+ const stringifyStart = perfNowMs();
145
+ const body = JSON.stringify(payload);
146
+ relayPerfLog('http_payload_stringify', {
147
+ session: shortAffinity(payload.affinity),
148
+ ms: formatMs(perfNowMs() - stringifyStart),
149
+ relayBytes: body.length,
150
+ mode: payload.mode,
151
+ });
140
152
  return fetch(config.url, {
141
153
  method: 'POST',
142
154
  headers: {
143
155
  'content-type': 'application/json',
144
156
  'x-relay-token': config.token,
145
157
  },
146
- body: JSON.stringify(payload),
158
+ body,
159
+ signal: signal ?? undefined,
147
160
  });
148
161
  }
149
162
  function shortAffinity(affinity) {
@@ -155,6 +168,14 @@ function perfNowMs() {
155
168
  function formatMs(value) {
156
169
  return Math.round(value * 10) / 10;
157
170
  }
171
+ function relayPerfLoggingEnabled() {
172
+ return process.env.OPENCODE_ANTHROPIC_AUTH_PERF === '1';
173
+ }
174
+ function relayPerfLog(stage, data) {
175
+ if (!relayPerfLoggingEnabled())
176
+ return;
177
+ relayLog(`[perf] relay ${stage} ${JSON.stringify(data)}`);
178
+ }
158
179
  function createRelayPayload(options) {
159
180
  const { input, init, headers, bodyText, affinity, previous, nextHash } = options;
160
181
  const rewrittenUrl = input instanceof Request ? input.url : input.toString();
@@ -197,21 +218,25 @@ function createFullSyncPayload(payload, bodyText) {
197
218
  };
198
219
  }
199
220
  function updateLocalRelayState(affinity, bodyText, nextHash, revision) {
221
+ const now = Date.now();
222
+ sessionState.delete(affinity);
200
223
  sessionState.set(affinity, {
201
224
  body: bodyText,
202
225
  hash: nextHash,
203
226
  revision,
227
+ updatedAt: now,
204
228
  });
229
+ cleanupRelaySessionCaches(now);
205
230
  }
206
231
  async function sendRelayHttp(options) {
207
- const { config, payload, bodyText, fallback } = options;
232
+ const { config, payload, bodyText, fallback, signal } = options;
208
233
  let actualPayload = payload;
209
- let response = await postRelay(config, actualPayload);
234
+ let response = await postRelay(config, actualPayload, signal);
210
235
  if (response.status === 409 && actualPayload.mode === 'patch') {
211
236
  relayLog(`state mismatch from relay session=${shortAffinity(actualPayload.affinity)}; retrying full_sync`);
212
237
  await response.body?.cancel().catch(() => { });
213
238
  actualPayload = createFullSyncPayload(actualPayload, bodyText);
214
- response = await postRelay(config, actualPayload);
239
+ response = await postRelay(config, actualPayload, signal);
215
240
  }
216
241
  if (!response.ok && response.status >= 500 && config.fallbackToDirect) {
217
242
  relayLog(`relay returned ${response.status}; falling back direct session=${shortAffinity(actualPayload.affinity)}`);
@@ -279,11 +304,27 @@ class PersistentRelaySession {
279
304
  queue = Promise.resolve();
280
305
  pending;
281
306
  serverState;
307
+ lastUsedAt = Date.now();
282
308
  constructor(config, affinity) {
283
309
  this.config = config;
284
310
  this.affinity = affinity;
285
311
  }
312
+ touch(now = Date.now()) {
313
+ this.lastUsedAt = now;
314
+ }
315
+ canEvict(now = Date.now()) {
316
+ return (!this.pending &&
317
+ !this.connecting &&
318
+ now - this.lastUsedAt > RELAY_WEBSOCKET_IDLE_TTL_MS);
319
+ }
320
+ dispose() {
321
+ this.socket?.close();
322
+ this.socket = undefined;
323
+ this.serverState = undefined;
324
+ this.connecting = undefined;
325
+ }
286
326
  send(payload, bodyText, optimisticResponse = false) {
327
+ this.touch();
287
328
  const enqueuedAt = perfNowMs();
288
329
  const start = this.queue
289
330
  .catch(() => { })
@@ -638,12 +679,48 @@ class PersistentRelaySession {
638
679
  pending.rejectDone(error);
639
680
  }
640
681
  }
682
+ function cleanupRelaySessionCaches(now = Date.now()) {
683
+ for (const [affinity, state] of sessionState) {
684
+ if (now - state.updatedAt > RELAY_SESSION_STATE_TTL_MS) {
685
+ sessionState.delete(affinity);
686
+ }
687
+ }
688
+ while (sessionState.size > MAX_RELAY_SESSION_STATES) {
689
+ const oldest = sessionState.keys().next().value;
690
+ if (oldest === undefined)
691
+ break;
692
+ sessionState.delete(oldest);
693
+ }
694
+ for (const [affinity, session] of websocketSessions) {
695
+ if (session.canEvict(now)) {
696
+ session.dispose();
697
+ websocketSessions.delete(affinity);
698
+ }
699
+ }
700
+ while (websocketSessions.size > MAX_RELAY_WEBSOCKET_SESSIONS) {
701
+ let evicted = false;
702
+ for (const [affinity, session] of websocketSessions) {
703
+ if (!session.canEvict(now))
704
+ continue;
705
+ session.dispose();
706
+ websocketSessions.delete(affinity);
707
+ evicted = true;
708
+ break;
709
+ }
710
+ if (!evicted)
711
+ break;
712
+ }
713
+ }
641
714
  function getPersistentRelaySession(config, affinity) {
715
+ cleanupRelaySessionCaches();
642
716
  const existing = websocketSessions.get(affinity);
643
- if (existing)
717
+ if (existing) {
718
+ existing.touch();
644
719
  return existing;
720
+ }
645
721
  const session = new PersistentRelaySession(config, affinity);
646
722
  websocketSessions.set(affinity, session);
723
+ cleanupRelaySessionCaches();
647
724
  return session;
648
725
  }
649
726
  export async function sendViaRelay(options) {
@@ -658,8 +735,15 @@ export async function sendViaRelay(options) {
658
735
  return fallback();
659
736
  }
660
737
  const bodyText = body;
738
+ const hashStart = perfNowMs();
661
739
  const nextHash = hashBody(bodyText);
740
+ relayPerfLog('hash_body', {
741
+ session: shortAffinity(affinity),
742
+ ms: formatMs(perfNowMs() - hashStart),
743
+ bodyBytes: bodyText.length,
744
+ });
662
745
  const previous = sessionState.get(affinity);
746
+ const payloadStart = perfNowMs();
663
747
  const payload = createRelayPayload({
664
748
  input,
665
749
  init,
@@ -669,8 +753,16 @@ export async function sendViaRelay(options) {
669
753
  previous,
670
754
  nextHash,
671
755
  });
756
+ relayPerfLog('create_payload', {
757
+ session: shortAffinity(affinity),
758
+ ms: formatMs(perfNowMs() - payloadStart),
759
+ mode: payload.mode,
760
+ bodyBytes: bodyText.length,
761
+ previousBytes: previous?.body.length,
762
+ });
672
763
  try {
673
764
  let result;
765
+ const sendStart = perfNowMs();
674
766
  if (config.transport === 'websocket') {
675
767
  try {
676
768
  const session = getPersistentRelaySession(config, affinity);
@@ -678,19 +770,47 @@ export async function sendViaRelay(options) {
678
770
  }
679
771
  catch (error) {
680
772
  relayLog(`websocket relay failed session=${shortAffinity(affinity)}; trying http relay: ${error instanceof Error ? error.message : String(error)}`);
681
- result = await sendRelayHttp({ config, payload, bodyText, fallback });
773
+ result = await sendRelayHttp({
774
+ config,
775
+ payload,
776
+ bodyText,
777
+ fallback,
778
+ signal: init?.signal,
779
+ });
682
780
  }
683
781
  }
684
782
  else {
685
- result = await sendRelayHttp({ config, payload, bodyText, fallback });
783
+ result = await sendRelayHttp({
784
+ config,
785
+ payload,
786
+ bodyText,
787
+ fallback,
788
+ signal: init?.signal,
789
+ });
686
790
  }
791
+ relayPerfLog('send', {
792
+ session: shortAffinity(affinity),
793
+ ms: formatMs(perfNowMs() - sendStart),
794
+ transport: result.transport,
795
+ protocol: result.protocol,
796
+ mode: result.payload.mode,
797
+ status: result.response.status,
798
+ usedRelay: result.usedRelay,
799
+ });
687
800
  if (!result.usedRelay)
688
801
  return result.response;
689
802
  if (result.transport === 'http') {
690
803
  updateLocalRelayState(affinity, bodyText, result.payload.next_hash, result.payload.revision);
691
804
  }
805
+ const payloadStringifyStart = perfNowMs();
692
806
  const actualPayloadBytes = JSON.stringify(result.payload).length;
807
+ relayPerfLog('payload_stringify', {
808
+ session: shortAffinity(affinity),
809
+ ms: formatMs(perfNowMs() - payloadStringifyStart),
810
+ relayBytes: actualPayloadBytes,
811
+ });
693
812
  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}`);
813
+ const dumpStart = perfNowMs();
694
814
  await dumpRelayRequest({
695
815
  affinity,
696
816
  transport: result.transport,
@@ -702,6 +822,11 @@ export async function sendViaRelay(options) {
702
822
  payload: result.payload,
703
823
  relayBytes: actualPayloadBytes,
704
824
  });
825
+ relayPerfLog('dump', {
826
+ session: shortAffinity(affinity),
827
+ ms: formatMs(perfNowMs() - dumpStart),
828
+ enabled: true,
829
+ });
705
830
  return result.response;
706
831
  }
707
832
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",