@cortexkit/pi-anthropic-auth 1.21.0 → 1.23.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/stream.js CHANGED
@@ -1,7 +1,8 @@
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';
1
+ import { applyClaudeCodeHeaders, applyCustomHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, ClaustrumScopedRuntime, connectClaustrumScopedClient, createStickyNoRouteResponse, decideStickyQuotaFailure, dumpDirectRequest, FAST_MODE_BETA, FallbackAccountManager, fetchOAuthQuotaSnapshot, getCache1hPersistentMode, getClaudeCodeIdentityForVerifiedAccount, getClaustrumMode, getDefaultCacheKeepRegistryDirectory, getFallbackReauthLabels, getHostClaustrumEnrollmentPaths, getOrCreateMainAccountId, getQuotaCheckIntervalMs, getRelayConfig, getRoutingMode, getScopedQuotaWindowForModel, getStickyRoutingStatePath, getThinkingPrefixMismatchBehavior, hasThinkingBindingControls, isApiKeyAccount, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isOAuthAccount, isPermanentRefreshError, isValidApiBaseURL, killswitchPassesPolicy, loadAccounts, logger, MID_CONVERSATION_OUTPUT_CONFIG_BETA, mergeAnthropicBetas, QuotaManager, quotaSnapshotHasStandardWindows, quotaSnapshotModelScopeIsExhausted, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, refreshBackoffActive, resolveClaudeCodeIdentity, STICKY_ROUTING_MAIN_ACCOUNT_ID, StickySessionRouter, sendViaRelay, setDumpEnabled, shouldFallbackStatus, stickyQuotaSnapshotIsFresh, stickyRetryAfterWithJitter, stickyRouteFamilyForModel, THINKING_BINDING_CONTROLS_BETA, tokenFingerprint, usesMidConversationOutputConfig, } from '@cortexkit/anthropic-auth-core';
2
2
  import { calculateCost, createAssistantMessageEventStream, } from '@earendil-works/pi-ai';
3
3
  import { buildAnthropicRequest, fromClaudeCodeToolName } from './convert.js';
4
- import { getPiAccountStoragePath } from './paths.js';
4
+ import { requirePiEnrollment } from './custody.js';
5
+ import { getPiAccountStoragePath, getPiClaustrumConnectionOptions, } from './paths.js';
5
6
  function errorText(error) {
6
7
  return error instanceof Error ? error.message : String(error);
7
8
  }
@@ -12,6 +13,46 @@ const quotaManagers = new Map();
12
13
  const fallbackManagers = new Map();
13
14
  const mainAccountIdInitializations = new Map();
14
15
  const PI_SERVICE_CACHE_LIMIT = 16;
