@cortexkit/pi-anthropic-auth 1.20.0 → 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/convert.js CHANGED
@@ -3,6 +3,10 @@ import { applyClaudeCodeMetadata, buildBillingHeaderValue, CLAUDE_CODE_ENTRYPOIN
3
3
  // that Anthropic currently rejects in system[]. Unknown prompt shapes take the
4
4
  // service-preserving fallback below instead of returning the full prompt there.
5
5
  const PI_DOCS_ANCHOR = 'Pi documentation';
6
+ const ANTHROPIC_REPLAY_APIS = new Set([
7
+ 'anthropic-messages',
8
+ 'cortexkit-anthropic-messages',
9
+ ]);
6
10
  const CLAUDE_CODE_TOOLS = new Map([
7
11
  'Read',
8
12
  'Write',
@@ -105,7 +109,7 @@ function convertTextAndImages(content) {
105
109
  }
106
110
  return blocks;
107
111
  }
108
- function convertMessages(messages) {
112
+ function convertMessages(messages, targetModelId) {
109
113
  const result = [];
110
114
  for (let index = 0; index < messages.length; index++) {
111
115
  const message = messages[index];
@@ -140,33 +144,63 @@ function convertMessages(messages) {
140
144
  if (block.type === 'text' && block.text.trim()) {
141
145
  blocks.push({ type: 'text', text: sanitize(block.text) });
142
146
  }
143
- else if (block.type === 'thinking' && block.thinking.trim()) {
147
+ else if (block.type === 'thinking') {
144
148
  const thinking = block;
145
- if (isOpenAIReasoningSignature(thinking.thinkingSignature)) {
146
- // OpenAI reasoningEncryptedContent is also opaque encrypted state,
147
- // but it is not an Anthropic thinking signature. Sending it as
148
- // `thinking.signature` makes Anthropic reject the request.
149
+ const signature = thinking.thinkingSignature;
150
+ const hasSignature = typeof signature === 'string' && signature.trim().length > 0;
151
+ const signatureBelongsToTarget = message.provider === 'anthropic' &&
152
+ ANTHROPIC_REPLAY_APIS.has(message.api) &&
153
+ message.model === targetModelId &&
154
+ !isOpenAIReasoningSignature(signature);
155
+ // Match Pi's built-in origin check: only replay signatures from the
156
+ // same Anthropic API/model. Preserve visible foreign reasoning as
157
+ // ordinary text, but drop opaque redacted/OpenAI encrypted state.
158
+ if (hasSignature && !signatureBelongsToTarget) {
159
+ if (!thinking.redacted &&
160
+ !isOpenAIReasoningSignature(signature) &&
161
+ thinking.thinking.trim()) {
162
+ blocks.push({ type: 'text', text: sanitize(thinking.thinking) });
163
+ }
149
164
  continue;
150
165
  }
151
- if (thinking.thinkingSignature &&
152
- !hasLoneSurrogate(thinking.thinking)) {
153
- // Signed thinking blocks must be sent back verbatim — the signature
154
- // is computed over the original text. Sanitizing would alter it and
155
- // Anthropic rejects the block as "modified". Anthropic-origin
156
- // thinking is valid UTF-8, so this is the normal path.
166
+ if (thinking.redacted) {
167
+ if (hasSignature) {
168
+ blocks.push({
169
+ type: 'redacted_thinking',
170
+ data: signature,
171
+ });
172
+ }
173
+ continue;
174
+ }
175
+ if (!hasSignature) {
176
+ // Anthropic rejects unsigned thinking blocks. Preserve readable
177
+ // output from interrupted streams as ordinary assistant text.
178
+ if (thinking.thinking.trim()) {
179
+ blocks.push({
180
+ type: 'text',
181
+ text: sanitize(thinking.thinking),
182
+ });
183
+ }
184
+ continue;
185
+ }
186
+ if (hasLoneSurrogate(thinking.thinking)) {
187
+ // Signature authenticates the thinking bytes exactly. Sanitizing the
188
+ // text would invalidate it, so downgrade malformed signed thinking
189
+ // to plain text and omit the signature instead.
190
+ if (thinking.thinking.trim()) {
191
+ blocks.push({
192
+ type: 'text',
193
+ text: sanitize(thinking.thinking),
194
+ });
195
+ }
196
+ }
197
+ else {
157
198
  blocks.push({
158
199
  type: 'thinking',
159
200
  thinking: thinking.thinking,
160
- signature: thinking.thinkingSignature,
201
+ signature,
161
202
  });
162
203
  }
163
- else {
164
- // Either unsigned, or signed-but-contains a lone surrogate. In the
165
- // latter case we cannot keep the signature: sanitizing breaks it and
166
- // sending the raw lone surrogate is an invalid-UTF8 400. Drop the
167
- // signature and downgrade to sanitized text.
168
- blocks.push({ type: 'text', text: sanitize(thinking.thinking) });
169
- }
170
204
  }
171
205
  else if (block.type === 'toolCall') {
172
206
  blocks.push({
@@ -318,7 +352,7 @@ function applyCacheMode(body, enabled, mode) {
318
352
  addTtl(body);
319
353
  }
320
354
  export async function buildAnthropicRequest(modelId, context, options, cache, fastModeEnabled = false, identity) {
321
- const messages = convertMessages(context.messages);
355
+ const messages = convertMessages(context.messages, modelId);
322
356
  // Strip trailing assistant messages — Anthropic rejects prefill on some models
323
357
  while (messages.length &&
324
358
  messages[messages.length - 1]?.role === 'assistant') {
package/dist/stream.js CHANGED
@@ -1,4 +1,4 @@
1
- import { applyClaudeCodeHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, createStickyNoRouteResponse, decideStickyQuotaFailure, dumpDirectRequest, FAST_MODE_BETA, FallbackAccountManager, getCache1hPersistentMode, getDefaultCacheKeepRegistryDirectory, getFallbackReauthLabels, getRelayConfig, getRoutingMode, getStickyRoutingStatePath, isApiKeyAccount, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isOAuthAccount, isPermanentRefreshError, isValidApiBaseURL, killswitchPassesPolicy, loadAccounts, mergeAnthropicBetas, QuotaManager, quotaSnapshotModelScopeIsExhausted, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, resolveClaudeCodeIdentity, STICKY_ROUTING_MAIN_ACCOUNT_ID, StickySessionRouter, sendViaRelay, setDumpEnabled, shouldFallbackStatus, stickyQuotaSnapshotIsFresh, stickyRetryAfterWithJitter, stickyRouteFamilyForModel, } from '@cortexkit/anthropic-auth-core';
1
+ import { applyClaudeCodeHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, createStickyNoRouteResponse, decideStickyQuotaFailure, dumpDirectRequest, FAST_MODE_BETA, FallbackAccountManager, getCache1hPersistentMode, getDefaultCacheKeepRegistryDirectory, getFallbackReauthLabels, getOrCreateMainAccountId, getRelayConfig, getRoutingMode, getStickyRoutingStatePath, isApiKeyAccount, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isOAuthAccount, isPermanentRefreshError, isValidApiBaseURL, killswitchPassesPolicy, loadAccounts, mergeAnthropicBetas, QuotaManager, quotaSnapshotHasStandardWindows, quotaSnapshotModelScopeIsExhausted, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, refreshBackoffActive, resolveClaudeCodeIdentity, STICKY_ROUTING_MAIN_ACCOUNT_ID, StickySessionRouter, sendViaRelay, setDumpEnabled, shouldFallbackStatus, stickyQuotaSnapshotIsFresh, stickyRetryAfterWithJitter, stickyRouteFamilyForModel, } from '@cortexkit/anthropic-auth-core';
2
2
  import { calculateCost, createAssistantMessageEventStream, } from '@earendil-works/pi-ai';
3
3
  import { buildAnthropicRequest, fromClaudeCodeToolName } from './convert.js';
4
4
  import { getPiAccountStoragePath } from './paths.js';
@@ -10,6 +10,7 @@ let cacheKeepRegistryDirectory;
10
10
  const stickyRouters = new Map();
11
11
  const quotaManagers = new Map();
12
12
  const fallbackManagers = new Map();
13
+ const mainAccountIdInitializations = new Map();
13
14
  const PI_SERVICE_CACHE_LIMIT = 16;
14
15
  function setBoundedService(map, key, value) {
15
16
  map.delete(key);
@@ -21,6 +22,19 @@ function setBoundedService(map, key, value) {
21
22
  map.delete(oldest);
22
23
  }
23
24
  }
25
+ function ensurePiMainAccountId(storagePath) {
26
+ let initialization = mainAccountIdInitializations.get(storagePath);
27
+ if (!initialization) {
28
+ initialization = getOrCreateMainAccountId(storagePath).catch((error) => {
29
+ if (mainAccountIdInitializations.get(storagePath) === initialization) {
30
+ mainAccountIdInitializations.delete(storagePath);
31
+ }
32
+ throw error;
33
+ });
34
+ setBoundedService(mainAccountIdInitializations, storagePath, initialization);
35
+ }
36
+ return initialization;
37
+ }
24
38
  function getPiRoutingServices(storagePath, storage) {
25
39
  let quotaManager = quotaManagers.get(storagePath);
26
40
  let fallbackManager = fallbackManagers.get(storagePath);
@@ -71,7 +85,10 @@ const cacheKeepManager = new CacheKeepManager({
71
85
  return headers;
72
86
  try {
73
87
  const body = JSON.parse(target.bodyText);
74
- const identity = await resolveClaudeCodeIdentity(accessToken, typeof body.model === 'string' ? body.model : undefined);
88
+ const storagePath = getPiAccountStoragePath();
89
+ await ensurePiMainAccountId(storagePath);
90
+ const storage = await loadAccounts(storagePath);
91
+ const identity = await resolveClaudeCodeIdentity(accessToken, typeof body.model === 'string' ? body.model : undefined, target.oauthAccountId ?? storage?.mainAccountId);
75
92
  headers.delete('anthropic-beta');
76
93
  applyClaudeCodeHeaders(headers, accessToken, { body, identity });
77
94
  headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [
@@ -201,10 +218,14 @@ export async function* parseSse(response) {
201
218
  }
202
219
  }
203
220
  async function sendAnthropicRequest(options) {
221
+ await ensurePiMainAccountId(options.storagePath);
204
222
  const storage = await loadAccounts(options.storagePath);
205
223
  setDumpEnabled(isDumpPersistentlyEnabled(storage));
224
+ const accountIdentity = options.oauthAccountId === STICKY_ROUTING_MAIN_ACCOUNT_ID
225
+ ? storage?.mainAccountId
226
+ : options.oauthAccountId;
206
227
  const identity = options.accessToken
207
- ? await resolveClaudeCodeIdentity(options.accessToken, options.model.id)
228
+ ? await resolveClaudeCodeIdentity(options.accessToken, options.model.id, accountIdentity)
208
229
  : undefined;
209
230
  const { body, bodyText } = await buildAnthropicRequest(options.model.id, options.context, options.streamOptions, {
210
231
  enabled: isCache1hPersistentlyEnabled(storage),
@@ -307,16 +328,18 @@ async function firstStreamingError(response) {
307
328
  return response;
308
329
  }
309
330
  async function executeWithFallback(options) {
331
+ await ensurePiMainAccountId(options.storagePath);
310
332
  const storage = await loadAccounts(options.storagePath);
311
333
  const { quotaManager, fallbackManager: manager } = getPiRoutingServices(options.storagePath, storage);
312
- quotaManager.seedMainFromStorage(storage, options.primaryAccessToken);
334
+ const mainAccountId = storage?.mainAccountId;
335
+ quotaManager.seedMainFromStorage(storage, mainAccountId);
313
336
  quotaManager.seedFallbacksFromAccounts((storage?.accounts ?? []).filter(isOAuthAccount));
314
337
  async function buildStickyRoutes(modelId) {
315
- const mainEntry = quotaManager.getMain(options.primaryAccessToken);
338
+ const mainEntry = quotaManager.getMain(mainAccountId);
316
339
  let mainQuota = mainEntry?.quota;
317
340
  if (!stickyQuotaSnapshotIsFresh(mainEntry?.quota, storage, Date.now(), modelId)) {
318
341
  try {
319
- mainQuota = await quotaManager.refreshMain(options.primaryAccessToken);
342
+ mainQuota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
320
343
  }
321
344
  catch { }
322
345
  }
@@ -339,11 +362,10 @@ async function executeWithFallback(options) {
339
362
  const account = usableById.get(configured.id) ?? configured;
340
363
  if (!account.access || isPermanentRefreshError(account.lastRefreshError))
341
364
  continue;
342
- let accountQuota = quotaManager.getFallback(account.id, account.access)?.quota ??
343
- account.quota;
365
+ let accountQuota = quotaManager.getFallback(account.id, account)?.quota ?? account.quota;
344
366
  if (!stickyQuotaSnapshotIsFresh(accountQuota, storage, Date.now(), modelId)) {
345
367
  try {
346
- accountQuota = await quotaManager.refreshFallback(account.id, account.access);
368
+ accountQuota = await quotaManager.refreshFallback(account.id, account.access, account);
347
369
  }
348
370
  catch { }
349
371
  }
@@ -374,19 +396,26 @@ async function executeWithFallback(options) {
374
396
  }));
375
397
  const usableIds = new Set(usableFallbacks.map((account) => account.id));
376
398
  const candidates = allRoutes.flatMap((route) => {
377
- if (!route.quota)
378
- return [];
399
+ const quota = quotaSnapshotHasStandardWindows(route.quota)
400
+ ? route.quota
401
+ : undefined;
379
402
  const accountId = route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID ? undefined : route.id;
380
- const passes = quotaSnapshotPassesPolicy(route.quota, storage) &&
381
- quotaSnapshotPassesModelScope(route.quota, modelId) &&
382
- (!isKillswitchEnabled(storage) ||
383
- killswitchPassesPolicy(route.quota, storage, accountId, modelId)) &&
384
- (route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID || usableIds.has(route.id));
403
+ const quotaState = quota
404
+ ? { kind: 'known', quota }
405
+ : { kind: 'unknown' };
406
+ const passesKillswitch = !isKillswitchEnabled(storage) ||
407
+ killswitchPassesPolicy(quotaState.kind === 'known' ? quotaState.quota : undefined, storage, accountId, modelId);
408
+ const passes = passesKillswitch &&
409
+ (quotaState.kind === 'unknown' ||
410
+ (quotaSnapshotPassesPolicy(quotaState.quota, storage) &&
411
+ quotaSnapshotPassesModelScope(quotaState.quota, modelId) &&
412
+ (route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID ||
413
+ usableIds.has(route.id))));
385
414
  return passes
386
415
  ? [
387
416
  {
388
417
  accountId: route.id,
389
- quota: route.quota,
418
+ quota: quotaState,
390
419
  order: route.order,
391
420
  },
392
421
  ]
@@ -396,8 +425,8 @@ async function executeWithFallback(options) {
396
425
  }
397
426
  async function primaryQuotaRefreshConfirmsExhausted() {
398
427
  try {
399
- const quota = await quotaManager.refreshMain(options.primaryAccessToken);
400
- const entry = quotaManager.getMain(options.primaryAccessToken);
428
+ const quota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
429
+ const entry = quotaManager.getMain(mainAccountId);
401
430
  return Boolean(entry &&
402
431
  entry.refreshAfter > Date.now() &&
403
432
  quotaSnapshotIsExhausted(quota));
@@ -408,8 +437,8 @@ async function executeWithFallback(options) {
408
437
  }
409
438
  async function primaryQuotaRefreshConfirmsModelScopeExhausted() {
410
439
  try {
411
- const quota = await quotaManager.refreshMain(options.primaryAccessToken);
412
- const entry = quotaManager.getMain(options.primaryAccessToken);
440
+ const quota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
441
+ const entry = quotaManager.getMain(mainAccountId);
413
442
  return Boolean(entry &&
414
443
  entry.refreshAfter > Date.now() &&
415
444
  quotaSnapshotModelScopeIsExhausted(quota, options.model.id));
@@ -419,12 +448,12 @@ async function executeWithFallback(options) {
419
448
  }
420
449
  }
421
450
  function primaryCachedModelScopeExhausted() {
422
- const entry = quotaManager.getMain(options.primaryAccessToken);
451
+ const entry = quotaManager.getMain(mainAccountId);
423
452
  return Boolean(entry &&
424
453
  quotaSnapshotModelScopeIsExhausted(entry.quota, options.model.id));
425
454
  }
426
455
  function primaryFreshModelScopeExhausted() {
427
- const entry = quotaManager.getMain(options.primaryAccessToken);
456
+ const entry = quotaManager.getMain(mainAccountId);
428
457
  return Boolean(entry &&
429
458
  !quotaManager.isMainStale(options.model.id) &&
430
459
  quotaSnapshotModelScopeIsExhausted(entry.quota, options.model.id));
@@ -436,17 +465,38 @@ async function executeWithFallback(options) {
436
465
  const usableOAuthById = new Map(usableOAuth.map((account) => [account.id, account]));
437
466
  for (const configured of storage?.accounts ?? []) {
438
467
  let response = null;
439
- const account = isOAuthAccount(configured)
468
+ let account = isOAuthAccount(configured)
440
469
  ? usableOAuthById.get(configured.id)
441
470
  : configured;
442
- if (!account)
471
+ if (!account) {
472
+ if (!isOAuthAccount(configured) ||
473
+ !configured.access ||
474
+ (configured.expires !== undefined &&
475
+ configured.expires <= Date.now()) ||
476
+ isPermanentRefreshError(configured.lastRefreshError) ||
477
+ refreshBackoffActive(configured.lastRefreshError, configured.id, Date.now()))
478
+ continue;
479
+ account = configured;
480
+ const quota = quotaManager.getFallback(configured.id, configured)?.quota ??
481
+ configured.quota;
482
+ if (quotaSnapshotHasStandardWindows(quota))
483
+ continue;
484
+ if (isKillswitchEnabled(storage) &&
485
+ !killswitchPassesPolicy(quota, storage, configured.id, options.model.id))
486
+ continue;
487
+ }
488
+ else if (isOAuthAccount(account) &&
489
+ isKillswitchEnabled(storage) &&
490
+ !killswitchPassesPolicy(quotaManager.getFallback(account.id, account)?.quota ?? account.quota, storage, account.id, options.model.id)) {
443
491
  continue;
492
+ }
444
493
  if (isOAuthAccount(account)) {
445
494
  if (routeOptions.apiOnly === true || !account.access)
446
495
  continue;
447
496
  response = await sendAnthropicRequest({
448
497
  ...options,
449
498
  accessToken: account.access,
499
+ oauthAccountId: account.id,
450
500
  });
451
501
  }
452
502
  else if (routeOptions.includeApiRoutes === true &&
@@ -587,8 +637,8 @@ async function executeWithFallback(options) {
587
637
  try {
588
638
  quota =
589
639
  route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID
590
- ? await quotaManager.refreshMain(route.access)
591
- : await quotaManager.refreshFallback(route.id, route.access);
640
+ ? await quotaManager.refreshMain(mainAccountId, route.access)
641
+ : await quotaManager.refreshFallback(route.id, route.access, route.account);
592
642
  }
593
643
  catch {
594
644
  // Retain affinity when the quota probe itself is unavailable.
@@ -680,9 +730,39 @@ async function executeWithFallback(options) {
680
730
  if (fallback)
681
731
  return fallback;
682
732
  }
733
+ if (isKillswitchEnabled(storage)) {
734
+ let mainQuota = quotaManager.getMain(mainAccountId)?.quota;
735
+ if (!mainQuota || quotaManager.isMainStale(options.model.id)) {
736
+ try {
737
+ mainQuota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
738
+ }
739
+ catch { }
740
+ }
741
+ if (!killswitchPassesPolicy(mainQuota, storage, undefined, options.model.id)) {
742
+ if (!fallbackFirst) {
743
+ const fallback = await tryFallbackAccounts();
744
+ if (fallback)
745
+ return fallback;
746
+ }
747
+ return new Response(JSON.stringify({
748
+ type: 'error',
749
+ error: {
750
+ type: 'rate_limit_error',
751
+ message: 'Killswitch blocked all OAuth routes',
752
+ },
753
+ }), {
754
+ status: 429,
755
+ headers: {
756
+ 'content-type': 'application/json',
757
+ 'retry-after': '60',
758
+ },
759
+ });
760
+ }
761
+ }
683
762
  const primary = await sendAnthropicRequest({
684
763
  ...options,
685
764
  accessToken: options.primaryAccessToken,
765
+ oauthAccountId: STICKY_ROUTING_MAIN_ACCOUNT_ID,
686
766
  });
687
767
  const primaryPreflight = await firstStreamingError(primary);
688
768
  if (primaryPreflight instanceof Response) {
@@ -758,6 +838,20 @@ export function streamCortexKitAnthropic(model, context, options) {
758
838
  partial: output,
759
839
  });
760
840
  }
841
+ else if (block?.type === 'redacted_thinking') {
842
+ output.content.push({
843
+ type: 'thinking',
844
+ thinking: '[Reasoning redacted]',
845
+ thinkingSignature: String(block.data ?? ''),
846
+ redacted: true,
847
+ index: event.index,
848
+ });
849
+ stream.push({
850
+ type: 'thinking_start',
851
+ contentIndex: output.content.length - 1,
852
+ partial: output,
853
+ });
854
+ }
761
855
  else if (block?.type === 'tool_use') {
762
856
  output.content.push({
763
857
  type: 'toolCall',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/pi-anthropic-auth",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,7 +36,7 @@
36
36
  "prepublishOnly": "bun run build"
37
37
  },
38
38
  "dependencies": {
39
- "@cortexkit/anthropic-auth-core": "1.20.0"
39
+ "@cortexkit/anthropic-auth-core": "1.21.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@earendil-works/pi-ai": "*",