@cortexkit/anthropic-auth-core 1.5.0 → 1.6.1

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.
@@ -31,6 +31,14 @@ export type AccountQuotaWindow = {
31
31
  checkedAt: number;
32
32
  };
33
33
  export type RoutingMode = 'main-first' | 'fallback-first';
34
+ export type KillswitchThresholds = Partial<Record<QuotaWindowName | '5h' | '1w', number>>;
35
+ export type KillswitchConfig = {
36
+ enabled?: boolean;
37
+ /** Thresholds for the main OAuth account (remaining % below which the account is killed). */
38
+ main?: KillswitchThresholds;
39
+ /** Per-account overrides keyed by account ID. Accounts without an entry use the `main` thresholds. */
40
+ accounts?: Record<string, KillswitchThresholds>;
41
+ };
34
42
  export type AccountStorage = {
35
43
  version: 1;
36
44
  main?: {
@@ -56,6 +64,8 @@ export type AccountStorage = {
56
64
  refreshEveryNRequests?: number;
57
65
  minimumRemaining?: Partial<Record<QuotaWindowName | '5h' | '1w', number>>;
58
66
  failClosedOnUnknownQuota?: boolean;
67
+ /** Opt-in OpenCode TUI toast after quota refresh. Default: false. */
68
+ showToasts?: boolean;
59
69
  mainQuota?: OAuthQuotaSnapshot;
60
70
  mainQuotaCheckedAt?: number;
61
71
  mainQuotaToken?: string;
@@ -83,6 +93,7 @@ export type AccountStorage = {
83
93
  fallbackToDirect?: boolean;
84
94
  transport?: 'http' | 'websocket';
85
95
  };
96
+ killswitch?: KillswitchConfig;
86
97
  accounts: OAuthAccount[];
87
98
  };
88
99
  export type OAuthQuotaSnapshot = Partial<Record<QuotaWindowName, AccountQuotaWindow>>;
@@ -91,6 +102,7 @@ export type AccountManagerOptions = {
91
102
  fetchImpl?: typeof fetch;
92
103
  configPath?: string;
93
104
  quotaManager?: import('./quota-manager.ts').QuotaManager;
105
+ onFallbackStorageChanged?: () => void;
94
106
  };
95
107
  export type AccountRefreshError = {
96
108
  accountId: string;
@@ -149,6 +161,22 @@ export declare function getPersistedMainQuota(storage: AccountStorage | null): {
149
161
  */
150
162
  export declare function getQuotaRefreshEveryNRequests(storage: AccountStorage | null): number;
151
163
  export declare function quotaSnapshotPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null): boolean;
164
+ export declare const DEFAULT_KILLSWITCH_THRESHOLDS: Record<QuotaWindowName, number>;
165
+ export declare function isKillswitchEnabled(storage: AccountStorage | null): boolean;
166
+ /**
167
+ * Returns true if the account's quota is above its killswitch threshold.
168
+ * When killswitch is disabled, always returns true.
169
+ */
170
+ export declare function killswitchPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, accountId?: string): boolean;
171
+ /**
172
+ * Find the earliest reset time across all accounts' quota windows.
173
+ * Returns seconds from `now` until that reset, or 300 as a fallback.
174
+ */
175
+ export declare function killswitchRetryAfterSeconds(mainQuota: OAuthQuotaSnapshot | undefined, fallbackAccounts: Array<{
176
+ quota?: OAuthQuotaSnapshot;
177
+ }>, now: number): number;
178
+ export declare function getKillswitchConfig(storage: AccountStorage | null): KillswitchConfig;
179
+ export declare function setKillswitchPersistent(config: KillswitchConfig, path?: string): Promise<AccountStorage>;
152
180
  export declare function getQuotaNextRefreshAt(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, now: number): number;
153
181
  export declare function fetchOAuthQuotaSnapshot(input: {
154
182
  accessToken: string;
@@ -166,6 +194,7 @@ export declare class FallbackAccountManager {
166
194
  private refreshTimer;
167
195
  private quotaTimer;
168
196
  readonly quotaManager: import('./quota-manager.ts').QuotaManager | null;
197
+ private readonly onFallbackStorageChanged;
169
198
  constructor(options?: AccountManagerOptions);
170
199
  /**
171
200
  * Seed QuotaManager from persisted account.quota if no cache entry exists
package/dist/accounts.js CHANGED
@@ -130,6 +130,7 @@ function normalizeStorage(value) {
130
130
  claudeFast: isRecord(value.claudeFast) ? value.claudeFast : undefined,
131
131
  cacheKeep: isRecord(value.cacheKeep) ? value.cacheKeep : undefined,
132
132
  relay: isRecord(value.relay) ? value.relay : undefined,
133
+ killswitch: isRecord(value.killswitch) ? value.killswitch : undefined,
133
134
  accounts: value.accounts
134
135
  .map(normalizeAccount)
135
136
  .filter((account) => account != null),
@@ -517,6 +518,95 @@ export function quotaSnapshotPassesPolicy(quota, storage) {
517
518
  }
518
519
  return true;
519
520
  }
521
+ // ---------------------------------------------------------------------------
522
+ // Killswitch — hard-block requests when remaining quota drops below per-account
523
+ // thresholds, even if the API would still accept them.
524
+ // ---------------------------------------------------------------------------
525
+ export const DEFAULT_KILLSWITCH_THRESHOLDS = {
526
+ five_hour: 5,
527
+ seven_day: 10,
528
+ };
529
+ function normalizeKillswitchThresholds(thresholds) {
530
+ return {
531
+ five_hour: thresholds?.five_hour ??
532
+ thresholds?.['5h'] ??
533
+ DEFAULT_KILLSWITCH_THRESHOLDS.five_hour,
534
+ seven_day: thresholds?.seven_day ??
535
+ thresholds?.['1w'] ??
536
+ DEFAULT_KILLSWITCH_THRESHOLDS.seven_day,
537
+ };
538
+ }
539
+ export function isKillswitchEnabled(storage) {
540
+ return storage?.killswitch?.enabled === true;
541
+ }
542
+ function getKillswitchThresholdsForAccount(storage, accountId) {
543
+ if (!storage?.killswitch)
544
+ return DEFAULT_KILLSWITCH_THRESHOLDS;
545
+ if (accountId && storage.killswitch.accounts?.[accountId]) {
546
+ return normalizeKillswitchThresholds(storage.killswitch.accounts[accountId]);
547
+ }
548
+ return normalizeKillswitchThresholds(storage.killswitch.main);
549
+ }
550
+ /**
551
+ * Returns true if the account's quota is above its killswitch threshold.
552
+ * When killswitch is disabled, always returns true.
553
+ */
554
+ export function killswitchPassesPolicy(quota, storage, accountId) {
555
+ if (!isKillswitchEnabled(storage))
556
+ return true;
557
+ const thresholds = getKillswitchThresholdsForAccount(storage, accountId);
558
+ let sawUnknownWindow = false;
559
+ for (const key of ['five_hour', 'seven_day']) {
560
+ const window = quota?.[key];
561
+ // Defer the unknown-window decision: a quota snapshot can legally carry
562
+ // only one window, and a present window below its threshold must still
563
+ // block even if the other window is missing.
564
+ if (!window) {
565
+ sawUnknownWindow = true;
566
+ continue;
567
+ }
568
+ if (window.remainingPercent < thresholds[key])
569
+ return false;
570
+ }
571
+ if (sawUnknownWindow)
572
+ return !failClosedOnUnknownQuota(storage);
573
+ return true;
574
+ }
575
+ /**
576
+ * Find the earliest reset time across all accounts' quota windows.
577
+ * Returns seconds from `now` until that reset, or 300 as a fallback.
578
+ */
579
+ export function killswitchRetryAfterSeconds(mainQuota, fallbackAccounts, now) {
580
+ const resetTimes = [];
581
+ const allQuotas = [mainQuota, ...fallbackAccounts.map((a) => a.quota)];
582
+ for (const quota of allQuotas) {
583
+ for (const key of ['five_hour', 'seven_day']) {
584
+ const resetStr = quota?.[key]?.resetsAt;
585
+ if (!resetStr)
586
+ continue;
587
+ const resetTime = Date.parse(resetStr);
588
+ if (Number.isFinite(resetTime) && resetTime > now) {
589
+ resetTimes.push(resetTime);
590
+ }
591
+ }
592
+ }
593
+ if (!resetTimes.length)
594
+ return 300;
595
+ return Math.max(1, Math.ceil((Math.min(...resetTimes) - now) / 1000)) + 60;
596
+ }
597
+ export function getKillswitchConfig(storage) {
598
+ return storage?.killswitch ?? { enabled: false };
599
+ }
600
+ export async function setKillswitchPersistent(config, path = getAccountStoragePath()) {
601
+ const storage = (await loadAccounts(path)) ?? {
602
+ version: 1,
603
+ main: { type: 'opencode', provider: 'anthropic' },
604
+ accounts: [],
605
+ };
606
+ storage.killswitch = config;
607
+ await saveAccounts(storage, path);
608
+ return storage;
609
+ }
520
610
  export function getQuotaNextRefreshAt(quota, storage, now) {
521
611
  if (!quotaEnabled(storage))
522
612
  return now + getQuotaCheckIntervalMs(storage);
@@ -664,11 +754,13 @@ export class FallbackAccountManager {
664
754
  refreshTimer = null;
665
755
  quotaTimer = null;
666
756
  quotaManager;
757
+ onFallbackStorageChanged;
667
758
  constructor(options = {}) {
668
759
  this.now = options.now ?? Date.now;
669
760
  this.fetchImpl = options.fetchImpl ?? fetch;
670
761
  this.configPath = options.configPath ?? getAccountStoragePath();
671
762
  this.quotaManager = options.quotaManager ?? null;
763
+ this.onFallbackStorageChanged = options.onFallbackStorageChanged;
672
764
  }
673
765
  /**
674
766
  * Seed QuotaManager from persisted account.quota if no cache entry exists
@@ -880,8 +972,10 @@ export class FallbackAccountManager {
880
972
  // Quota probes are advisory; failed probes fail closed at selection time.
881
973
  }
882
974
  }
883
- if (changed)
975
+ if (changed) {
884
976
  await this.save(storage);
977
+ this.onFallbackStorageChanged?.();
978
+ }
885
979
  }
886
980
  async refreshQuotaForAllAccounts(options = {}) {
887
981
  const storage = await this.load();
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from './claude-code.ts';
7
7
  export * from './constants.ts';
8
8
  export * from './dump.ts';
9
9
  export * from './fast.ts';
10
+ export * from './killswitch.ts';
10
11
  export * from './logger.ts';
11
12
  export * from './pkce.ts';
12
13
  export * from './quota-manager.ts';
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export * from "./claude-code.js";
7
7
  export * from "./constants.js";
8
8
  export * from "./dump.js";
9
9
  export * from "./fast.js";
10
+ export * from "./killswitch.js";
10
11
  export * from "./logger.js";
11
12
  export * from "./pkce.js";
12
13
  export * from "./quota-manager.js";
@@ -0,0 +1,27 @@
1
+ import type { KillswitchConfig } from './accounts.ts';
2
+ export declare const KILLSWITCH_COMMAND_NAME = "claude-killswitch";
3
+ export type KillswitchCommandAction = {
4
+ type: 'status';
5
+ } | {
6
+ type: 'on';
7
+ } | {
8
+ type: 'off';
9
+ } | {
10
+ type: 'set';
11
+ entries: Array<{
12
+ account: string;
13
+ fh: number;
14
+ sd: number;
15
+ }>;
16
+ } | {
17
+ type: 'usage';
18
+ };
19
+ export declare function parseKillswitchCommandAction(argumentsText: string): KillswitchCommandAction;
20
+ export declare function executeKillswitchCommand(input: {
21
+ argumentsText: string;
22
+ config: KillswitchConfig;
23
+ accountIds: string[];
24
+ }): {
25
+ text: string;
26
+ updatedConfig?: KillswitchConfig;
27
+ };
@@ -0,0 +1,127 @@
1
+ import { DEFAULT_KILLSWITCH_THRESHOLDS as DEFAULT_THRESHOLDS } from "./accounts.js";
2
+ export const KILLSWITCH_COMMAND_NAME = 'claude-killswitch';
3
+ export function parseKillswitchCommandAction(argumentsText) {
4
+ const parts = argumentsText.trim().split(/\s+/).filter(Boolean);
5
+ if (parts.length === 0)
6
+ return { type: 'status' };
7
+ if (parts.length === 1 && parts[0] === 'on')
8
+ return { type: 'on' };
9
+ if (parts.length === 1 && parts[0] === 'off')
10
+ return { type: 'off' };
11
+ if (parts[0] === 'set') {
12
+ const entries = [];
13
+ for (let i = 1; i < parts.length; i++) {
14
+ const match = parts[i]?.match(/^([^:]+):(\d+),(\d+)$/);
15
+ if (!match)
16
+ return { type: 'usage' };
17
+ const [, account, fhStr, sdStr] = match;
18
+ entries.push({
19
+ account,
20
+ fh: Number.parseInt(fhStr, 10),
21
+ sd: Number.parseInt(sdStr, 10),
22
+ });
23
+ }
24
+ if (entries.length === 0)
25
+ return { type: 'usage' };
26
+ return { type: 'set', entries };
27
+ }
28
+ return { type: 'usage' };
29
+ }
30
+ function buildStatusTable(config, accountIds) {
31
+ const enabled = config.enabled === true;
32
+ const lines = [
33
+ '## Killswitch',
34
+ '',
35
+ `Status: **${enabled ? 'ON' : 'OFF'}**`,
36
+ ];
37
+ if (enabled) {
38
+ lines.push('');
39
+ lines.push('| Account | 5h threshold | 1w threshold |');
40
+ lines.push('| ------- | ------------ | ------------ |');
41
+ const mainT = config.main ?? {};
42
+ const fh = mainT.five_hour ?? mainT['5h'] ?? DEFAULT_THRESHOLDS.five_hour;
43
+ const sd = mainT.seven_day ?? mainT['1w'] ?? DEFAULT_THRESHOLDS.seven_day;
44
+ lines.push(`| main | \u2265 ${fh}% | \u2265 ${sd}% |`);
45
+ for (const id of accountIds) {
46
+ const t = config.accounts?.[id] ?? config.main ?? {};
47
+ const afh = t.five_hour ?? t['5h'] ?? DEFAULT_THRESHOLDS.five_hour;
48
+ const asd = t.seven_day ?? t['1w'] ?? DEFAULT_THRESHOLDS.seven_day;
49
+ lines.push(`| ${id} | \u2265 ${afh}% | \u2265 ${asd}% |`);
50
+ }
51
+ }
52
+ return lines.join('\n');
53
+ }
54
+ const USAGE_TEXT = [
55
+ '## Killswitch Commands',
56
+ '',
57
+ '```',
58
+ `/${KILLSWITCH_COMMAND_NAME} — show status`,
59
+ `/${KILLSWITCH_COMMAND_NAME} on — enable with current or default thresholds`,
60
+ `/${KILLSWITCH_COMMAND_NAME} off — disable`,
61
+ `/${KILLSWITCH_COMMAND_NAME} set all:5,10 — set all accounts to 5h≥5%, 1w≥10%`,
62
+ `/${KILLSWITCH_COMMAND_NAME} set main:3,8 work-alt:5,10 — per-account`,
63
+ '```',
64
+ ].join('\n');
65
+ export function executeKillswitchCommand(input) {
66
+ const action = parseKillswitchCommandAction(input.argumentsText);
67
+ if (action.type === 'status') {
68
+ const status = buildStatusTable(input.config, input.accountIds);
69
+ return { text: `${status}\n\n${USAGE_TEXT}` };
70
+ }
71
+ if (action.type === 'on') {
72
+ const updated = {
73
+ ...input.config,
74
+ enabled: true,
75
+ main: input.config.main ?? {
76
+ five_hour: DEFAULT_THRESHOLDS.five_hour,
77
+ seven_day: DEFAULT_THRESHOLDS.seven_day,
78
+ },
79
+ };
80
+ const status = buildStatusTable(updated, input.accountIds);
81
+ return {
82
+ text: `## Killswitch Enabled\n\n${status}`,
83
+ updatedConfig: updated,
84
+ };
85
+ }
86
+ if (action.type === 'off') {
87
+ const updated = { ...input.config, enabled: false };
88
+ return {
89
+ text: '## Killswitch Disabled',
90
+ updatedConfig: updated,
91
+ };
92
+ }
93
+ if (action.type === 'set') {
94
+ const accounts = { ...(input.config.accounts ?? {}) };
95
+ const updated = {
96
+ ...input.config,
97
+ enabled: true,
98
+ accounts,
99
+ };
100
+ for (const entry of action.entries) {
101
+ const thresholds = {
102
+ five_hour: entry.fh,
103
+ seven_day: entry.sd,
104
+ };
105
+ if (entry.account === 'main') {
106
+ updated.main = thresholds;
107
+ }
108
+ else if (entry.account === 'all') {
109
+ updated.main = thresholds;
110
+ for (const id of input.accountIds) {
111
+ accounts[id] = thresholds;
112
+ }
113
+ }
114
+ else {
115
+ accounts[entry.account] = thresholds;
116
+ }
117
+ }
118
+ const status = buildStatusTable(updated, input.accountIds);
119
+ return {
120
+ text: `## Killswitch Updated\n\n${status}`,
121
+ updatedConfig: updated,
122
+ };
123
+ }
124
+ // usage
125
+ const status = buildStatusTable(input.config, input.accountIds);
126
+ return { text: `${status}\n\n${USAGE_TEXT}` };
127
+ }
package/dist/relay.js CHANGED
@@ -4,6 +4,20 @@ import { dumpRelayRequest } from "./dump.js";
4
4
  import { relayLog } from "./logger.js";
5
5
  class RelayStateMismatchError extends Error {
6
6
  }
7
+ class RelayWebSocketConnectionResetError extends Error {
8
+ code = 'ECONNRESET';
9
+ syscall = 'websocket';
10
+ closeCode;
11
+ closeReason;
12
+ wasClean;
13
+ constructor(message, event) {
14
+ super(message);
15
+ this.name = 'RelayWebSocketConnectionResetError';
16
+ this.closeCode = event?.code;
17
+ this.closeReason = event?.reason;
18
+ this.wasClean = event?.wasClean;
19
+ }
20
+ }
7
21
  const sessionState = new Map();
8
22
  const websocketSessions = new Map();
9
23
  const loggedRelayConfigMessages = new Set();
@@ -241,6 +255,16 @@ function toBinaryChunk(data) {
241
255
  function parseRelayControlMessage(data) {
242
256
  return JSON.parse(data);
243
257
  }
258
+ function formatCloseEvent(event) {
259
+ const close = event;
260
+ const parts = [
261
+ `code=${close.code ?? 'unknown'}`,
262
+ `wasClean=${close.wasClean ?? 'unknown'}`,
263
+ ];
264
+ if (close.reason)
265
+ parts.push(`reason=${JSON.stringify(close.reason)}`);
266
+ return parts.join(' ');
267
+ }
244
268
  function relayControlErrorMessage(message) {
245
269
  const detail = message.message || 'relay websocket error';
246
270
  return message.status
@@ -360,11 +384,11 @@ class PersistentRelaySession {
360
384
  });
361
385
  socket.addEventListener('error', () => {
362
386
  clearTimeout(timeout);
363
- const error = new Error('relay websocket error');
387
+ const error = new RelayWebSocketConnectionResetError('relay websocket error');
364
388
  reject(error);
365
389
  this.failPending(error);
366
390
  });
367
- socket.addEventListener('close', () => {
391
+ socket.addEventListener('close', (event) => {
368
392
  clearTimeout(timeout);
369
393
  if (this.socket === socket) {
370
394
  this.socket = undefined;
@@ -374,9 +398,16 @@ class PersistentRelaySession {
374
398
  if (this.pending) {
375
399
  if (this.pending.retryingBeforeResponse)
376
400
  return;
377
- if (this.retryPendingBeforeResponse(this.pending, 'closed before response'))
401
+ const closedAfterResponseStart = this.pending.responseStartedAt != null;
402
+ const reason = closedAfterResponseStart
403
+ ? 'closed after response_start before stream bytes'
404
+ : 'closed before response';
405
+ if (this.retryPendingBeforeResponse(this.pending, reason, event))
378
406
  return;
379
- const error = new Error('relay websocket closed before response');
407
+ const error = closedAfterResponseStart
408
+ ? new RelayWebSocketConnectionResetError('relay websocket closed during response stream', event)
409
+ : new Error('relay websocket closed before response');
410
+ relayLog(`websocket ${closedAfterResponseStart ? 'closed during response stream' : 'closed before response without retry'} session=${shortAffinity(this.affinity)} request=${this.pending.payload.id} chunks=${this.pending.streamChunkCount} bytes=${this.pending.streamByteCount} ${formatCloseEvent(event)}`);
380
411
  this.failPending(error);
381
412
  }
382
413
  });
@@ -410,6 +441,8 @@ class PersistentRelaySession {
410
441
  rejectDone,
411
442
  sentAt,
412
443
  optimisticResponse,
444
+ streamChunkCount: 0,
445
+ streamByteCount: 0,
413
446
  retryAttempts: 0,
414
447
  retryingBeforeResponse: false,
415
448
  };
@@ -436,8 +469,8 @@ class PersistentRelaySession {
436
469
  this.socket?.close();
437
470
  }, 45_000);
438
471
  }
439
- retryPendingBeforeResponse(pending, reason) {
440
- if (pending.responseStartedAt != null)
472
+ retryPendingBeforeResponse(pending, reason, closeEvent) {
473
+ if (pending.responseStartedAt != null && pending.streamByteCount > 0)
441
474
  return false;
442
475
  if (pending.streamDone)
443
476
  return false;
@@ -448,7 +481,7 @@ class PersistentRelaySession {
448
481
  clearTimeout(pending.timeout);
449
482
  const attempt = pending.retryAttempts;
450
483
  const originalId = pending.payload.id;
451
- relayLog(`websocket ${reason}; reconnecting and retrying full_sync session=${shortAffinity(this.affinity)} request=${originalId} retry=${attempt}`);
484
+ relayLog(`websocket ${reason}; reconnecting and retrying full_sync session=${shortAffinity(this.affinity)} request=${originalId} retry=${attempt} chunks=${pending.streamChunkCount} bytes=${pending.streamByteCount}${closeEvent ? ` ${formatCloseEvent(closeEvent)}` : ''}`);
452
485
  this.socket?.close();
453
486
  this.socket = undefined;
454
487
  this.serverState = undefined;
@@ -484,11 +517,16 @@ class PersistentRelaySession {
484
517
  })();
485
518
  return true;
486
519
  }
520
+ enqueueStreamChunk(pending, chunk) {
521
+ pending.streamChunkCount += 1;
522
+ pending.streamByteCount += chunk.byteLength;
523
+ pending.streamController?.enqueue(chunk);
524
+ }
487
525
  handleBinaryChunk(data) {
488
526
  const pending = this.pending;
489
527
  if (!pending?.responseStarted)
490
528
  return;
491
- pending.streamController?.enqueue(toBinaryChunk(data));
529
+ this.enqueueStreamChunk(pending, toBinaryChunk(data));
492
530
  }
493
531
  resolvePendingResponse(pending, status, statusText, headers) {
494
532
  if (pending.responseStarted)
@@ -542,7 +580,7 @@ class PersistentRelaySession {
542
580
  return;
543
581
  }
544
582
  if (message.type === 'chunk') {
545
- pending.streamController?.enqueue(decodeRelayChunk(message.base64));
583
+ this.enqueueStreamChunk(pending, decodeRelayChunk(message.base64));
546
584
  return;
547
585
  }
548
586
  if (message.type === 'done') {
@@ -555,7 +593,7 @@ class PersistentRelaySession {
555
593
  // would reject a settled promise and leave the stream hanging. Instead,
556
594
  // inject an SSE error event and close cleanly.
557
595
  if (pending.optimisticResponse && pending.responseStartedAt != null) {
558
- relayLog(`websocket relay error during optimistic response session=${shortAffinity(this.affinity)} request=${pending.payload.id} status=${message.status} message=${message.message || 'unknown'}`);
596
+ relayLog(`websocket relay error during optimistic response session=${shortAffinity(this.affinity)} request=${pending.payload.id} status=${message.status} message=${message.message || 'unknown'} chunks=${pending.streamChunkCount} bytes=${pending.streamByteCount}`);
559
597
  pending.streamController?.enqueue(new TextEncoder().encode(`event: error\ndata: ${JSON.stringify({ type: 'error', error: { type: 'relay_error', message: message.message || 'relay error' } })}\n\n`));
560
598
  this.finishPending();
561
599
  this.socket?.close();
@@ -576,7 +614,7 @@ class PersistentRelaySession {
576
614
  if (!pending)
577
615
  return;
578
616
  const finishedAt = perfNowMs();
579
- relayLog(`perf websocket done session=${shortAffinity(this.affinity)} request=${pending.payload.id} sentMs=${formatMs(finishedAt - pending.sentAt)} streamMs=${pending.responseStartedAt == null ? 'unknown' : formatMs(finishedAt - pending.responseStartedAt)}`);
617
+ relayLog(`perf websocket done session=${shortAffinity(this.affinity)} request=${pending.payload.id} sentMs=${formatMs(finishedAt - pending.sentAt)} streamMs=${pending.responseStartedAt == null ? 'unknown' : formatMs(finishedAt - pending.responseStartedAt)} chunks=${pending.streamChunkCount} bytes=${pending.streamByteCount}`);
580
618
  clearTimeout(pending.timeout);
581
619
  if (!pending.streamDone) {
582
620
  pending.streamDone = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",