@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/README.md +50 -58
- package/dist/commands.d.ts +23 -3
- package/dist/commands.js +58 -252
- package/dist/convert.d.ts +4 -0
- package/dist/convert.js +36 -19
- package/dist/custody.d.ts +2 -6
- package/dist/custody.js +5 -38
- package/dist/index.d.ts +2 -3
- package/dist/index.js +124 -68
- package/dist/native-commands.d.ts +2 -0
- package/dist/native-commands.js +347 -0
- package/dist/native.d.ts +71 -0
- package/dist/native.js +308 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +6 -0
- package/dist/stream.d.ts +17 -3
- package/dist/stream.js +544 -427
- package/dist/transcript.d.ts +19 -0
- package/dist/transcript.js +122 -0
- package/dist/transport.d.ts +5 -0
- package/dist/transport.js +30 -0
- package/package.json +2 -2
package/dist/stream.js
CHANGED
|
@@ -1,58 +1,16 @@
|
|
|
1
|
-
import { applyClaudeCodeHeaders, applyCustomHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry,
|
|
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 {
|
|
5
|
-
import { getPiAccountStoragePath
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
120
|
+
const nativePrewarmAttempts = new Map();
|
|
147
121
|
const cacheKeepManager = new CacheKeepManager({
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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 =
|
|
215
|
-
if (entry)
|
|
216
|
-
await entry.runtime.
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
//
|
|
337
|
-
//
|
|
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
|
-
|
|
347
|
-
|
|
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
|
|
357
|
-
|
|
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
|
|
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
|
-
?
|
|
382
|
-
: applyClaudeCodeHeaders(new Headers(),
|
|
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
|
-
|
|
399
|
-
?
|
|
400
|
-
:
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
? 'hybrid'
|
|
415
|
-
:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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 (
|
|
388
|
+
if (status !== 401 || reported.has(attempt))
|
|
422
389
|
return;
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
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:
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
517
|
-
let
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
|
544
|
-
//
|
|
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
|
-
|
|
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,
|
|
753
|
+
mainQuota = await quotaManager.refreshMain(mainAccountId, '');
|
|
573
754
|
}
|
|
574
755
|
catch { }
|
|
575
756
|
}
|
|
576
|
-
const usableFallbacks = await
|
|
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 (
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
836
|
+
await refreshRoutingSnapshot();
|
|
837
|
+
if (!primaryAvailable() || !mainAccountId)
|
|
662
838
|
return false;
|
|
663
839
|
try {
|
|
664
|
-
const quota = await quotaManager.refreshMain(mainAccountId,
|
|
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,
|
|
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
|
|
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
|
|
885
|
+
for (const configured of storage.accounts) {
|
|
709
886
|
if (configured.enabled === false)
|
|
710
887
|
continue;
|
|
711
|
-
let response
|
|
712
|
-
|
|
713
|
-
|
|
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(
|
|
727
|
-
|
|
728
|
-
|
|
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,
|
|
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
|
|
750
|
-
|
|
751
|
-
account
|
|
752
|
-
|
|
753
|
-
|
|
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
|
|
762
|
-
|
|
763
|
-
|
|
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 =
|
|
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
|
-
//
|
|
791
|
-
//
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
895
|
-
: await quotaManager.refreshFallback(route.id,
|
|
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,
|
|
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
|
-
|
|
1179
|
+
buildPiOAuthMessagesUrl(model.baseUrl);
|
|
1063
1180
|
const storagePath = getPiAccountStoragePath();
|
|
1064
|
-
|
|
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),
|
|
1245
|
+
name: fromClaudeCodeToolName(String(block.name), hostTools),
|
|
1129
1246
|
arguments: {},
|
|
1130
1247
|
partialJson: '',
|
|
1131
1248
|
index: event.index,
|