@cortexkit/pi-anthropic-auth 1.23.0 → 2.0.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,58 +1,16 @@
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';
1
+ import { applyClaudeCodeHeaders, applyCustomHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, createProviderErrorResponseScope, createSseFramer, createStickyNoRouteResponse, decideStickyQuotaFailure, decodeSseFrame, dumpDirectRequest, FAST_MODE_BETA, getCache1hPersistentMode, getClaudeCodeIdentityForVerifiedAccount, getDefaultCacheKeepRegistryDirectory, getFallbackReauthLabels, getQuotaCheckIntervalMs, getRoutingMode, getScopedQuotaWindowForModel, getThinkingPrefixMismatchBehavior, hasThinkingBindingControls, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isNativeTransientAccountError, isOAuthAccount, isPermanentRefreshError, isValidApiBaseURL, killswitchPassesPolicy, logger, MID_CONVERSATION_OUTPUT_CONFIG_BETA, mergeAnthropicBetas, NativeVaultNoAccountError, parseSseFrame, QuotaManager, quotaBackoffActive, quotaSnapshotHasStandardWindows, quotaSnapshotModelScopeIsExhausted, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, STICKY_ROUTING_MAIN_ACCOUNT_ID, StickySessionRouter, sendViaRelay, setDumpEnabled, shouldFallbackStatus, stickyQuotaSnapshotIsFresh, stickyRetryAfterWithJitter, stickyRouteFamilyForModel, THINKING_BINDING_CONTROLS_BETA, 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 { requirePiEnrollment } from './custody.js';
5
- import { getPiAccountStoragePath, getPiClaustrumConnectionOptions, } from './paths.js';
6
- function errorText(error) {
7
- return error instanceof Error ? error.message : String(error);
8
- }
4
+ import { getPiNativeRuntime, piAttemptAccessToken, } from './native.js';
5
+ import { getPiAccountStoragePath } from './paths.js';
6
+ import { buildPiOAuthMessagesUrl, fetchPiOAuth, requirePiOAuthOrigin, } from './transport.js';
7
+ /** Dump label for a physical send that failed before any response arrived. */
8
+ const PHYSICAL_SEND_FAILED = 'physical send failed before a response';
9
9
  let cacheKeepRegistry;
10
10
  let cacheKeepRegistryDirectory;
11
11
  const stickyRouters = new Map();
12
12
  const quotaManagers = new Map();
13
- const fallbackManagers = new Map();
14
- const mainAccountIdInitializations = new Map();
15
13
  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
- }
56
14
  function setBoundedService(map, key, value) {
57
15
  map.delete(key);
58
16
  map.set(key, value);
@@ -63,76 +21,92 @@ function setBoundedService(map, key, value) {
63
21
  map.delete(oldest);
64
22
  }
65
23
  }