16
+ const scopedRuntimes = new Map();
17
+ export function getPiScopedRuntime(storagePath = getPiAccountStoragePath(), overrides = {}) {
18
+ const existing = scopedRuntimes.get(storagePath);
19
+ if (existing)
20
+ return existing;
21
+ let warned = false;
22
+ const { connect, ...runtimeOverrides } = overrides;
23
+ const runtime = new ClaustrumScopedRuntime({
24
+ storagePath,
25
+ tokenPath: getHostClaustrumEnrollmentPaths('pi').tokenPath,
26
+ connect: async () => {
27
+ await requirePiEnrollment();
28
+ return connect
29
+ ? connect()
30
+ : connectClaustrumScopedClient(getPiClaustrumConnectionOptions(storagePath));
31
+ },
32
+ onRoster: () => {
33
+ warned = false;
34
+ },
35
+ onError: () => {
36
+ if (!warned)
37
+ logger.warn('claustrum', 'Pi scoped credential service unavailable');
38
+ warned = true;
39
+ },
40
+ ...runtimeOverrides,
41
+ });
42
+ while (scopedRuntimes.size >= PI_SERVICE_CACHE_LIMIT) {
43
+ const oldest = scopedRuntimes.keys().next().value;
44
+ if (oldest === undefined)
45
+ break;
46
+ scopedRuntimes.get(oldest)?.close();
47
+ scopedRuntimes.delete(oldest);
48
+ }
49
+ scopedRuntimes.set(storagePath, runtime);
50
+ return runtime;
51
+ }
52
+ export function closePiScopedRuntime(storagePath = getPiAccountStoragePath()) {
53
+ scopedRuntimes.get(storagePath)?.close();
54
+ scopedRuntimes.delete(storagePath);
55
+ }
15
56
  function setBoundedService(map, key, value) {
16
57
  map.delete(key);
17
58
  map.set(key, value);
@@ -39,10 +80,38 @@ function getPiRoutingServices(storagePath, storage) {
39
80
  let quotaManager = quotaManagers.get(storagePath);
40
81
  let fallbackManager = fallbackManagers.get(storagePath);
41
82
  if (!quotaManager || !fallbackManager) {
42
- quotaManager = new QuotaManager({ storage });
83
+ quotaManager = new QuotaManager({
84
+ storage,
85
+ fetchQuotaSnapshot: async (request) => {
86
+ const current = await loadAccounts(storagePath);
87
+ if (getClaustrumMode(current) === 'claustrum') {
88
+ const id = request.kind === 'main' ? 'main' : request.accountId;
89
+ if (!id)
90
+ throw new Error('Claustrum quota request has no account identity');
91
+ return getPiScopedRuntime(storagePath).fetchQuota(id);
92
+ }
93
+ if (!request.accessToken)
94
+ throw new Error('Missing local OAuth quota credential');
95
+ return fetchOAuthQuotaSnapshot({ accessToken: request.accessToken });
96
+ },
97
+ });
43
98
  fallbackManager = new FallbackAccountManager({
44
99
  configPath: storagePath,
45
100
  quotaManager,
101
+ isFallbackAccountVaultEnabled: (_id, current) => getClaustrumMode(current) === 'claustrum',
102
+ isFallbackAccountVaultServed: (id, current) => {
103
+ const account = current.accounts.find((candidate) => candidate.id === id);
104
+ return (getClaustrumMode(current) === 'claustrum' &&
105
+ Boolean(account &&
106
+ isOAuthAccount(account) &&
107
+ account.claustrumScopedCredentialId &&
108
+ account.claustrumScopedState === 'active'));
109
+ },
110
+ resolveFallbackAccessToken: (account, current) => getClaustrumMode(current) === 'claustrum'
111
+ ? undefined
112
+ : account.access
113
+ ? { token: account.access, source: 'sidecar' }
114
+ : undefined,
46
115
  });
47
116
  setBoundedService(quotaManagers, storagePath, quotaManager);
48
117
  setBoundedService(fallbackManagers, storagePath, fallbackManager);
@@ -74,26 +143,64 @@ function getPiCacheKeepRegistry() {
74
143
  }
75
144
  return cacheKeepRegistry;
76
145
  }
146
+ const scopedPrewarmAttempts = new Map();
77
147
  const cacheKeepManager = new CacheKeepManager({
78
148
  loadStorage: () => loadAccounts(getPiAccountStoragePath()),
79
149
  onTrackedSessionsChanged: (sessions) => getPiCacheKeepRegistry().publish(sessions),
80
- prepareHeaders: async (headers, target) => {
150
+ prepareHeaders: async (headers, target, attempt) => {
151
+ const storagePath = target.accountStoragePath ?? getPiAccountStoragePath();
152
+ const current = await loadAccounts(storagePath);
153
+ if (target.accountStoragePath && !current)
154
+ return undefined;
155
+ if (getClaustrumMode(current) === 'claustrum') {
156
+ const runtime = getPiScopedRuntime(storagePath);
157
+ const id = target.oauthAccountId ?? STICKY_ROUTING_MAIN_ACCOUNT_ID;
158
+ const receipt = await runtime.authorize(id, attempt.signal);
159
+ if (!target.oauthAccountIdentity ||
160
+ receipt.accountId !== target.oauthAccountIdentity) {
161
+ throw new Error('CacheKeep account identity changed');
162
+ }
163
+ const body = JSON.parse(target.bodyText);
164
+ const identity = getClaudeCodeIdentityForVerifiedAccount(id === STICKY_ROUTING_MAIN_ACCOUNT_ID
165
+ ? (current?.mainAccountId ?? id)
166
+ : id, receipt.accountId);
167
+ headers.delete('anthropic-beta');
168
+ applyClaudeCodeHeaders(headers, receipt.accessToken, {
169
+ body,
170
+ identity,
171
+ extraBetas: [
172
+ CACHE_KEEP_EXTENDED_TTL_BETA,
173
+ ...(hasThinkingBindingControls(body)
174
+ ? [THINKING_BINDING_CONTROLS_BETA]
175
+ : []),
176
+ ],
177
+ });
178
+ if (body.speed === 'fast')
179
+ headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
180
+ scopedPrewarmAttempts.set(attempt.id, { runtime, receipt });
181
+ return headers;
182
+ }
81
183
  const authorization = headers.get('authorization') ?? '';
82
184
  const match = /^Bearer\s+(.+)$/i.exec(authorization);
83
185
  const accessToken = match?.[1];
84
186
  if (!accessToken)
85
- return headers;
187
+ return undefined;
86
188
  try {
87
189
  const body = JSON.parse(target.bodyText);
88
- const storagePath = getPiAccountStoragePath();
89
190
  await ensurePiMainAccountId(storagePath);
90
191
  const storage = await loadAccounts(storagePath);
91
192
  const identity = await resolveClaudeCodeIdentity(accessToken, typeof body.model === 'string' ? body.model : undefined, target.oauthAccountId ?? storage?.mainAccountId);
92
193
  headers.delete('anthropic-beta');
93
- applyClaudeCodeHeaders(headers, accessToken, { body, identity });
94
- headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [
95
- CACHE_KEEP_EXTENDED_TTL_BETA,
96
- ]));
194
+ applyClaudeCodeHeaders(headers, accessToken, {
195
+ body,
196
+ identity,
197
+ extraBetas: [
198
+ CACHE_KEEP_EXTENDED_TTL_BETA,
199
+ ...(hasThinkingBindingControls(body)
200
+ ? [THINKING_BINDING_CONTROLS_BETA]
201
+ : []),
202
+ ],
203
+ });
97
204
  if (body.speed === 'fast') {
98
205
  headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
99
206
  }
@@ -103,6 +210,14 @@ const cacheKeepManager = new CacheKeepManager({
103
210
  }
104
211
  return headers;
105
212
  },
213
+ onResponse: async ({ attempt, status }) => {
214
+ const entry = scopedPrewarmAttempts.get(attempt.id);
215
+ if (entry)
216
+ await entry.runtime.reportFailure(entry.receipt, status, 'direct');
217
+ },
218
+ onComplete: ({ attempt }) => {
219
+ scopedPrewarmAttempts.delete(attempt.id);
220
+ },
106
221
  });
107
222
  export async function getPiTrackedCacheKeepSessions() {
108
223
  return getPiCacheKeepRegistry().list(cacheKeepManager.trackedSessions());
@@ -158,7 +273,16 @@ function updateUsage(model, output, usage) {
158
273
  }
159
274
  export function buildExplicitBaseMessagesUrl(baseURL) {
160
275
  const url = new URL(baseURL);
161
- url.pathname = `${url.pathname.replace(/\/$/, '')}/v1/messages`;
276
+ const basePath = url.pathname.replace(/\/$/, '');
277
+ if (/\/v\d[^/]*\/messages$/.test(basePath)) {
278
+ url.pathname = basePath;
279
+ }
280
+ else if (/\/v\d[^/]*$/.test(basePath)) {
281
+ url.pathname = `${basePath}/messages`;
282
+ }
283
+ else {
284
+ url.pathname = `${basePath}/v1/messages`;
285
+ }
162
286
  url.searchParams.set('beta', 'true');
163
287
  return url;
164
288
  }
@@ -177,6 +301,7 @@ export function configureApiRouteHeaders(account, fastMode) {
177
301
  if (fastMode) {
178
302
  headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
179
303
  }
304
+ applyCustomHeaders(headers);
180
305
  return headers;
181
306
  }
182
307
  export async function* parseSse(response) {
@@ -224,19 +349,47 @@ async function sendAnthropicRequest(options) {
224
349
  const accountIdentity = options.oauthAccountId === STICKY_ROUTING_MAIN_ACCOUNT_ID
225
350
  ? storage?.mainAccountId
226
351
  : options.oauthAccountId;
227
- const identity = options.accessToken
228
- ? await resolveClaudeCodeIdentity(options.accessToken, options.model.id, accountIdentity)
352
+ const scoped = !options.apiAccount && getClaustrumMode(storage) === 'claustrum'
353
+ ? getPiScopedRuntime(options.storagePath)
354
+ : undefined;
355
+ const routeId = options.oauthAccountId ?? STICKY_ROUTING_MAIN_ACCOUNT_ID;
356
+ const roster = scoped
357
+ ? (scoped.snapshot() ?? (await scoped.refresh()))
229
358
  : undefined;
359
+ const verifiedUuid = routeId === STICKY_ROUTING_MAIN_ACCOUNT_ID
360
+ ? roster?.primary?.accountId
361
+ : roster?.accounts.find((account) => account.id === routeId)
362
+ ?.anthropicAccountUuid;
363
+ if (scoped && !verifiedUuid)
364
+ throw new Error('Claustrum route has no verified provider identity');
365
+ const identity = scoped && verifiedUuid
366
+ ? getClaudeCodeIdentityForVerifiedAccount(accountIdentity ?? routeId, verifiedUuid)
367
+ : options.accessToken
368
+ ? await resolveClaudeCodeIdentity(options.accessToken, options.model.id, accountIdentity)
369
+ : undefined;
230
370
  const { body, bodyText } = await buildAnthropicRequest(options.model.id, options.context, options.streamOptions, {
231
371
  enabled: isCache1hPersistentlyEnabled(storage),
232
372
  mode: getCache1hPersistentMode(storage),
233
- }, isFastModePersistentlyEnabled(storage), identity);
373
+ }, isFastModePersistentlyEnabled(storage), identity, {
374
+ effortTransitions: options.apiAccount ? [] : options.effortTransitions,
375
+ thinkingPrefixMismatchBehavior: options.apiAccount
376
+ ? 'account-default'
377
+ : getThinkingPrefixMismatchBehavior(storage),
378
+ });
234
379
  const fastMode = body.speed === 'fast';
235
380
  const headers = options.apiAccount
236
381
  ? configureApiRouteHeaders(options.apiAccount, fastMode)
237
- : applyClaudeCodeHeaders(new Headers(), options.accessToken ?? '', {
382
+ : applyClaudeCodeHeaders(new Headers(), scoped ? '' : (options.accessToken ?? ''), {
238
383
  body,
239
384
  identity,
385
+ extraBetas: [
386
+ ...(hasThinkingBindingControls(body)
387
+ ? [THINKING_BINDING_CONTROLS_BETA]
388
+ : []),
389
+ ...(usesMidConversationOutputConfig(body)
390
+ ? [MID_CONVERSATION_OUTPUT_CONFIG_BETA]
391
+ : []),
392
+ ],
240
393
  });
241
394
  if (!options.apiAccount && fastMode) {
242
395
  headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
@@ -257,12 +410,31 @@ async function sendAnthropicRequest(options) {
257
410
  headers,
258
411
  bodyText,
259
412
  storage,
260
- cacheMode: isCacheKeepHybridActive(storage) ? 'hybrid' : 'disabled',
413
+ cacheMode: !options.apiAccount && isCacheKeepHybridActive(storage)
414
+ ? 'hybrid'
415
+ : 'disabled',
261
416
  oauthAccountId: options.oauthAccountId,
417
+ oauthAccountIdentity: verifiedUuid ?? identity?.accountUuid,
418
+ accountStoragePath: options.storagePath,
262
419
  });
420
+ const report = async (attempt, status, source) => {
421
+ if (!scoped || status !== 401)
422
+ return;
423
+ await scoped.reportFailure(attempt, status, source).catch(() => {
424
+ logger.warn('claustrum', 'Pi scoped auth-failure report unavailable');
425
+ });
426
+ };
263
427
  const directFetch = async () => {
428
+ // Every physical attempt, including relay-to-direct fallback, is authorized.
429
+ const attempt = scoped
430
+ ? await scoped.authorize(routeId, options.streamOptions?.signal)
431
+ : undefined;
432
+ if (attempt)
433
+ headers.set('authorization', `Bearer ${attempt.accessToken}`);
264
434
  try {
265
435
  const response = await fetch(input, init);
436
+ if (attempt)
437
+ await report(attempt, response.status, 'direct');
266
438
  await dumpDirectRequest({
267
439
  affinity: relayAffinity,
268
440
  route: options.route ??
@@ -299,6 +471,19 @@ async function sendAnthropicRequest(options) {
299
471
  body: bodyText,
300
472
  fallback: directFetch,
301
473
  affinity: relayAffinity,
474
+ authorizeAttempt: scoped
475
+ ? async () => {
476
+ const attempt = await scoped.authorize(routeId, options.streamOptions?.signal);
477
+ const authorizedHeaders = new Headers(headers);
478
+ authorizedHeaders.set('authorization', `Bearer ${attempt.accessToken}`);
479
+ return {
480
+ headers: authorizedHeaders,
481
+ onUpstreamStatus: (status) => {
482
+ void report(attempt, status, 'relay_status_field');
483
+ },
484
+ };
485
+ }
486
+ : undefined,
302
487
  });
303
488
  }
304
489
  function quotaSnapshotIsExhausted(quota) {
@@ -329,17 +514,62 @@ async function firstStreamingError(response) {
329
514
  }
330
515
  async function executeWithFallback(options) {
331
516
  await ensurePiMainAccountId(options.storagePath);
332
- const storage = await loadAccounts(options.storagePath);
517
+ let storage = await loadAccounts(options.storagePath);
518
+ const scoped = getClaustrumMode(storage) === 'claustrum'
519
+ ? getPiScopedRuntime(options.storagePath)
520
+ : undefined;
521
+ let roster = scoped ? await scoped.refresh() : undefined;
522
+ if (scoped && !roster)
523
+ throw new Error('Claustrum custody changed during request preparation');
524
+ if (roster)
525
+ storage = roster.storage;
526
+ const primaryCredential = scoped ? '' : options.primaryAccessToken;
527
+ const scopedReady = (account) => Boolean(scoped &&
528
+ account.enabled !== false &&
529
+ account.claustrumScopedCredentialId &&
530
+ account.claustrumScopedState === 'active');
531
+ const primaryAvailable = () => !scoped ||
532
+ (roster?.primary?.state === 'active' &&
533
+ !storage?.claustrum?.disabledAccountIdentities?.includes(roster.primary.accountId));
333
534
  const { quotaManager, fallbackManager: manager } = getPiRoutingServices(options.storagePath, storage);
334
- const mainAccountId = storage?.mainAccountId;
535
+ let mainAccountId = scoped
536
+ ? roster?.primary?.accountId
537
+ : storage?.mainAccountId;
335
538
  quotaManager.seedMainFromStorage(storage, mainAccountId);
336
539
  quotaManager.seedFallbacksFromAccounts((storage?.accounts ?? []).filter(isOAuthAccount));
540
+ function quotaObservationIsFresh(quota, modelId) {
541
+ if (stickyQuotaSnapshotIsFresh(quota, storage, Date.now(), modelId))
542
+ return true;
543
+ // A successful windowless usage response is not the same as a failed
544
+ // probe. Pi intentionally admits the former as unknown-capacity OAuth.
545
+ if (!quota ||
546
+ quota.five_hour ||
547
+ quota.seven_day ||
548
+ quota.source !== 'poll' ||
549
+ quota.checkedAt === undefined)
550
+ return false;
551
+ const age = getQuotaCheckIntervalMs(storage);
552
+ const scopedWindow = getScopedQuotaWindowForModel(quota, modelId);
553
+ return (Date.now() - quota.checkedAt < age &&
554
+ (!scopedWindow || Date.now() - scopedWindow.checkedAt < age));
555
+ }
337
556
  async function buildStickyRoutes(modelId) {
557
+ if (scoped) {
558
+ roster = await scoped.refresh();
559
+ if (!roster)
560
+ throw new Error('Claustrum custody changed during routing');
561
+ storage = roster.storage;
562
+ mainAccountId = roster.primary?.accountId;
563
+ quotaManager.updateStorage(storage);
564
+ quotaManager.seedMainFromStorage(storage, mainAccountId);
565
+ quotaManager.seedFallbacksFromAccounts(storage.accounts.filter(isOAuthAccount));
566
+ }
338
567
  const mainEntry = quotaManager.getMain(mainAccountId);
339
568
  let mainQuota = mainEntry?.quota;
340
- if (!stickyQuotaSnapshotIsFresh(mainEntry?.quota, storage, Date.now(), modelId)) {
569
+ if (primaryAvailable() &&
570
+ !stickyQuotaSnapshotIsFresh(mainEntry?.quota, storage, Date.now(), modelId)) {
341
571
  try {
342
- mainQuota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
572
+ mainQuota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
343
573
  }
344
574
  catch { }
345
575
  }
@@ -348,10 +578,12 @@ async function executeWithFallback(options) {
348
578
  });
349
579
  const usableById = new Map(usableFallbacks.map((account) => [account.id, account]));
350
580
  const allRoutes = [];
351
- if (!isPermanentRefreshError(storage?.refresh?.mainLastRefreshError)) {
581
+ if (primaryAvailable() &&
582
+ (scoped ||
583
+ !isPermanentRefreshError(storage?.refresh?.mainLastRefreshError))) {
352
584
  allRoutes.push({
353
585
  id: STICKY_ROUTING_MAIN_ACCOUNT_ID,
354
- access: options.primaryAccessToken,
586
+ access: primaryCredential,
355
587
  quota: mainQuota,
356
588
  order: 0,
357
589
  });
@@ -360,18 +592,20 @@ async function executeWithFallback(options) {
360
592
  if (configured.enabled === false || !isOAuthAccount(configured))
361
593
  continue;
362
594
  const account = usableById.get(configured.id) ?? configured;
363
- if (!account.access || isPermanentRefreshError(account.lastRefreshError))
595
+ if (scoped
596
+ ? !scopedReady(account)
597
+ : !account.access || isPermanentRefreshError(account.lastRefreshError))
364
598
  continue;
365
599
  let accountQuota = quotaManager.getFallback(account.id, account)?.quota ?? account.quota;
366
600
  if (!stickyQuotaSnapshotIsFresh(accountQuota, storage, Date.now(), modelId)) {
367
601
  try {
368
- accountQuota = await quotaManager.refreshFallback(account.id, account.access, account);
602
+ accountQuota = await quotaManager.refreshFallback(account.id, scoped ? '' : (account.access ?? ''), account);
369
603
  }
370
604
  catch { }
371
605
  }
372
606
  allRoutes.push({
373
607
  id: account.id,
374
- access: account.access,
608
+ access: scoped ? '' : (account.access ?? ''),
375
609
  quota: accountQuota,
376
610
  order: index + 1,
377
611
  account,
@@ -381,7 +615,7 @@ async function executeWithFallback(options) {
381
615
  const refreshError = route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID
382
616
  ? storage?.refresh?.mainLastRefreshError
383
617
  : route.account?.lastRefreshError;
384
- if (isPermanentRefreshError(refreshError))
618
+ if (!scoped && isPermanentRefreshError(refreshError))
385
619
  return [];
386
620
  if (stickyQuotaSnapshotIsFresh(route.quota, storage, Date.now(), modelId) &&
387
621
  decideStickyQuotaFailure({ quota: route.quota, modelId }).action ===
@@ -424,8 +658,10 @@ async function executeWithFallback(options) {
424
658
  return { allRoutes, candidates, retainAccountIds };
425
659
  }
426
660
  async function primaryQuotaRefreshConfirmsExhausted() {
661
+ if (!primaryAvailable())
662
+ return false;
427
663
  try {
428
- const quota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
664
+ const quota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
429
665
  const entry = quotaManager.getMain(mainAccountId);
430
666
  return Boolean(entry &&
431
667
  entry.refreshAfter > Date.now() &&
@@ -436,8 +672,10 @@ async function executeWithFallback(options) {
436
672
  }
437
673
  }
438
674
  async function primaryQuotaRefreshConfirmsModelScopeExhausted() {
675
+ if (!primaryAvailable())
676
+ return false;
439
677
  try {
440
- const quota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
678
+ const quota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
441
679
  const entry = quotaManager.getMain(mainAccountId);
442
680
  return Boolean(entry &&
443
681
  entry.refreshAfter > Date.now() &&
@@ -448,11 +686,15 @@ async function executeWithFallback(options) {
448
686
  }
449
687
  }
450
688
  function primaryCachedModelScopeExhausted() {
689
+ if (!primaryAvailable())
690
+ return false;
451
691
  const entry = quotaManager.getMain(mainAccountId);
452
692
  return Boolean(entry &&
453
693
  quotaSnapshotModelScopeIsExhausted(entry.quota, options.model.id));
454
694
  }
455
695
  function primaryFreshModelScopeExhausted() {
696
+ if (!primaryAvailable())
697
+ return false;
456
698
  const entry = quotaManager.getMain(mainAccountId);
457
699
  return Boolean(entry &&
458
700
  !quotaManager.isMainStale(options.model.id) &&
@@ -464,17 +706,21 @@ async function executeWithFallback(options) {
464
706
  });
465
707
  const usableOAuthById = new Map(usableOAuth.map((account) => [account.id, account]));
466
708
  for (const configured of storage?.accounts ?? []) {
709
+ if (configured.enabled === false)
710
+ continue;
467
711
  let response = null;
468
712
  let account = isOAuthAccount(configured)
469
713
  ? usableOAuthById.get(configured.id)
470
714
  : configured;
471
715
  if (!account) {
472
716
  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()))
717
+ (scoped
718
+ ? !scopedReady(configured)
719
+ : !configured.access ||
720
+ (configured.expires !== undefined &&
721
+ configured.expires <= Date.now()) ||
722
+ isPermanentRefreshError(configured.lastRefreshError) ||
723
+ refreshBackoffActive(configured.lastRefreshError, configured.id, Date.now(), tokenFingerprint(configured.refresh))))
478
724
  continue;
479
725
  account = configured;
480
726
  const quota = quotaManager.getFallback(configured.id, configured)?.quota ??
@@ -491,11 +737,12 @@ async function executeWithFallback(options) {
491
737
  continue;
492
738
  }
493
739
  if (isOAuthAccount(account)) {
494
- if (routeOptions.apiOnly === true || !account.access)
740
+ if (routeOptions.apiOnly === true ||
741
+ (scoped ? !scopedReady(account) : !account.access))
495
742
  continue;
496
743
  response = await sendAnthropicRequest({
497
744
  ...options,
498
- accessToken: account.access,
745
+ accessToken: scoped ? undefined : account.access,
499
746
  oauthAccountId: account.id,
500
747
  });
501
748
  }
@@ -530,15 +777,19 @@ async function executeWithFallback(options) {
530
777
  const router = getPiStickyRouter(options.storagePath);
531
778
  const initialInputBytes = Math.max(1, Buffer.byteLength(JSON.stringify(options.context)));
532
779
  let routes = await buildStickyRoutes(options.model.id);
533
- const mainPermanentlyUnavailable = isPermanentRefreshError(storage?.refresh?.mainLastRefreshError);
780
+ const mainPermanentlyUnavailable = scoped
781
+ ? !primaryAvailable()
782
+ : isPermanentRefreshError(storage?.refresh?.mainLastRefreshError);
534
783
  const incompleteQuotaPool = (routes.allRoutes.length === 0 && !mainPermanentlyUnavailable) ||
535
- routes.allRoutes.some((candidate) => !candidate.quota ||
536
- !stickyQuotaSnapshotIsFresh(candidate.quota, storage, Date.now(), options.model.id));
784
+ routes.allRoutes.some((candidate) => !quotaObservationIsFresh(candidate.quota, options.model.id));
537
785
  let resolution = await router.resolve({
538
786
  sessionId,
539
787
  family: stickyRouteFamilyForModel(options.model.id),
540
788
  modelId: options.model.id,
541
- candidates: routes.candidates,
789
+ affinityModelId: options.model.id,
790
+ // Existing affinity can survive a transient probe failure, but an
791
+ // incomplete pool cannot create a new balanced assignment.
792
+ candidates: incompleteQuotaPool ? [] : routes.candidates,
542
793
  retainAccountIds: routes.retainAccountIds,
543
794
  storage,
544
795
  inputBytes: initialInputBytes,
@@ -553,7 +804,9 @@ async function executeWithFallback(options) {
553
804
  }
554
805
  if (!resolution) {
555
806
  return createStickyNoRouteResponse({
556
- mainRefreshError: storage?.refresh?.mainLastRefreshError,
807
+ mainRefreshError: scoped
808
+ ? undefined
809
+ : storage?.refresh?.mainLastRefreshError,
557
810
  fallbackReauthLabels: getFallbackReauthLabels(storage),
558
811
  routeQuotas: routes.allRoutes.flatMap((route) => route.quota ? [route.quota] : []),
559
812
  modelId: options.model.id,
@@ -600,11 +853,12 @@ async function executeWithFallback(options) {
600
853
  }
601
854
  let permanentAuthFailure = preflight instanceof Response &&
602
855
  preflight.status === 401 &&
603
- route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID;
856
+ (Boolean(scoped) || route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID);
604
857
  if (preflight instanceof Response &&
605
858
  preflight.status === 401 &&
606
859
  route.account &&
607
- storage) {
860
+ storage &&
861
+ !scoped) {
608
862
  const authRouteId = route.id;
609
863
  try {
610
864
  const refreshed = await manager.refreshAccount(route.account, storage, {
@@ -730,11 +984,24 @@ async function executeWithFallback(options) {
730
984
  if (fallback)
731
985
  return fallback;
732
986
  }
987
+ if (!primaryAvailable()) {
988
+ if (!fallbackFirst) {
989
+ const fallback = await tryFallbackAccounts();
990
+ if (fallback)
991
+ return fallback;
992
+ }
993
+ return createStickyNoRouteResponse({
994
+ routeQuotas: (storage?.accounts ?? [])
995
+ .filter(isOAuthAccount)
996
+ .flatMap((account) => (account.quota ? [account.quota] : [])),
997
+ modelId: options.model.id,
998
+ });
999
+ }
733
1000
  if (isKillswitchEnabled(storage)) {
734
1001
  let mainQuota = quotaManager.getMain(mainAccountId)?.quota;
735
1002
  if (!mainQuota || quotaManager.isMainStale(options.model.id)) {
736
1003
  try {
737
- mainQuota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
1004
+ mainQuota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
738
1005
  }
739
1006
  catch { }
740
1007
  }
@@ -761,7 +1028,7 @@ async function executeWithFallback(options) {
761
1028
  }
762
1029
  const primary = await sendAnthropicRequest({
763
1030
  ...options,
764
- accessToken: options.primaryAccessToken,
1031
+ accessToken: primaryCredential,
765
1032
  oauthAccountId: STICKY_ROUTING_MAIN_ACCOUNT_ID,
766
1033
  });
767
1034
  const primaryPreflight = await firstStreamingError(primary);
@@ -786,22 +1053,24 @@ async function executeWithFallback(options) {
786
1053
  }
787
1054
  return primaryPreflight instanceof Response ? primaryPreflight : primary;
788
1055
  }
789
- export function streamCortexKitAnthropic(model, context, options) {
1056
+ export function streamCortexKitAnthropic(model, context, options, effortTransitions) {
790
1057
  const stream = createAssistantMessageEventStream();
791
1058
  void (async () => {
792
1059
  const output = createOutput(model);
793
1060
  stream.push({ type: 'start', partial: output });
794
1061
  try {
795
1062
  const accessToken = options?.apiKey ?? '';
796
- if (!accessToken)
797
- throw new Error('Missing Anthropic OAuth access token');
798
1063
  const storagePath = getPiAccountStoragePath();
1064
+ if (!accessToken &&
1065
+ getClaustrumMode(await loadAccounts(storagePath)) !== 'claustrum')
1066
+ throw new Error('Missing Anthropic OAuth access token');
799
1067
  const response = await executeWithFallback({
800
1068
  model,
801
1069
  context,
802
1070
  streamOptions: options,
803
1071
  primaryAccessToken: accessToken,
804
1072
  storagePath,
1073
+ effortTransitions,
805
1074
  });
806
1075
  if (!response.ok) {
807
1076
  throw new Error(`Anthropic request failed: HTTP ${response.status} ${await response.text()}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/pi-anthropic-auth",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,11 +36,11 @@
36
36
  "prepublishOnly": "bun run build"
37
37
  },
38
38
  "dependencies": {
39
- "@cortexkit/anthropic-auth-core": "1.21.0"
39
+ "@cortexkit/anthropic-auth-core": "1.23.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "@earendil-works/pi-ai": "*",
43
- "@earendil-works/pi-coding-agent": "*",
44
- "@earendil-works/pi-tui": "*"
42
+ "@earendil-works/pi-ai": ">=0.86.1",
43
+ "@earendil-works/pi-coding-agent": ">=0.86.1",
44
+ "@earendil-works/pi-tui": ">=0.86.1"
45
45
  }
46
46
  }