66
- function ensurePiMainAccountId(storagePath) {
67
- let initialization = mainAccountIdInitializations.get(storagePath);
68
- if (!initialization) {
69
- initialization = getOrCreateMainAccountId(storagePath).catch((error) => {
70
- if (mainAccountIdInitializations.get(storagePath) === initialization) {
71
- mainAccountIdInitializations.delete(storagePath);
72
- }
73
- throw error;
74
- });
75
- setBoundedService(mainAccountIdInitializations, storagePath, initialization);
76
- }
77
- return initialization;
78
- }
79
- function getPiRoutingServices(storagePath, storage) {
80
- let quotaManager = quotaManagers.get(storagePath);
81
- let fallbackManager = fallbackManagers.get(storagePath);
82
- if (!quotaManager || !fallbackManager) {
24
+ function getPiRoutingServices(storagePath, snapshot) {
25
+ const storage = snapshot.policyStorage;
26
+ const native = getPiNativeRuntime(storagePath);
27
+ const identityKey = JSON.stringify(snapshot.accounts.map((account) => [
28
+ account.id,
29
+ account.accountIdentity,
30
+ account.source,
31
+ account.credentialId,
32
+ account.binding?.rowId,
33
+ account.binding?.credentialEpoch,
34
+ ]));
35
+ const existing = quotaManagers.get(storagePath);
36
+ let quotaManager = existing?.identityKey === identityKey ? existing.manager : undefined;
37
+ if (!quotaManager) {
83
38
  quotaManager = new QuotaManager({
84
39
  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 });
40
+ fetchQuotaSnapshot: async (request, dispatch) => {
41
+ const id = request.kind === 'main' ? 'main' : request.accountId;
42
+ if (!id)
43
+ throw new Error('Native quota request has no account identity');
44
+ return (await native.service()).fetchQuota(id, fetchPiOAuth, undefined, dispatch);
96
45
  },
97
46
  });
98
- fallbackManager = new FallbackAccountManager({
99
- configPath: storagePath,
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,
47
+ setBoundedService(quotaManagers, storagePath, {
48
+ identityKey,
49
+ manager: quotaManager,
115
50
  });
116
- setBoundedService(quotaManagers, storagePath, quotaManager);
117
- setBoundedService(fallbackManagers, storagePath, fallbackManager);
118
51
  }
119
52
  else {
120
53
  quotaManager.updateStorage(storage);
121
54
  }
122
- return { quotaManager, fallbackManager };
55
+ const quotas = quotaManager;
56
+ const getUsableFallbackAccounts = async (current, options = {}) => {
57
+ const snapshot = await native.view();
58
+ const accounts = new Map(snapshot.accounts.map((account) => [account.id, account]));
59
+ const usable = [];
60
+ for (const account of current.accounts) {
61
+ const view = accounts.get(account.id);
62
+ if (!isOAuthAccount(account) || !view?.enabled || view.type !== 'oauth')
63
+ continue;
64
+ if (view.source === 'vault' && view.state !== 'active')
65
+ continue;
66
+ if (isPermanentRefreshError(view.lastRefreshError) ||
67
+ (view.source === 'local' &&
68
+ (view.lastRefreshError?.nextRetryAt ?? 0) > Date.now()))
69
+ continue;
70
+ const needsProbe = view.source !== 'vault' ||
71
+ Boolean(current.quota?.minimumRemaining) ||
72
+ isKillswitchEnabled(current);
73
+ if (needsProbe &&
74
+ quotas.isFallbackStale(account.id, undefined, options.modelId, account) &&
75
+ !quotaBackoffActive(account.lastQuotaRefreshError, Date.now())) {
76
+ try {
77
+ await quotas.refreshFallback(account.id, '', account);
78
+ }
79
+ catch (error) {
80
+ // A transient account error means another writer is still saving
81
+ // this fallback. Rethrow it: swallowing it would leave the fallback
82
+ // without a quota reading, which can exclude it below and send the
83
+ // request through a different account. Other probe errors are
84
+ // ignored.
85
+ if (isNativeTransientAccountError(error))
86
+ throw error;
87
+ }
88
+ }
89
+ const quota = quotas.getFallback(account.id, account)?.quota ?? account.quota;
90
+ if ((!needsProbe || quotaSnapshotPassesPolicy(quota, current)) &&
91
+ quotaSnapshotPassesModelScope(quota, options.modelId))
92
+ usable.push({ ...account, quota });
93
+ }
94
+ return usable;
95
+ };
96
+ return { quotaManager, getUsableFallbackAccounts };
123
97
  }
124
- function getPiStickyRouter(storagePath) {
125
- const path = process.env.PI_ANTHROPIC_AUTH_ROUTING_STATE_FILE ||
126
- getStickyRoutingStatePath(storagePath);
98
+ async function getPiStickyRouter(storagePath) {
99
+ const { path, vaultSeed } = await getPiNativeRuntime(storagePath).routingStorage();
127
100
  let router = stickyRouters.get(path);
128
101
  if (!router) {
129
- router = new StickySessionRouter({ path });
102
+ router = new StickySessionRouter({ path, vaultSeed });
130
103
  setBoundedService(stickyRouters, path, router);
131
104
  }
105
+ router.setVaultSeed(vaultSeed);
132
106
  return router;
133
107
  }
134
108
  export async function clearPiStickyRoutingSession(storagePath, sessionId) {
135
- await getPiStickyRouter(storagePath).clear(sessionId);
109
+ await (await getPiStickyRouter(storagePath)).clear(sessionId);
136
110
  }
137
111
  function getPiCacheKeepRegistry() {
138
112
  const directory = process.env.PI_ANTHROPIC_AUTH_CACHEKEEP_REGISTRY_DIR ||
@@ -143,85 +117,81 @@ function getPiCacheKeepRegistry() {
143
117
  }
144
118
  return cacheKeepRegistry;
145
119
  }
146
- const scopedPrewarmAttempts = new Map();
120
+ const nativePrewarmAttempts = new Map();
147
121
  const cacheKeepManager = new CacheKeepManager({
148
- loadStorage: () => loadAccounts(getPiAccountStoragePath()),
122
+ fetchImpl: fetchPiOAuth,
123
+ loadStorage: async () => (await getPiNativeRuntime().view()).policyStorage,
149
124
  onTrackedSessionsChanged: (sessions) => getPiCacheKeepRegistry().publish(sessions),
150
125
  prepareHeaders: async (headers, target, attempt) => {
151
- const storagePath = target.accountStoragePath ?? getPiAccountStoragePath();
152
- const current = await loadAccounts(storagePath);
153
- if (target.accountStoragePath && !current)
126
+ requirePiOAuthOrigin(target.url);
127
+ const runtime = getPiNativeRuntime(target.accountStoragePath ?? getPiAccountStoragePath());
128
+ const id = target.oauthAccountId ?? STICKY_ROUTING_MAIN_ACCOUNT_ID;
129
+ const current = await runtime.view();
130
+ const body = JSON.parse(target.bodyText);
131
+ const served = await runtime.authorize(id, attempt.signal, typeof body.model === 'string' ? body.model : undefined);
132
+ if (!target.oauthAccountIdentity ||
133
+ served.accountIdentity !== target.oauthAccountIdentity)
134
+ throw new Error('CacheKeep account identity changed');
135
+ const identity = getClaudeCodeIdentityForVerifiedAccount(id === STICKY_ROUTING_MAIN_ACCOUNT_ID
136
+ ? (current.policyStorage.mainAccountId ?? id)
137
+ : id, served.accountIdentity);
138
+ headers.delete('anthropic-beta');
139
+ applyClaudeCodeHeaders(headers, piAttemptAccessToken(served), {
140
+ body,
141
+ identity,
142
+ extraBetas: [
143
+ CACHE_KEEP_EXTENDED_TTL_BETA,
144
+ ...(hasThinkingBindingControls(body)
145
+ ? [THINKING_BINDING_CONTROLS_BETA]
146
+ : []),
147
+ ],
148
+ });
149
+ if (body.speed === 'fast')
150
+ headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
151
+ nativePrewarmAttempts.set(attempt.id, { runtime, served, reported: false });
152
+ return headers;
153
+ },
154
+ retryOnUnauthorized: async ({ target, headers, attempt }) => {
155
+ const entry = nativePrewarmAttempts.get(attempt.id);
156
+ if (!entry)
154
157
  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
- }
183
- const authorization = headers.get('authorization') ?? '';
184
- const match = /^Bearer\s+(.+)$/i.exec(authorization);
185
- const accessToken = match?.[1];
186
- if (!accessToken)
158
+ await entry.runtime.report(entry.served, 401, 'direct').catch(() => {
159
+ logger.warn('native-auth', 'Pi CacheKeep auth-failure report unavailable');
160
+ });
161
+ entry.reported = true;
162
+ const retryBody = JSON.parse(target.bodyText);
163
+ const rotated = await entry.runtime
164
+ .retry(target.oauthAccountId ?? STICKY_ROUTING_MAIN_ACCOUNT_ID, entry.served, attempt.signal, typeof retryBody.model === 'string' ? retryBody.model : undefined)
165
+ .catch(() => undefined);
166
+ if (!rotated)
187
167
  return undefined;
188
- try {
189
- const body = JSON.parse(target.bodyText);
190
- await ensurePiMainAccountId(storagePath);
191
- const storage = await loadAccounts(storagePath);
192
- const identity = await resolveClaudeCodeIdentity(accessToken, typeof body.model === 'string' ? body.model : undefined, target.oauthAccountId ?? storage?.mainAccountId);
193
- headers.delete('anthropic-beta');
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
- });
204
- if (body.speed === 'fast') {
205
- headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
206
- }
207
- }
208
- catch {
209
- applyClaudeCodeHeaders(headers, accessToken);
210
- }
211
- return headers;
168
+ const rotatedHeaders = new Headers(headers);
169
+ rotatedHeaders.set('authorization', `Bearer ${piAttemptAccessToken(rotated)}`);
170
+ nativePrewarmAttempts.set(attempt.id, {
171
+ runtime: entry.runtime,
172
+ served: rotated,
173
+ reported: false,
174
+ });
175
+ return rotatedHeaders;
212
176
  },
213
177
  onResponse: async ({ attempt, status }) => {
214
- const entry = scopedPrewarmAttempts.get(attempt.id);
215
- if (entry)
216
- await entry.runtime.reportFailure(entry.receipt, status, 'direct');
178
+ const entry = nativePrewarmAttempts.get(attempt.id);
179
+ if (entry && status === 401 && !entry.reported)
180
+ await entry.runtime.report(entry.served, status, 'direct').catch(() => {
181
+ logger.warn('native-auth', 'Pi CacheKeep auth-failure report unavailable');
182
+ });
217
183
  },
218
184
  onComplete: ({ attempt }) => {
219
- scopedPrewarmAttempts.delete(attempt.id);
185
+ nativePrewarmAttempts.delete(attempt.id);
220
186
  },
221
187
  });
222
188
  export async function getPiTrackedCacheKeepSessions() {
223
189
  return getPiCacheKeepRegistry().list(cacheKeepManager.trackedSessions());
224
190
  }
191
+ /** Call prewarmNow directly to exercise its provider request without clock or timer mocks. */
192
+ export function __prewarmPiCacheKeepForTest(input) {
193
+ return cacheKeepManager.prewarmNow(input);
194
+ }
225
195
  function mapStopReason(reason) {
226
196
  switch (reason) {
227
197
  case 'end_turn':
@@ -308,66 +278,59 @@ export async function* parseSse(response) {
308
278
  if (!response.body)
309
279
  return;
310
280
  const reader = response.body.getReader();
311
- const decoder = new TextDecoder();
312
- let buffer = '';
281
+ // Core's byte framer ends frames at blank lines with CRLF, LF or CR line
282
+ // endings and keeps a multibyte character split across reads in one frame,
283
+ // so each frame is decoded once, whole. A frame without its closing blank
284
+ // line at end of stream is incomplete and dispatches nothing, as in
285
+ // EventSource.
286
+ const framer = createSseFramer();
287
+ // Only the stream's first frame may drop a leading byte-order mark.
288
+ let streamStart = true;
313
289
  try {
314
290
  while (true) {
315
291
  const { done, value } = await reader.read();
316
292
  if (done)
317
293
  break;
318
- buffer += decoder.decode(value, { stream: true });
319
- let boundary = buffer.indexOf('\n\n');
320
- while (boundary !== -1) {
321
- const frame = buffer.slice(0, boundary);
322
- buffer = buffer.slice(boundary + 2);
323
- boundary = buffer.indexOf('\n\n');
324
- for (const line of frame.split('\n')) {
325
- if (!line.startsWith('data:'))
326
- continue;
327
- const data = line.slice(5).trim();
328
- if (!data || data === '[DONE]')
329
- continue;
330
- yield JSON.parse(data);
331
- }
294
+ for (const frame of framer.push(value)) {
295
+ const data = parseSseFrame(decodeSseFrame(frame, { streamStart })).data;
296
+ streamStart = false;
297
+ if (!data || data === '[DONE]')
298
+ continue;
299
+ yield JSON.parse(data);
332
300
  }
333
301
  }
334
302
  }
335
303
  finally {
336
- // Do not cancel the reader on early abandon. `firstStreamingError()` peeks
337
- // the first SSE event from a `response.clone()` and then abandons this
338
- // generator; cancelling the cloned (tee'd) reader tears down the shared
339
- // underlying body, so the real `parseSse(response)` that streams the reply
340
- // reads zero events and the assistant message comes back empty. Releasing
341
- // the lock is enough — the abandoned clone branch is garbage-collected.
304
+ // Only the lock is released when the consumer stops early: cancelling the
305
+ // body on abandonment is the consumer's decision, not the parser's.
342
306
  reader.releaseLock();
343
307
  }
344
308
  }
345
309
  async function sendAnthropicRequest(options) {
346
- await ensurePiMainAccountId(options.storagePath);
347
- const storage = await loadAccounts(options.storagePath);
310
+ const native = getPiNativeRuntime(options.storagePath);
311
+ // Capture the serving service while authorization is open. Shutdown rejects
312
+ // new serving work immediately but drains writes for already-served attempts.
313
+ const servingRuntime = await native.service();
314
+ const snapshot = await native.view();
315
+ const storage = snapshot.policyStorage;
348
316
  setDumpEnabled(isDumpPersistentlyEnabled(storage));
349
- const accountIdentity = options.oauthAccountId === STICKY_ROUTING_MAIN_ACCOUNT_ID
350
- ? storage?.mainAccountId
351
- : options.oauthAccountId;
352
- const scoped = !options.apiAccount && getClaustrumMode(storage) === 'claustrum'
353
- ? getPiScopedRuntime(options.storagePath)
354
- : undefined;
355
317
  const routeId = options.oauthAccountId ?? STICKY_ROUTING_MAIN_ACCOUNT_ID;
356
- const roster = scoped
357
- ? (scoped.snapshot() ?? (await scoped.refresh()))
318
+ const selected = snapshot.accounts.find((account) => account.id === routeId);
319
+ let verifiedUuid = options.apiAccount ? undefined : selected?.accountIdentity;
320
+ if (!options.apiAccount && !verifiedUuid) {
321
+ if (selected?.source === 'vault')
322
+ throw new Error('Pi vault route identity is unasserted');
323
+ // Validation can learn the UUID of an account that has no verified identity.
324
+ // This lookup returns only that UUID; the model send authorizes the credential again.
325
+ verifiedUuid = (await native.authorize(routeId, options.streamOptions?.signal, options.model.id)).accountIdentity;
326
+ }
327
+ let nextAttempt;
328
+ const identity = verifiedUuid
329
+ ? getClaudeCodeIdentityForVerifiedAccount(routeId === STICKY_ROUTING_MAIN_ACCOUNT_ID
330
+ ? (storage.mainAccountId ?? routeId)
331
+ : routeId, verifiedUuid)
358
332
  : 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;
370
- const { body, bodyText } = await buildAnthropicRequest(options.model.id, options.context, options.streamOptions, {
333
+ const { body, bodyText, hostTools } = await buildAnthropicRequest(options.model.id, options.context, options.streamOptions, {
371
334
  enabled: isCache1hPersistentlyEnabled(storage),
372
335
  mode: getCache1hPersistentMode(storage),
373
336
  }, isFastModePersistentlyEnabled(storage), identity, {
@@ -376,10 +339,11 @@ async function sendAnthropicRequest(options) {
376
339
  ? 'account-default'
377
340
  : getThinkingPrefixMismatchBehavior(storage),
378
341
  });
342
+ options.onResolvedTools?.(hostTools);
379
343
  const fastMode = body.speed === 'fast';
380
344
  const headers = options.apiAccount
381
- ? configureApiRouteHeaders(options.apiAccount, fastMode)
382
- : applyClaudeCodeHeaders(new Headers(), scoped ? '' : (options.accessToken ?? ''), {
345
+ ? new Headers()
346
+ : applyClaudeCodeHeaders(new Headers(), '', {
383
347
  body,
384
348
  identity,
385
349
  extraBetas: [
@@ -391,48 +355,120 @@ async function sendAnthropicRequest(options) {
391
355
  : []),
392
356
  ],
393
357
  });
394
- if (!options.apiAccount && fastMode) {
358
+ if (!options.apiAccount && fastMode)
395
359
  headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
396
- }
397
360
  const relayAffinity = options.streamOptions?.sessionId ?? null;
398
- const input = options.apiAccount
399
- ? buildExplicitBaseMessagesUrl(options.apiAccount.baseURL)
400
- : new URL('/v1/messages?beta=true', options.model.baseUrl);
361
+ let input = options.apiAccount
362
+ ? new URL('https://api.anthropic.com/v1/messages')
363
+ : buildPiOAuthMessagesUrl(options.model.baseUrl);
401
364
  const init = {
402
365
  method: 'POST',
403
366
  headers,
404
367
  body: bodyText,
405
368
  signal: options.streamOptions?.signal,
369
+ ...(!options.apiAccount && { redirect: 'error' }),
406
370
  };
407
- await cacheKeepManager.track({
408
- sessionId: relayAffinity,
409
- url: input.toString(),
410
- headers,
411
- bodyText,
412
- storage,
413
- cacheMode: !options.apiAccount && isCacheKeepHybridActive(storage)
414
- ? 'hybrid'
415
- : 'disabled',
416
- oauthAccountId: options.oauthAccountId,
417
- oauthAccountIdentity: verifiedUuid ?? identity?.accountUuid,
418
- accountStoragePath: options.storagePath,
419
- });
371
+ if (!options.apiAccount)
372
+ await cacheKeepManager.track({
373
+ sessionId: relayAffinity,
374
+ url: input.toString(),
375
+ headers,
376
+ bodyText,
377
+ storage,
378
+ cacheMode: isCacheKeepHybridActive(storage) ? 'hybrid' : 'disabled',
379
+ oauthAccountId: routeId,
380
+ oauthAccountIdentity: verifiedUuid,
381
+ accountStoragePath: options.storagePath,
382
+ });
383
+ const reported = new Set();
384
+ let retried401 = false;
385
+ let lastTransport;
386
+ const used = new Set();
420
387
  const report = async (attempt, status, source) => {
421
- if (!scoped || status !== 401)
388
+ if (status !== 401 || reported.has(attempt))
422
389
  return;
423
- await scoped.reportFailure(attempt, status, source).catch(() => {
424
- logger.warn('claustrum', 'Pi scoped auth-failure report unavailable');
390
+ reported.add(attempt);
391
+ await native.report(attempt, status, source).catch(() => {
392
+ logger.warn('native-auth', 'Pi auth-failure report unavailable');
425
393
  });
426
394
  };
395
+ const markServed = (attempt) => {
396
+ if (used.has(attempt))
397
+ return;
398
+ used.add(attempt);
399
+ native.usagePublications.publish(() => native.markUsed(attempt, servingRuntime));
400
+ };
401
+ const takeAttempt = async () => {
402
+ const attempt = nextAttempt ??
403
+ (await native.authorize(routeId, init.signal ?? undefined, options.model.id));
404
+ nextAttempt = undefined;
405
+ if (attempt.accountIdentity !== verifiedUuid)
406
+ throw new Error('Pi native OAuth route identity changed');
407
+ return attempt;
408
+ };
427
409
  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}`);
410
+ let attempt;
411
+ let apiSubject;
412
+ // The secret this physical send goes out with; a provider error may repeat it.
413
+ let secret = '';
414
+ if (options.apiAccount) {
415
+ const credential = await (await native.service()).authorizeApi(options.apiAccount.id, init.signal ?? undefined);
416
+ apiSubject = credential.subject;
417
+ secret = credential.apiKey ?? '';
418
+ input = buildExplicitBaseMessagesUrl(credential.baseURL);
419
+ const authorized = configureApiRouteHeaders({
420
+ ...credential,
421
+ id: options.apiAccount.id,
422
+ }, fastMode);
423
+ init.headers = authorized;
424
+ }
425
+ else {
426
+ attempt = await takeAttempt();
427
+ secret = piAttemptAccessToken(attempt);
428
+ headers.set('authorization', `Bearer ${secret}`);
429
+ }
430
+ lastTransport = 'direct';
431
+ // The response scope of the latest physical send. Each send gets its own
432
+ // before the request leaves, so a rejected 401 is already scrubbed when
433
+ // it is reported, dumped or cancelled.
434
+ const physical = {};
435
+ const send = async () => {
436
+ const scope = createProviderErrorResponseScope([secret], 'event-stream');
437
+ physical.scope = scope;
438
+ return scope.redactResponse(await fetch(input, init));
439
+ };
440
+ let response;
434
441
  try {
435
- const response = await fetch(input, init);
442
+ response = await send();
443
+ if (attempt && response.status === 401) {
444
+ // Report the rejected credential version before retrying, even if its replacement later succeeds.
445
+ await report(attempt, response.status, 'direct');
446
+ const rotated = !retried401 && !init.signal?.aborted
447
+ ? await native
448
+ .retry(routeId, attempt, init.signal ?? undefined, options.model.id)
449
+ .catch(() => undefined)
450
+ : undefined;
451
+ if (rotated) {
452
+ retried401 = true;
453
+ await dumpDirectRequest({
454
+ affinity: relayAffinity,
455
+ route: options.route ?? 'oauth',
456
+ status: response.status,
457
+ bodyText,
458
+ url: input.toString(),
459
+ method: init.method,
460
+ headers,
461
+ });
462
+ // Retire the rejected send's credentials before cancelling its
463
+ // body, since that cancellation may never settle.
464
+ physical.scope?.dispose();
465
+ await response.body?.cancel().catch(() => { });
466
+ attempt = rotated;
467
+ secret = piAttemptAccessToken(rotated);
468
+ headers.set('authorization', `Bearer ${secret}`);
469
+ response = await send();
470
+ }
471
+ }
436
472
  if (attempt)
437
473
  await report(attempt, response.status, 'direct');
438
474
  await dumpDirectRequest({
@@ -443,48 +479,95 @@ async function sendAnthropicRequest(options) {
443
479
  bodyText,
444
480
  url: input.toString(),
445
481
  method: init.method,
446
- headers,
482
+ headers: init.headers,
447
483
  });
448
- return response;
449
484
  }
450
- catch (error) {
485
+ catch (caught) {
486
+ // No response leaves this send, so its credentials retire here.
487
+ physical.scope?.dispose();
488
+ const scope = createProviderErrorResponseScope([secret], 'event-stream');
489
+ const error = scope.redactError(caught);
490
+ scope.dispose();
451
491
  await dumpDirectRequest({
452
492
  affinity: relayAffinity,
453
493
  route: options.route ??
454
494
  (options.apiAccount ? `api:${options.apiAccount.id}` : 'oauth'),
455
- error: errorText(error),
495
+ // A fixed label: the error text can repeat provider output or a credential.
496
+ error: PHYSICAL_SEND_FAILED,
456
497
  bodyText,
457
498
  url: input.toString(),
458
499
  method: init.method,
459
- headers,
500
+ headers: init.headers,
460
501
  });
461
502
  throw error;
462
503
  }
504
+ if (apiSubject && response.ok) {
505
+ const subject = apiSubject;
506
+ native.usagePublications.publish(() => servingRuntime.publishApi(subject, { lastUsed: Date.now() }));
507
+ }
508
+ if (attempt && response.ok)
509
+ markServed(attempt);
510
+ return response;
463
511
  };
464
512
  if (options.apiAccount)
465
513
  return directFetch();
466
- return sendViaRelay({
467
- config: getRelayConfig(storage),
514
+ let relay401Attempt;
515
+ let relayAttempt;
516
+ const relayReports = [];
517
+ // The relay scrubs each physical relay attempt's responses and errors with
518
+ // that attempt's own credentials and the relay token; a direct fallback is
519
+ // scrubbed by directFetch.
520
+ const sendRelayAttempt = async () => sendViaRelay({
521
+ config: await (await native.service()).getRelayConfig(),
468
522
  input,
469
523
  init,
470
524
  headers,
471
525
  body: bodyText,
472
526
  fallback: directFetch,
473
527
  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,
528
+ authorizeAttempt: async () => {
529
+ const attempt = await takeAttempt();
530
+ relayAttempt = attempt;
531
+ lastTransport = 'relay';
532
+ const authorizedHeaders = new Headers(headers);
533
+ const access = piAttemptAccessToken(attempt);
534
+ authorizedHeaders.set('authorization', `Bearer ${access}`);
535
+ return {
536
+ headers: authorizedHeaders,
537
+ errorSecrets: [access],
538
+ onUpstreamStatus: (status) => {
539
+ if (status === 401) {
540
+ relay401Attempt = attempt;
541
+ // The relay can report an upstream 401 after returning the streaming response.
542
+ relayReports.push(report(attempt, status, 'relay_status_field'));
543
+ }
544
+ },
545
+ };
546
+ },
487
547
  });
548
+ let response = await sendRelayAttempt();
549
+ await Promise.all(relayReports);
550
+ if (relay401Attempt &&
551
+ response.status === 401 &&
552
+ !retried401 &&
553
+ !init.signal?.aborted) {
554
+ const rotated = await native
555
+ .retry(routeId, relay401Attempt, init.signal ?? undefined, options.model.id)
556
+ .catch(() => undefined);
557
+ if (rotated) {
558
+ retried401 = true;
559
+ await response.body?.cancel().catch(() => { });
560
+ // Use the replacement credential only for the retried relay request.
561
+ // A direct request after relay failure obtains its own authorization.
562
+ nextAttempt = rotated;
563
+ relay401Attempt = undefined;
564
+ response = await sendRelayAttempt();
565
+ await Promise.all(relayReports);
566
+ }
567
+ }
568
+ if (response.ok && lastTransport === 'relay' && relayAttempt)
569
+ markServed(relayAttempt);
570
+ return response;
488
571
  }
489
572
  function quotaSnapshotIsExhausted(quota) {
490
573
  return ['five_hour', 'seven_day'].some((key) => (quota?.[key]?.remainingPercent ?? 1) <= 0);
@@ -493,55 +576,154 @@ export function primaryResponseAllowsApiFallback(preflight) {
493
576
  return (preflight === 'rate_limit_error' ||
494
577
  (preflight instanceof Response && preflight.status === 429));
495
578
  }
496
- async function firstStreamingError(response) {
497
- if (!response.ok)
498
- return response;
499
- const clone = response.clone();
579
+ /**
580
+ * Bytes read while looking for a successful stream's first event. Comments
581
+ * and heartbeats carry no event, so a stream of them alone must not keep the
582
+ * inspection reading (and buffering) without end.
583
+ */
584
+ const FIRST_EVENT_INSPECTION_BYTES = 65_536;
585
+ /**
586
+ * Classify a successful stream by its first event without a tee: the bytes
587
+ * read are replayed in front of the rest of the body, so cancelling the
588
+ * returned response cancels the provider body itself. `preflight` is
589
+ * `rate_limit_error` for a streamed rate limit (the provider body is then
590
+ * cancelled and only the bytes read are replayed), else the returned
591
+ * response. A stream whose first event does not arrive within the budget, or
592
+ * cannot be read, is returned unclassified.
593
+ */
594
+ export async function firstStreamingError(response) {
595
+ if (!response.ok || !response.body)
596
+ return { response, preflight: response };
597
+ const reader = response.body.getReader();
598
+ const chunks = [];
599
+ const framer = createSseFramer({
600
+ maxFrameBytes: FIRST_EVENT_INSPECTION_BYTES,
601
+ });
602
+ let bytes = 0;
603
+ let rateLimited = false;
604
+ let decided = false;
605
+ // Only the stream's first frame may drop a leading byte-order mark.
606
+ let streamStart = true;
500
607
  try {
501
- for await (const event of parseSse(clone)) {
502
- if (event.type === 'error' &&
503
- typeof event.delta?.type === 'string' &&
504
- event.delta.type === 'rate_limit_error') {
505
- return 'rate_limit_error';
608
+ while (!decided && bytes < FIRST_EVENT_INSPECTION_BYTES) {
609
+ const { done, value } = await reader.read();
610
+ if (done)
611
+ break;
612
+ chunks.push(value);
613
+ bytes += value.byteLength;
614
+ for (const frame of framer.push(value)) {
615
+ const data = parseSseFrame(decodeSseFrame(frame, { streamStart })).data;
616
+ streamStart = false;
617
+ if (!data || data === '[DONE]')
618
+ continue;
619
+ const event = JSON.parse(data);
620
+ // Anthropic reports a streamed rate limit as an `error` event whose
621
+ // `error.type` is `rate_limit_error`; only that event is a rate limit.
622
+ rateLimited =
623
+ event.type === 'error' && event.error?.type === 'rate_limit_error';
624
+ decided = true;
625
+ break;
506
626
  }
507
- return response;
508
627
  }
509
628
  }
510
629
  catch {
511
- return response;
630
+ // An unreadable body, an oversized first frame or a first event that is
631
+ // not JSON is not treated as a rate limit: the response is returned for
632
+ // the consumer to read, and an unreadable body fails that read too.
512
633
  }
513
- return response;
634
+ // The provider reader is released as soon as nothing will read it again:
635
+ // at end of stream, after a read failure, or once a cancellation settles.
636
+ const release = () => {
637
+ try {
638
+ reader.releaseLock();
639
+ }
640
+ catch {
641
+ // Already released.
642
+ }
643
+ };
644
+ const replay = (rest) => new Response(new ReadableStream({
645
+ start(controller) {
646
+ for (const chunk of chunks)
647
+ controller.enqueue(chunk);
648
+ if (!rest)
649
+ controller.close();
650
+ },
651
+ async pull(controller) {
652
+ let next;
653
+ try {
654
+ next = await reader.read();
655
+ }
656
+ catch (error) {
657
+ release();
658
+ throw error;
659
+ }
660
+ if (next.done) {
661
+ release();
662
+ controller.close();
663
+ }
664
+ else
665
+ controller.enqueue(next.value);
666
+ },
667
+ async cancel(reason) {
668
+ if (!rest)
669
+ return;
670
+ try {
671
+ await reader.cancel(reason);
672
+ }
673
+ finally {
674
+ release();
675
+ }
676
+ },
677
+ }), {
678
+ status: response.status,
679
+ statusText: response.statusText,
680
+ headers: response.headers,
681
+ });
682
+ if (rateLimited) {
683
+ // Routing replaces this response. Cancel the provider body and clear its
684
+ // copied redaction material; retain the inspected error event for callers
685
+ // that still return the replay response.
686
+ await reader.cancel().catch(() => { });
687
+ release();
688
+ return { response: replay(false), preflight: 'rate_limit_error' };
689
+ }
690
+ const inspected = replay(true);
691
+ return { response: inspected, preflight: inspected };
514
692
  }
515
693
  async function executeWithFallback(options) {
516
- await ensurePiMainAccountId(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));
534
- const { quotaManager, fallbackManager: manager } = getPiRoutingServices(options.storagePath, storage);
535
- let mainAccountId = scoped
536
- ? roster?.primary?.accountId
537
- : storage?.mainAccountId;
694
+ const native = getPiNativeRuntime(options.storagePath);
695
+ let snapshot = await native.requestView(options.streamOptions?.signal);
696
+ let storage = snapshot.policyStorage;
697
+ const accountView = (id) => snapshot.accounts.find((account) => account.id === id);
698
+ const vaultAccountIdentities = () => {
699
+ if (snapshot.mode !== 'claustrum')
700
+ return undefined;
701
+ const identities = new Map();
702
+ for (const account of snapshot.accounts) {
703
+ if (account.source === 'vault' && account.accountIdentity)
704
+ identities.set(account.id, account.accountIdentity);
705
+ }
706
+ return identities;
707
+ };
708
+ const nativeReady = (id) => {
709
+ const account = accountView(id);
710
+ return Boolean(account?.type === 'oauth' &&
711
+ account.enabled &&
712
+ (account.source !== 'vault' || account.state === 'active') &&
713
+ !isPermanentRefreshError(account.lastRefreshError) &&
714
+ (account.source !== 'local' ||
715
+ (account.lastRefreshError?.nextRetryAt ?? 0) <= Date.now()));
716
+ };
717
+ const primaryAvailable = () => nativeReady(STICKY_ROUTING_MAIN_ACCOUNT_ID);
718
+ let { quotaManager, getUsableFallbackAccounts } = getPiRoutingServices(options.storagePath, snapshot);
719
+ let mainAccountId = accountView(STICKY_ROUTING_MAIN_ACCOUNT_ID)?.accountIdentity;
538
720
  quotaManager.seedMainFromStorage(storage, mainAccountId);
539
721
  quotaManager.seedFallbacksFromAccounts((storage?.accounts ?? []).filter(isOAuthAccount));
540
722
  function quotaObservationIsFresh(quota, modelId) {
541
723
  if (stickyQuotaSnapshotIsFresh(quota, storage, Date.now(), modelId))
542
724
  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.
725
+ // A successful usage response without quota windows is not a failed poll.
726
+ // Pi can serve that OAuth account without a known remaining capacity.
545
727
  if (!quota ||
546
728
  quota.five_hour ||
547
729
  quota.seven_day ||
@@ -553,37 +735,33 @@ async function executeWithFallback(options) {
553
735
  return (Date.now() - quota.checkedAt < age &&
554
736
  (!scopedWindow || Date.now() - scopedWindow.checkedAt < age));
555
737
  }
738
+ async function refreshRoutingSnapshot() {
739
+ snapshot = await native.view();
740
+ storage = snapshot.policyStorage;
741
+ mainAccountId = accountView(STICKY_ROUTING_MAIN_ACCOUNT_ID)?.accountIdentity;
742
+ ({ quotaManager, getUsableFallbackAccounts } = getPiRoutingServices(options.storagePath, snapshot));
743
+ quotaManager.seedMainFromStorage(storage, mainAccountId);
744
+ quotaManager.seedFallbacksFromAccounts(storage.accounts.filter(isOAuthAccount));
745
+ }
556
746
  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
- }
747
+ await refreshRoutingSnapshot();
567
748
  const mainEntry = quotaManager.getMain(mainAccountId);
568
749
  let mainQuota = mainEntry?.quota;
569
750
  if (primaryAvailable() &&
570
751
  !stickyQuotaSnapshotIsFresh(mainEntry?.quota, storage, Date.now(), modelId)) {
571
752
  try {
572
- mainQuota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
753
+ mainQuota = await quotaManager.refreshMain(mainAccountId, '');
573
754
  }
574
755
  catch { }
575
756
  }
576
- const usableFallbacks = await manager.getUsableFallbackAccounts(storage, {
757
+ const usableFallbacks = await getUsableFallbackAccounts(storage, {
577
758
  modelId,
578
759
  });
579
760
  const usableById = new Map(usableFallbacks.map((account) => [account.id, account]));
580
761
  const allRoutes = [];
581
- if (primaryAvailable() &&
582
- (scoped ||
583
- !isPermanentRefreshError(storage?.refresh?.mainLastRefreshError))) {
762
+ if (primaryAvailable()) {
584
763
  allRoutes.push({
585
764
  id: STICKY_ROUTING_MAIN_ACCOUNT_ID,
586
- access: primaryCredential,
587
765
  quota: mainQuota,
588
766
  order: 0,
589
767
  });
@@ -592,20 +770,17 @@ async function executeWithFallback(options) {
592
770
  if (configured.enabled === false || !isOAuthAccount(configured))
593
771
  continue;
594
772
  const account = usableById.get(configured.id) ?? configured;
595
- if (scoped
596
- ? !scopedReady(account)
597
- : !account.access || isPermanentRefreshError(account.lastRefreshError))
773
+ if (!nativeReady(account.id))
598
774
  continue;
599
775
  let accountQuota = quotaManager.getFallback(account.id, account)?.quota ?? account.quota;
600
776
  if (!stickyQuotaSnapshotIsFresh(accountQuota, storage, Date.now(), modelId)) {
601
777
  try {
602
- accountQuota = await quotaManager.refreshFallback(account.id, scoped ? '' : (account.access ?? ''), account);
778
+ accountQuota = await quotaManager.refreshFallback(account.id, '', account);
603
779
  }
604
780
  catch { }
605
781
  }
606
782
  allRoutes.push({
607
783
  id: account.id,
608
- access: scoped ? '' : (account.access ?? ''),
609
784
  quota: accountQuota,
610
785
  order: index + 1,
611
786
  account,
@@ -615,7 +790,7 @@ async function executeWithFallback(options) {
615
790
  const refreshError = route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID
616
791
  ? storage?.refresh?.mainLastRefreshError
617
792
  : route.account?.lastRefreshError;
618
- if (!scoped && isPermanentRefreshError(refreshError))
793
+ if (isPermanentRefreshError(refreshError))
619
794
  return [];
620
795
  if (stickyQuotaSnapshotIsFresh(route.quota, storage, Date.now(), modelId) &&
621
796
  decideStickyQuotaFailure({ quota: route.quota, modelId }).action ===
@@ -658,10 +833,11 @@ async function executeWithFallback(options) {
658
833
  return { allRoutes, candidates, retainAccountIds };
659
834
  }
660
835
  async function primaryQuotaRefreshConfirmsExhausted() {
661
- if (!primaryAvailable())
836
+ await refreshRoutingSnapshot();
837
+ if (!primaryAvailable() || !mainAccountId)
662
838
  return false;
663
839
  try {
664
- const quota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
840
+ const quota = await quotaManager.refreshMain(mainAccountId, '');
665
841
  const entry = quotaManager.getMain(mainAccountId);
666
842
  return Boolean(entry &&
667
843
  entry.refreshAfter > Date.now() &&
@@ -672,10 +848,11 @@ async function executeWithFallback(options) {
672
848
  }
673
849
  }
674
850
  async function primaryQuotaRefreshConfirmsModelScopeExhausted() {
851
+ await refreshRoutingSnapshot();
675
852
  if (!primaryAvailable())
676
853
  return false;
677
854
  try {
678
- const quota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
855
+ const quota = await quotaManager.refreshMain(mainAccountId, '');
679
856
  const entry = quotaManager.getMain(mainAccountId);
680
857
  return Boolean(entry &&
681
858
  entry.refreshAfter > Date.now() &&
@@ -701,56 +878,36 @@ async function executeWithFallback(options) {
701
878
  quotaSnapshotModelScopeIsExhausted(entry.quota, options.model.id));
702
879
  }
703
880
  async function tryFallbackAccounts(routeOptions = {}) {
704
- const usableOAuth = await manager.getUsableFallbackAccounts(storage, {
881
+ const usableOAuth = await getUsableFallbackAccounts(storage, {
705
882
  modelId: options.model.id,
706
883
  });
707
884
  const usableOAuthById = new Map(usableOAuth.map((account) => [account.id, account]));
708
- for (const configured of storage?.accounts ?? []) {
885
+ for (const configured of storage.accounts) {
709
886
  if (configured.enabled === false)
710
887
  continue;
711
- let response = null;
712
- let account = isOAuthAccount(configured)
713
- ? usableOAuthById.get(configured.id)
714
- : configured;
715
- if (!account) {
716
- if (!isOAuthAccount(configured) ||
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))))
888
+ let response;
889
+ if (isOAuthAccount(configured)) {
890
+ if (routeOptions.apiOnly || !nativeReady(configured.id))
724
891
  continue;
725
- account = configured;
726
- const quota = quotaManager.getFallback(configured.id, configured)?.quota ??
727
- configured.quota;
728
- if (quotaSnapshotHasStandardWindows(quota))
892
+ const account = usableOAuthById.get(configured.id) ?? configured;
893
+ const quota = quotaManager.getFallback(account.id, account)?.quota ?? account.quota;
894
+ if (!usableOAuthById.has(account.id) &&
895
+ quotaSnapshotHasStandardWindows(quota))
729
896
  continue;
730
897
  if (isKillswitchEnabled(storage) &&
731
- !killswitchPassesPolicy(quota, storage, configured.id, options.model.id))
732
- continue;
733
- }
734
- else if (isOAuthAccount(account) &&
735
- isKillswitchEnabled(storage) &&
736
- !killswitchPassesPolicy(quotaManager.getFallback(account.id, account)?.quota ?? account.quota, storage, account.id, options.model.id)) {
737
- continue;
738
- }
739
- if (isOAuthAccount(account)) {
740
- if (routeOptions.apiOnly === true ||
741
- (scoped ? !scopedReady(account) : !account.access))
898
+ !killswitchPassesPolicy(quota, storage, account.id, options.model.id))
742
899
  continue;
743
900
  response = await sendAnthropicRequest({
744
901
  ...options,
745
- accessToken: scoped ? undefined : account.access,
746
902
  oauthAccountId: account.id,
747
903
  });
748
904
  }
749
- else if (routeOptions.includeApiRoutes === true &&
750
- isApiKeyAccount(account) &&
751
- account.enabled !== false &&
752
- account.apiKey &&
753
- isValidApiBaseURL(account.baseURL)) {
905
+ else if (routeOptions.includeApiRoutes) {
906
+ const account = accountView(configured.id);
907
+ if (!account?.enabled ||
908
+ account.type !== 'api' ||
909
+ !isValidApiBaseURL(account.baseURL))
910
+ continue;
754
911
  response = await sendAnthropicRequest({
755
912
  ...options,
756
913
  apiAccount: account,
@@ -758,28 +915,24 @@ async function executeWithFallback(options) {
758
915
  }
759
916
  if (!response)
760
917
  continue;
761
- const preflight = await firstStreamingError(response);
762
- if (preflight instanceof Response && preflight.ok) {
763
- await manager.markUsed(account);
918
+ const inspected = await firstStreamingError(response);
919
+ const preflight = inspected.preflight;
920
+ if (preflight instanceof Response && preflight.ok)
764
921
  return preflight;
765
- }
766
922
  if (preflight instanceof Response &&
767
- !shouldFallbackStatus(preflight.status, storage)) {
923
+ !shouldFallbackStatus(preflight.status, storage))
768
924
  return preflight;
769
- }
770
- await response.body?.cancel().catch(() => { });
925
+ await inspected.response.body?.cancel().catch(() => { });
771
926
  }
772
927
  return null;
773
928
  }
774
929
  const routingMode = getRoutingMode(storage);
775
930
  if (routingMode === 'sticky-balanced' && options.streamOptions?.sessionId) {
776
931
  const sessionId = options.streamOptions.sessionId;
777
- const router = getPiStickyRouter(options.storagePath);
932
+ const router = await getPiStickyRouter(options.storagePath);
778
933
  const initialInputBytes = Math.max(1, Buffer.byteLength(JSON.stringify(options.context)));
779
934
  let routes = await buildStickyRoutes(options.model.id);
780
- const mainPermanentlyUnavailable = scoped
781
- ? !primaryAvailable()
782
- : isPermanentRefreshError(storage?.refresh?.mainLastRefreshError);
935
+ const mainPermanentlyUnavailable = !primaryAvailable();
783
936
  const incompleteQuotaPool = (routes.allRoutes.length === 0 && !mainPermanentlyUnavailable) ||
784
937
  routes.allRoutes.some((candidate) => !quotaObservationIsFresh(candidate.quota, options.model.id));
785
938
  let resolution = await router.resolve({
@@ -787,10 +940,11 @@ async function executeWithFallback(options) {
787
940
  family: stickyRouteFamilyForModel(options.model.id),
788
941
  modelId: options.model.id,
789
942
  affinityModelId: options.model.id,
790
- // Existing affinity can survive a transient probe failure, but an
791
- // incomplete pool cannot create a new balanced assignment.
943
+ // Keep an existing route after a temporary quota poll failure, but do not
944
+ // choose a new balanced route until the account pool has current quota data.
792
945
  candidates: incompleteQuotaPool ? [] : routes.candidates,
793
946
  retainAccountIds: routes.retainAccountIds,
947
+ accountIdentities: vaultAccountIdentities(),
794
948
  storage,
795
949
  inputBytes: initialInputBytes,
796
950
  });
@@ -803,10 +957,10 @@ async function executeWithFallback(options) {
803
957
  throw error;
804
958
  }
805
959
  if (!resolution) {
960
+ if (snapshot.mode === 'claustrum')
961
+ throw new NativeVaultNoAccountError();
806
962
  return createStickyNoRouteResponse({
807
- mainRefreshError: scoped
808
- ? undefined
809
- : storage?.refresh?.mainLastRefreshError,
963
+ mainRefreshError: storage?.refresh?.mainLastRefreshError,
810
964
  fallbackReauthLabels: getFallbackReauthLabels(storage),
811
965
  routeQuotas: routes.allRoutes.flatMap((route) => route.quota ? [route.quota] : []),
812
966
  modelId: options.model.id,
@@ -816,15 +970,10 @@ async function executeWithFallback(options) {
816
970
  if (resolution && route) {
817
971
  const sendRoute = (selected) => sendAnthropicRequest({
818
972
  ...options,
819
- accessToken: selected.access,
820
973
  oauthAccountId: selected.id,
821
974
  route: `sticky:${selected.id}`,
822
975
  });
823
- const completeRoute = async (selected, response, markUsed = true) => {
824
- if (markUsed && selected.account)
825
- await manager.markUsed(selected.account);
826
- return response;
827
- };
976
+ const completeRoute = async (_selected, response, _markUsed = true) => response;
828
977
  const proactiveQuotaDecision = stickyQuotaSnapshotIsFresh(route.quota, storage, Date.now(), options.model.id)
829
978
  ? decideStickyQuotaFailure({
830
979
  quota: route.quota,
@@ -846,44 +995,11 @@ async function executeWithFallback(options) {
846
995
  },
847
996
  }), false);
848
997
  }
849
- let response = await sendRoute(route);
850
- let preflight = await firstStreamingError(response);
998
+ const { response, preflight } = await firstStreamingError(await sendRoute(route));
851
999
  if (preflight instanceof Response && preflight.ok) {
852
1000
  return completeRoute(route, preflight);
853
1001
  }
854
- let permanentAuthFailure = preflight instanceof Response &&
855
- preflight.status === 401 &&
856
- (Boolean(scoped) || route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID);
857
- if (preflight instanceof Response &&
858
- preflight.status === 401 &&
859
- route.account &&
860
- storage &&
861
- !scoped) {
862
- const authRouteId = route.id;
863
- try {
864
- const refreshed = await manager.refreshAccount(route.account, storage, {
865
- force: true,
866
- });
867
- if (refreshed.access) {
868
- route = { ...route, access: refreshed.access, account: refreshed };
869
- }
870
- await preflight.body?.cancel().catch(() => { });
871
- response = await sendRoute(route);
872
- preflight = await firstStreamingError(response);
873
- if (preflight instanceof Response && preflight.ok) {
874
- return completeRoute(route, preflight);
875
- }
876
- permanentAuthFailure =
877
- preflight instanceof Response && preflight.status === 401;
878
- }
879
- catch (error) {
880
- const latest = await loadAccounts(options.storagePath);
881
- const refreshError = latest?.accounts.find((account) => account.id === authRouteId && isOAuthAccount(account))?.lastRefreshError;
882
- if (!isPermanentRefreshError(refreshError))
883
- throw error;
884
- permanentAuthFailure = true;
885
- }
886
- }
1002
+ const permanentAuthFailure = preflight instanceof Response && preflight.status === 401;
887
1003
  let migrate = (preflight instanceof Response && preflight.status === 403) ||
888
1004
  permanentAuthFailure;
889
1005
  if (primaryResponseAllowsApiFallback(preflight)) {
@@ -891,8 +1007,8 @@ async function executeWithFallback(options) {
891
1007
  try {
892
1008
  quota =
893
1009
  route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID
894
- ? await quotaManager.refreshMain(mainAccountId, route.access)
895
- : await quotaManager.refreshFallback(route.id, route.access, route.account);
1010
+ ? await quotaManager.refreshMain(mainAccountId, '')
1011
+ : await quotaManager.refreshFallback(route.id, '', route.account);
896
1012
  }
897
1013
  catch {
898
1014
  // Retain affinity when the quota probe itself is unavailable.
@@ -942,6 +1058,7 @@ async function executeWithFallback(options) {
942
1058
  modelId: options.model.id,
943
1059
  candidates: routes.candidates,
944
1060
  retainAccountIds: routes.retainAccountIds,
1061
+ accountIdentities: vaultAccountIdentities(),
945
1062
  storage,
946
1063
  inputBytes: initialInputBytes,
947
1064
  excludeAccountIds: new Set([failedRouteId]),
@@ -990,6 +1107,8 @@ async function executeWithFallback(options) {
990
1107
  if (fallback)
991
1108
  return fallback;
992
1109
  }
1110
+ if (snapshot.mode === 'claustrum')
1111
+ throw new NativeVaultNoAccountError();
993
1112
  return createStickyNoRouteResponse({
994
1113
  routeQuotas: (storage?.accounts ?? [])
995
1114
  .filter(isOAuthAccount)
@@ -1001,7 +1120,7 @@ async function executeWithFallback(options) {
1001
1120
  let mainQuota = quotaManager.getMain(mainAccountId)?.quota;
1002
1121
  if (!mainQuota || quotaManager.isMainStale(options.model.id)) {
1003
1122
  try {
1004
- mainQuota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
1123
+ mainQuota = await quotaManager.refreshMain(mainAccountId, '');
1005
1124
  }
1006
1125
  catch { }
1007
1126
  }
@@ -1026,12 +1145,10 @@ async function executeWithFallback(options) {
1026
1145
  });
1027
1146
  }
1028
1147
  }
1029
- const primary = await sendAnthropicRequest({
1148
+ const { response: primary, preflight: primaryPreflight } = await firstStreamingError(await sendAnthropicRequest({
1030
1149
  ...options,
1031
- accessToken: primaryCredential,
1032
1150
  oauthAccountId: STICKY_ROUTING_MAIN_ACCOUNT_ID,
1033
- });
1034
- const primaryPreflight = await firstStreamingError(primary);
1151
+ }));
1035
1152
  if (primaryPreflight instanceof Response) {
1036
1153
  if (!shouldFallbackStatus(primaryPreflight.status, storage))
1037
1154
  return primaryPreflight;
@@ -1059,18 +1176,18 @@ export function streamCortexKitAnthropic(model, context, options, effortTransiti
1059
1176
  const output = createOutput(model);
1060
1177
  stream.push({ type: 'start', partial: output });
1061
1178
  try {
1062
- const accessToken = options?.apiKey ?? '';
1179
+ buildPiOAuthMessagesUrl(model.baseUrl);
1063
1180
  const storagePath = getPiAccountStoragePath();
1064
- if (!accessToken &&
1065
- getClaustrumMode(await loadAccounts(storagePath)) !== 'claustrum')
1066
- throw new Error('Missing Anthropic OAuth access token');
1181
+ let hostTools = [];
1067
1182
  const response = await executeWithFallback({
1068
1183
  model,
1069
1184
  context,
1070
1185
  streamOptions: options,
1071
- primaryAccessToken: accessToken,
1072
1186
  storagePath,
1073
1187
  effortTransitions,
1188
+ onResolvedTools: (tools) => {
1189
+ hostTools = tools;
1190
+ },
1074
1191
  });
1075
1192
  if (!response.ok) {
1076
1193
  throw new Error(`Anthropic request failed: HTTP ${response.status} ${await response.text()}`);
@@ -1125,7 +1242,7 @@ export function streamCortexKitAnthropic(model, context, options, effortTransiti
1125
1242
  output.content.push({
1126
1243
  type: 'toolCall',
1127
1244
  id: String(block.id),
1128
- name: fromClaudeCodeToolName(String(block.name), context.tools),
1245
+ name: fromClaudeCodeToolName(String(block.name), hostTools),
1129
1246
  arguments: {},
1130
1247
  partialJson: '',
1131
1248
  index: event.index,