@cortexkit/pi-anthropic-auth 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +18 -4
- package/dist/convert.d.ts +1 -0
- package/dist/convert.js +123 -26
- package/dist/custody.d.ts +17 -0
- package/dist/custody.js +66 -0
- package/dist/effort-history.d.ts +15 -0
- package/dist/effort-history.js +40 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +103 -8
- package/dist/paths.d.ts +11 -0
- package/dist/paths.js +42 -2
- package/dist/stream.d.ts +3 -1
- package/dist/stream.js +290 -41
- package/package.json +5 -5
package/dist/paths.d.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
export declare function getPiConfigDir(): string;
|
|
2
2
|
export declare function getPiAccountStoragePath(): string;
|
|
3
|
+
export declare function getPiClaustrumConnectionOptions(storagePath?: string): {
|
|
4
|
+
storagePath: string;
|
|
5
|
+
identity: {
|
|
6
|
+
project_root: string;
|
|
7
|
+
harness: string;
|
|
8
|
+
session: string;
|
|
9
|
+
};
|
|
10
|
+
connectionFile: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
/** Read-only metadata inspection; never create or rewrite Pi's credential store. */
|
|
13
|
+
export declare function hasPiLocalAnthropicOAuth(): Promise<boolean>;
|
package/dist/paths.js
CHANGED
|
@@ -1,10 +1,50 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
1
2
|
import { homedir } from 'node:os';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
|
-
import { ACCOUNT_FILE_NAME } from '@cortexkit/anthropic-auth-core';
|
|
4
|
+
import { ACCOUNT_FILE_NAME, parseJsonRedacted, primeStorageFingerprint, } from '@cortexkit/anthropic-auth-core';
|
|
4
5
|
export function getPiConfigDir() {
|
|
5
|
-
return process.env.
|
|
6
|
+
return (process.env.PI_CODING_AGENT_DIR?.trim() ||
|
|
7
|
+
process.env.PI_AGENT_DIR?.trim() ||
|
|
8
|
+
join(homedir(), '.pi', 'agent'));
|
|
6
9
|
}
|
|
7
10
|
export function getPiAccountStoragePath() {
|
|
8
11
|
return (process.env.PI_ANTHROPIC_AUTH_FILE?.trim() ||
|
|
9
12
|
join(getPiConfigDir(), ACCOUNT_FILE_NAME));
|
|
10
13
|
}
|
|
14
|
+
export function getPiClaustrumConnectionOptions(storagePath = getPiAccountStoragePath()) {
|
|
15
|
+
return {
|
|
16
|
+
storagePath,
|
|
17
|
+
identity: {
|
|
18
|
+
project_root: process.cwd(),
|
|
19
|
+
harness: 'pi',
|
|
20
|
+
session: `store-${primeStorageFingerprint(storagePath)}`,
|
|
21
|
+
},
|
|
22
|
+
connectionFile: process.env.PI_ANTHROPIC_AUTH_CLAUSTRUM_CONNECTION_FILE?.trim() ||
|
|
23
|
+
process.env.CLAUSTRUM_SUBC_CONNECTION?.trim() ||
|
|
24
|
+
undefined,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Read-only metadata inspection; never create or rewrite Pi's credential store. */
|
|
28
|
+
export async function hasPiLocalAnthropicOAuth() {
|
|
29
|
+
try {
|
|
30
|
+
const value = parseJsonRedacted(await readFile(join(getPiConfigDir(), 'auth.json'), 'utf8'));
|
|
31
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
32
|
+
throw new Error('Invalid authentication store');
|
|
33
|
+
const credential = value.anthropic;
|
|
34
|
+
if (credential === undefined)
|
|
35
|
+
return false;
|
|
36
|
+
if (!credential ||
|
|
37
|
+
typeof credential !== 'object' ||
|
|
38
|
+
Array.isArray(credential))
|
|
39
|
+
throw new Error('Invalid Anthropic authentication entry');
|
|
40
|
+
const type = credential.type;
|
|
41
|
+
if (type !== 'oauth' && type !== 'api_key')
|
|
42
|
+
throw new Error('Unknown Anthropic authentication entry');
|
|
43
|
+
return type === 'oauth';
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code === 'ENOENT')
|
|
47
|
+
return false;
|
|
48
|
+
throw new Error('Cannot safely inspect Pi authentication metadata');
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/stream.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type ApiKeyAccount, type MidConversationEffortTransition } from '@cortexkit/anthropic-auth-core';
|
|
1
|
+
import { type ApiKeyAccount, ClaustrumScopedRuntime, type ClaustrumScopedRuntimeOptions, type MidConversationEffortTransition } from '@cortexkit/anthropic-auth-core';
|
|
2
2
|
import { type Api, type AssistantMessageEventStream, type Context, type Model, type SimpleStreamOptions } from '@earendil-works/pi-ai';
|
|
3
|
+
export declare function getPiScopedRuntime(storagePath?: string, overrides?: Partial<Pick<ClaustrumScopedRuntimeOptions, 'connect' | 'pollIntervalMs' | 'setTimeoutImpl' | 'clearTimeoutImpl'>>): ClaustrumScopedRuntime;
|
|
4
|
+
export declare function closePiScopedRuntime(storagePath?: string): void;
|
|
3
5
|
export declare function clearPiStickyRoutingSession(storagePath: string, sessionId: string): Promise<void>;
|
|
4
6
|
export declare function getPiTrackedCacheKeepSessions(): Promise<import("@cortexkit/anthropic-auth-core").CacheKeepTrackedSession[]>;
|
|
5
7
|
type AnthropicEvent = {
|
package/dist/stream.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { applyClaudeCodeHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, createStickyNoRouteResponse, decideStickyQuotaFailure, dumpDirectRequest, FAST_MODE_BETA, FallbackAccountManager, getCache1hPersistentMode, getDefaultCacheKeepRegistryDirectory, getFallbackReauthLabels, getOrCreateMainAccountId, getRelayConfig, getRoutingMode, getStickyRoutingStatePath, getThinkingPrefixMismatchBehavior, hasThinkingBindingControls, isApiKeyAccount, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isOAuthAccount, isPermanentRefreshError, isValidApiBaseURL, killswitchPassesPolicy, loadAccounts, 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, ClaustrumScopedRuntime, connectClaustrumScopedClient, createStickyNoRouteResponse, decideStickyQuotaFailure, dumpDirectRequest, FAST_MODE_BETA, FallbackAccountManager, fetchOAuthQuotaSnapshot, getCache1hPersistentMode, getClaudeCodeIdentityForVerifiedAccount, getClaustrumMode, getDefaultCacheKeepRegistryDirectory, getFallbackReauthLabels, getHostClaustrumEnrollmentPaths, getOrCreateMainAccountId, getQuotaCheckIntervalMs, getRelayConfig, getRoutingMode, getScopedQuotaWindowForModel, getStickyRoutingStatePath, getThinkingPrefixMismatchBehavior, hasThinkingBindingControls, isApiKeyAccount, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isOAuthAccount, isPermanentRefreshError, isValidApiBaseURL, killswitchPassesPolicy, loadAccounts, logger, MID_CONVERSATION_OUTPUT_CONFIG_BETA, mergeAnthropicBetas, QuotaManager, quotaSnapshotHasStandardWindows, quotaSnapshotModelScopeIsExhausted, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, refreshBackoffActive, resolveClaudeCodeIdentity, STICKY_ROUTING_MAIN_ACCOUNT_ID, StickySessionRouter, sendViaRelay, setDumpEnabled, shouldFallbackStatus, stickyQuotaSnapshotIsFresh, stickyRetryAfterWithJitter, stickyRouteFamilyForModel, THINKING_BINDING_CONTROLS_BETA, tokenFingerprint, usesMidConversationOutputConfig, } from '@cortexkit/anthropic-auth-core';
|
|
2
2
|
import { calculateCost, createAssistantMessageEventStream, } from '@earendil-works/pi-ai';
|
|
3
3
|
import { buildAnthropicRequest, fromClaudeCodeToolName } from './convert.js';
|
|
4
|
-
import {
|
|
4
|
+
import { requirePiEnrollment } from './custody.js';
|
|
5
|
+
import { getPiAccountStoragePath, getPiClaustrumConnectionOptions, } from './paths.js';
|
|
5
6
|
function errorText(error) {
|
|
6
7
|
return error instanceof Error ? error.message : String(error);
|
|
7
8
|
}
|
|
@@ -12,6 +13,46 @@ const quotaManagers = new Map();
|
|
|
12
13
|
const fallbackManagers = new Map();
|
|
13
14
|
const mainAccountIdInitializations = new Map();
|
|
14
15
|
const PI_SERVICE_CACHE_LIMIT = 16;
|
|
16
|
+
const scopedRuntimes = new Map();
|
|
17
|
+
export function getPiScopedRuntime(storagePath = getPiAccountStoragePath(), overrides = {}) {
|
|
18
|
+
const existing = scopedRuntimes.get(storagePath);
|
|
19
|
+
if (existing)
|
|
20
|
+
return existing;
|
|
21
|
+
let warned = false;
|
|
22
|
+
const { connect, ...runtimeOverrides } = overrides;
|
|
23
|
+
const runtime = new ClaustrumScopedRuntime({
|
|
24
|
+
storagePath,
|
|
25
|
+
tokenPath: getHostClaustrumEnrollmentPaths('pi').tokenPath,
|
|
26
|
+
connect: async () => {
|
|
27
|
+
await requirePiEnrollment();
|
|
28
|
+
return connect
|
|
29
|
+
? connect()
|
|
30
|
+
: connectClaustrumScopedClient(getPiClaustrumConnectionOptions(storagePath));
|
|
31
|
+
},
|
|
32
|
+
onRoster: () => {
|
|
33
|
+
warned = false;
|
|
34
|
+
},
|
|
35
|
+
onError: () => {
|
|
36
|
+
if (!warned)
|
|
37
|
+
logger.warn('claustrum', 'Pi scoped credential service unavailable');
|
|
38
|
+
warned = true;
|
|
39
|
+
},
|
|
40
|
+
...runtimeOverrides,
|
|
41
|
+
});
|
|
42
|
+
while (scopedRuntimes.size >= PI_SERVICE_CACHE_LIMIT) {
|
|
43
|
+
const oldest = scopedRuntimes.keys().next().value;
|
|
44
|
+
if (oldest === undefined)
|
|
45
|
+
break;
|
|
46
|
+
scopedRuntimes.get(oldest)?.close();
|
|
47
|
+
scopedRuntimes.delete(oldest);
|
|
48
|
+
}
|
|
49
|
+
scopedRuntimes.set(storagePath, runtime);
|
|
50
|
+
return runtime;
|
|
51
|
+
}
|
|
52
|
+
export function closePiScopedRuntime(storagePath = getPiAccountStoragePath()) {
|
|
53
|
+
scopedRuntimes.get(storagePath)?.close();
|
|
54
|
+
scopedRuntimes.delete(storagePath);
|
|
55
|
+
}
|
|
15
56
|
function setBoundedService(map, key, value) {
|
|
16
57
|
map.delete(key);
|
|
17
58
|
map.set(key, value);
|
|
@@ -39,10 +80,38 @@ function getPiRoutingServices(storagePath, storage) {
|
|
|
39
80
|
let quotaManager = quotaManagers.get(storagePath);
|
|
40
81
|
let fallbackManager = fallbackManagers.get(storagePath);
|
|
41
82
|
if (!quotaManager || !fallbackManager) {
|
|
42
|
-
quotaManager = new QuotaManager({
|
|
83
|
+
quotaManager = new QuotaManager({
|
|
84
|
+
storage,
|
|
85
|
+
fetchQuotaSnapshot: async (request) => {
|
|
86
|
+
const current = await loadAccounts(storagePath);
|
|
87
|
+
if (getClaustrumMode(current) === 'claustrum') {
|
|
88
|
+
const id = request.kind === 'main' ? 'main' : request.accountId;
|
|
89
|
+
if (!id)
|
|
90
|
+
throw new Error('Claustrum quota request has no account identity');
|
|
91
|
+
return getPiScopedRuntime(storagePath).fetchQuota(id);
|
|
92
|
+
}
|
|
93
|
+
if (!request.accessToken)
|
|
94
|
+
throw new Error('Missing local OAuth quota credential');
|
|
95
|
+
return fetchOAuthQuotaSnapshot({ accessToken: request.accessToken });
|
|
96
|
+
},
|
|
97
|
+
});
|
|
43
98
|
fallbackManager = new FallbackAccountManager({
|
|
44
99
|
configPath: storagePath,
|
|
45
100
|
quotaManager,
|
|
101
|
+
isFallbackAccountVaultEnabled: (_id, current) => getClaustrumMode(current) === 'claustrum',
|
|
102
|
+
isFallbackAccountVaultServed: (id, current) => {
|
|
103
|
+
const account = current.accounts.find((candidate) => candidate.id === id);
|
|
104
|
+
return (getClaustrumMode(current) === 'claustrum' &&
|
|
105
|
+
Boolean(account &&
|
|
106
|
+
isOAuthAccount(account) &&
|
|
107
|
+
account.claustrumScopedCredentialId &&
|
|
108
|
+
account.claustrumScopedState === 'active'));
|
|
109
|
+
},
|
|
110
|
+
resolveFallbackAccessToken: (account, current) => getClaustrumMode(current) === 'claustrum'
|
|
111
|
+
? undefined
|
|
112
|
+
: account.access
|
|
113
|
+
? { token: account.access, source: 'sidecar' }
|
|
114
|
+
: undefined,
|
|
46
115
|
});
|
|
47
116
|
setBoundedService(quotaManagers, storagePath, quotaManager);
|
|
48
117
|
setBoundedService(fallbackManagers, storagePath, fallbackManager);
|
|
@@ -74,18 +143,50 @@ function getPiCacheKeepRegistry() {
|
|
|
74
143
|
}
|
|
75
144
|
return cacheKeepRegistry;
|
|
76
145
|
}
|
|
146
|
+
const scopedPrewarmAttempts = new Map();
|
|
77
147
|
const cacheKeepManager = new CacheKeepManager({
|
|
78
148
|
loadStorage: () => loadAccounts(getPiAccountStoragePath()),
|
|
79
149
|
onTrackedSessionsChanged: (sessions) => getPiCacheKeepRegistry().publish(sessions),
|
|
80
|
-
prepareHeaders: async (headers, target) => {
|
|
150
|
+
prepareHeaders: async (headers, target, attempt) => {
|
|
151
|
+
const storagePath = target.accountStoragePath ?? getPiAccountStoragePath();
|
|
152
|
+
const current = await loadAccounts(storagePath);
|
|
153
|
+
if (target.accountStoragePath && !current)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (getClaustrumMode(current) === 'claustrum') {
|
|
156
|
+
const runtime = getPiScopedRuntime(storagePath);
|
|
157
|
+
const id = target.oauthAccountId ?? STICKY_ROUTING_MAIN_ACCOUNT_ID;
|
|
158
|
+
const receipt = await runtime.authorize(id, attempt.signal);
|
|
159
|
+
if (!target.oauthAccountIdentity ||
|
|
160
|
+
receipt.accountId !== target.oauthAccountIdentity) {
|
|
161
|
+
throw new Error('CacheKeep account identity changed');
|
|
162
|
+
}
|
|
163
|
+
const body = JSON.parse(target.bodyText);
|
|
164
|
+
const identity = getClaudeCodeIdentityForVerifiedAccount(id === STICKY_ROUTING_MAIN_ACCOUNT_ID
|
|
165
|
+
? (current?.mainAccountId ?? id)
|
|
166
|
+
: id, receipt.accountId);
|
|
167
|
+
headers.delete('anthropic-beta');
|
|
168
|
+
applyClaudeCodeHeaders(headers, receipt.accessToken, {
|
|
169
|
+
body,
|
|
170
|
+
identity,
|
|
171
|
+
extraBetas: [
|
|
172
|
+
CACHE_KEEP_EXTENDED_TTL_BETA,
|
|
173
|
+
...(hasThinkingBindingControls(body)
|
|
174
|
+
? [THINKING_BINDING_CONTROLS_BETA]
|
|
175
|
+
: []),
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
if (body.speed === 'fast')
|
|
179
|
+
headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
|
|
180
|
+
scopedPrewarmAttempts.set(attempt.id, { runtime, receipt });
|
|
181
|
+
return headers;
|
|
182
|
+
}
|
|
81
183
|
const authorization = headers.get('authorization') ?? '';
|
|
82
184
|
const match = /^Bearer\s+(.+)$/i.exec(authorization);
|
|
83
185
|
const accessToken = match?.[1];
|
|
84
186
|
if (!accessToken)
|
|
85
|
-
return
|
|
187
|
+
return undefined;
|
|
86
188
|
try {
|
|
87
189
|
const body = JSON.parse(target.bodyText);
|
|
88
|
-
const storagePath = getPiAccountStoragePath();
|
|
89
190
|
await ensurePiMainAccountId(storagePath);
|
|
90
191
|
const storage = await loadAccounts(storagePath);
|
|
91
192
|
const identity = await resolveClaudeCodeIdentity(accessToken, typeof body.model === 'string' ? body.model : undefined, target.oauthAccountId ?? storage?.mainAccountId);
|
|
@@ -109,6 +210,14 @@ const cacheKeepManager = new CacheKeepManager({
|
|
|
109
210
|
}
|
|
110
211
|
return headers;
|
|
111
212
|
},
|
|
213
|
+
onResponse: async ({ attempt, status }) => {
|
|
214
|
+
const entry = scopedPrewarmAttempts.get(attempt.id);
|
|
215
|
+
if (entry)
|
|
216
|
+
await entry.runtime.reportFailure(entry.receipt, status, 'direct');
|
|
217
|
+
},
|
|
218
|
+
onComplete: ({ attempt }) => {
|
|
219
|
+
scopedPrewarmAttempts.delete(attempt.id);
|
|
220
|
+
},
|
|
112
221
|
});
|
|
113
222
|
export async function getPiTrackedCacheKeepSessions() {
|
|
114
223
|
return getPiCacheKeepRegistry().list(cacheKeepManager.trackedSessions());
|
|
@@ -164,7 +273,16 @@ function updateUsage(model, output, usage) {
|
|
|
164
273
|
}
|
|
165
274
|
export function buildExplicitBaseMessagesUrl(baseURL) {
|
|
166
275
|
const url = new URL(baseURL);
|
|
167
|
-
|
|
276
|
+
const basePath = url.pathname.replace(/\/$/, '');
|
|
277
|
+
if (/\/v\d[^/]*\/messages$/.test(basePath)) {
|
|
278
|
+
url.pathname = basePath;
|
|
279
|
+
}
|
|
280
|
+
else if (/\/v\d[^/]*$/.test(basePath)) {
|
|
281
|
+
url.pathname = `${basePath}/messages`;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
url.pathname = `${basePath}/v1/messages`;
|
|
285
|
+
}
|
|
168
286
|
url.searchParams.set('beta', 'true');
|
|
169
287
|
return url;
|
|
170
288
|
}
|
|
@@ -183,6 +301,7 @@ export function configureApiRouteHeaders(account, fastMode) {
|
|
|
183
301
|
if (fastMode) {
|
|
184
302
|
headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
|
|
185
303
|
}
|
|
304
|
+
applyCustomHeaders(headers);
|
|
186
305
|
return headers;
|
|
187
306
|
}
|
|
188
307
|
export async function* parseSse(response) {
|
|
@@ -230,9 +349,24 @@ async function sendAnthropicRequest(options) {
|
|
|
230
349
|
const accountIdentity = options.oauthAccountId === STICKY_ROUTING_MAIN_ACCOUNT_ID
|
|
231
350
|
? storage?.mainAccountId
|
|
232
351
|
: options.oauthAccountId;
|
|
233
|
-
const
|
|
234
|
-
?
|
|
352
|
+
const scoped = !options.apiAccount && getClaustrumMode(storage) === 'claustrum'
|
|
353
|
+
? getPiScopedRuntime(options.storagePath)
|
|
235
354
|
: undefined;
|
|
355
|
+
const routeId = options.oauthAccountId ?? STICKY_ROUTING_MAIN_ACCOUNT_ID;
|
|
356
|
+
const roster = scoped
|
|
357
|
+
? (scoped.snapshot() ?? (await scoped.refresh()))
|
|
358
|
+
: undefined;
|
|
359
|
+
const verifiedUuid = routeId === STICKY_ROUTING_MAIN_ACCOUNT_ID
|
|
360
|
+
? roster?.primary?.accountId
|
|
361
|
+
: roster?.accounts.find((account) => account.id === routeId)
|
|
362
|
+
?.anthropicAccountUuid;
|
|
363
|
+
if (scoped && !verifiedUuid)
|
|
364
|
+
throw new Error('Claustrum route has no verified provider identity');
|
|
365
|
+
const identity = scoped && verifiedUuid
|
|
366
|
+
? getClaudeCodeIdentityForVerifiedAccount(accountIdentity ?? routeId, verifiedUuid)
|
|
367
|
+
: options.accessToken
|
|
368
|
+
? await resolveClaudeCodeIdentity(options.accessToken, options.model.id, accountIdentity)
|
|
369
|
+
: undefined;
|
|
236
370
|
const { body, bodyText } = await buildAnthropicRequest(options.model.id, options.context, options.streamOptions, {
|
|
237
371
|
enabled: isCache1hPersistentlyEnabled(storage),
|
|
238
372
|
mode: getCache1hPersistentMode(storage),
|
|
@@ -245,7 +379,7 @@ async function sendAnthropicRequest(options) {
|
|
|
245
379
|
const fastMode = body.speed === 'fast';
|
|
246
380
|
const headers = options.apiAccount
|
|
247
381
|
? configureApiRouteHeaders(options.apiAccount, fastMode)
|
|
248
|
-
: applyClaudeCodeHeaders(new Headers(), options.accessToken ?? '', {
|
|
382
|
+
: applyClaudeCodeHeaders(new Headers(), scoped ? '' : (options.accessToken ?? ''), {
|
|
249
383
|
body,
|
|
250
384
|
identity,
|
|
251
385
|
extraBetas: [
|
|
@@ -276,12 +410,31 @@ async function sendAnthropicRequest(options) {
|
|
|
276
410
|
headers,
|
|
277
411
|
bodyText,
|
|
278
412
|
storage,
|
|
279
|
-
cacheMode: isCacheKeepHybridActive(storage)
|
|
413
|
+
cacheMode: !options.apiAccount && isCacheKeepHybridActive(storage)
|
|
414
|
+
? 'hybrid'
|
|
415
|
+
: 'disabled',
|
|
280
416
|
oauthAccountId: options.oauthAccountId,
|
|
417
|
+
oauthAccountIdentity: verifiedUuid ?? identity?.accountUuid,
|
|
418
|
+
accountStoragePath: options.storagePath,
|
|
281
419
|
});
|
|
420
|
+
const report = async (attempt, status, source) => {
|
|
421
|
+
if (!scoped || status !== 401)
|
|
422
|
+
return;
|
|
423
|
+
await scoped.reportFailure(attempt, status, source).catch(() => {
|
|
424
|
+
logger.warn('claustrum', 'Pi scoped auth-failure report unavailable');
|
|
425
|
+
});
|
|
426
|
+
};
|
|
282
427
|
const directFetch = async () => {
|
|
428
|
+
// Every physical attempt, including relay-to-direct fallback, is authorized.
|
|
429
|
+
const attempt = scoped
|
|
430
|
+
? await scoped.authorize(routeId, options.streamOptions?.signal)
|
|
431
|
+
: undefined;
|
|
432
|
+
if (attempt)
|
|
433
|
+
headers.set('authorization', `Bearer ${attempt.accessToken}`);
|
|
283
434
|
try {
|
|
284
435
|
const response = await fetch(input, init);
|
|
436
|
+
if (attempt)
|
|
437
|
+
await report(attempt, response.status, 'direct');
|
|
285
438
|
await dumpDirectRequest({
|
|
286
439
|
affinity: relayAffinity,
|
|
287
440
|
route: options.route ??
|
|
@@ -318,6 +471,19 @@ async function sendAnthropicRequest(options) {
|
|
|
318
471
|
body: bodyText,
|
|
319
472
|
fallback: directFetch,
|
|
320
473
|
affinity: relayAffinity,
|
|
474
|
+
authorizeAttempt: scoped
|
|
475
|
+
? async () => {
|
|
476
|
+
const attempt = await scoped.authorize(routeId, options.streamOptions?.signal);
|
|
477
|
+
const authorizedHeaders = new Headers(headers);
|
|
478
|
+
authorizedHeaders.set('authorization', `Bearer ${attempt.accessToken}`);
|
|
479
|
+
return {
|
|
480
|
+
headers: authorizedHeaders,
|
|
481
|
+
onUpstreamStatus: (status) => {
|
|
482
|
+
void report(attempt, status, 'relay_status_field');
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
: undefined,
|
|
321
487
|
});
|
|
322
488
|
}
|
|
323
489
|
function quotaSnapshotIsExhausted(quota) {
|
|
@@ -348,17 +514,62 @@ async function firstStreamingError(response) {
|
|
|
348
514
|
}
|
|
349
515
|
async function executeWithFallback(options) {
|
|
350
516
|
await ensurePiMainAccountId(options.storagePath);
|
|
351
|
-
|
|
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));
|
|
352
534
|
const { quotaManager, fallbackManager: manager } = getPiRoutingServices(options.storagePath, storage);
|
|
353
|
-
|
|
535
|
+
let mainAccountId = scoped
|
|
536
|
+
? roster?.primary?.accountId
|
|
537
|
+
: storage?.mainAccountId;
|
|
354
538
|
quotaManager.seedMainFromStorage(storage, mainAccountId);
|
|
355
539
|
quotaManager.seedFallbacksFromAccounts((storage?.accounts ?? []).filter(isOAuthAccount));
|
|
540
|
+
function quotaObservationIsFresh(quota, modelId) {
|
|
541
|
+
if (stickyQuotaSnapshotIsFresh(quota, storage, Date.now(), modelId))
|
|
542
|
+
return true;
|
|
543
|
+
// A successful windowless usage response is not the same as a failed
|
|
544
|
+
// probe. Pi intentionally admits the former as unknown-capacity OAuth.
|
|
545
|
+
if (!quota ||
|
|
546
|
+
quota.five_hour ||
|
|
547
|
+
quota.seven_day ||
|
|
548
|
+
quota.source !== 'poll' ||
|
|
549
|
+
quota.checkedAt === undefined)
|
|
550
|
+
return false;
|
|
551
|
+
const age = getQuotaCheckIntervalMs(storage);
|
|
552
|
+
const scopedWindow = getScopedQuotaWindowForModel(quota, modelId);
|
|
553
|
+
return (Date.now() - quota.checkedAt < age &&
|
|
554
|
+
(!scopedWindow || Date.now() - scopedWindow.checkedAt < age));
|
|
555
|
+
}
|
|
356
556
|
async function buildStickyRoutes(modelId) {
|
|
557
|
+
if (scoped) {
|
|
558
|
+
roster = await scoped.refresh();
|
|
559
|
+
if (!roster)
|
|
560
|
+
throw new Error('Claustrum custody changed during routing');
|
|
561
|
+
storage = roster.storage;
|
|
562
|
+
mainAccountId = roster.primary?.accountId;
|
|
563
|
+
quotaManager.updateStorage(storage);
|
|
564
|
+
quotaManager.seedMainFromStorage(storage, mainAccountId);
|
|
565
|
+
quotaManager.seedFallbacksFromAccounts(storage.accounts.filter(isOAuthAccount));
|
|
566
|
+
}
|
|
357
567
|
const mainEntry = quotaManager.getMain(mainAccountId);
|
|
358
568
|
let mainQuota = mainEntry?.quota;
|
|
359
|
-
if (
|
|
569
|
+
if (primaryAvailable() &&
|
|
570
|
+
!stickyQuotaSnapshotIsFresh(mainEntry?.quota, storage, Date.now(), modelId)) {
|
|
360
571
|
try {
|
|
361
|
-
mainQuota = await quotaManager.refreshMain(mainAccountId,
|
|
572
|
+
mainQuota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
|
|
362
573
|
}
|
|
363
574
|
catch { }
|
|
364
575
|
}
|
|
@@ -367,10 +578,12 @@ async function executeWithFallback(options) {
|
|
|
367
578
|
});
|
|
368
579
|
const usableById = new Map(usableFallbacks.map((account) => [account.id, account]));
|
|
369
580
|
const allRoutes = [];
|
|
370
|
-
if (
|
|
581
|
+
if (primaryAvailable() &&
|
|
582
|
+
(scoped ||
|
|
583
|
+
!isPermanentRefreshError(storage?.refresh?.mainLastRefreshError))) {
|
|
371
584
|
allRoutes.push({
|
|
372
585
|
id: STICKY_ROUTING_MAIN_ACCOUNT_ID,
|
|
373
|
-
access:
|
|
586
|
+
access: primaryCredential,
|
|
374
587
|
quota: mainQuota,
|
|
375
588
|
order: 0,
|
|
376
589
|
});
|
|
@@ -379,18 +592,20 @@ async function executeWithFallback(options) {
|
|
|
379
592
|
if (configured.enabled === false || !isOAuthAccount(configured))
|
|
380
593
|
continue;
|
|
381
594
|
const account = usableById.get(configured.id) ?? configured;
|
|
382
|
-
if (
|
|
595
|
+
if (scoped
|
|
596
|
+
? !scopedReady(account)
|
|
597
|
+
: !account.access || isPermanentRefreshError(account.lastRefreshError))
|
|
383
598
|
continue;
|
|
384
599
|
let accountQuota = quotaManager.getFallback(account.id, account)?.quota ?? account.quota;
|
|
385
600
|
if (!stickyQuotaSnapshotIsFresh(accountQuota, storage, Date.now(), modelId)) {
|
|
386
601
|
try {
|
|
387
|
-
accountQuota = await quotaManager.refreshFallback(account.id, account.access, account);
|
|
602
|
+
accountQuota = await quotaManager.refreshFallback(account.id, scoped ? '' : (account.access ?? ''), account);
|
|
388
603
|
}
|
|
389
604
|
catch { }
|
|
390
605
|
}
|
|
391
606
|
allRoutes.push({
|
|
392
607
|
id: account.id,
|
|
393
|
-
access: account.access,
|
|
608
|
+
access: scoped ? '' : (account.access ?? ''),
|
|
394
609
|
quota: accountQuota,
|
|
395
610
|
order: index + 1,
|
|
396
611
|
account,
|
|
@@ -400,7 +615,7 @@ async function executeWithFallback(options) {
|
|
|
400
615
|
const refreshError = route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID
|
|
401
616
|
? storage?.refresh?.mainLastRefreshError
|
|
402
617
|
: route.account?.lastRefreshError;
|
|
403
|
-
if (isPermanentRefreshError(refreshError))
|
|
618
|
+
if (!scoped && isPermanentRefreshError(refreshError))
|
|
404
619
|
return [];
|
|
405
620
|
if (stickyQuotaSnapshotIsFresh(route.quota, storage, Date.now(), modelId) &&
|
|
406
621
|
decideStickyQuotaFailure({ quota: route.quota, modelId }).action ===
|
|
@@ -443,8 +658,10 @@ async function executeWithFallback(options) {
|
|
|
443
658
|
return { allRoutes, candidates, retainAccountIds };
|
|
444
659
|
}
|
|
445
660
|
async function primaryQuotaRefreshConfirmsExhausted() {
|
|
661
|
+
if (!primaryAvailable())
|
|
662
|
+
return false;
|
|
446
663
|
try {
|
|
447
|
-
const quota = await quotaManager.refreshMain(mainAccountId,
|
|
664
|
+
const quota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
|
|
448
665
|
const entry = quotaManager.getMain(mainAccountId);
|
|
449
666
|
return Boolean(entry &&
|
|
450
667
|
entry.refreshAfter > Date.now() &&
|
|
@@ -455,8 +672,10 @@ async function executeWithFallback(options) {
|
|
|
455
672
|
}
|
|
456
673
|
}
|
|
457
674
|
async function primaryQuotaRefreshConfirmsModelScopeExhausted() {
|
|
675
|
+
if (!primaryAvailable())
|
|
676
|
+
return false;
|
|
458
677
|
try {
|
|
459
|
-
const quota = await quotaManager.refreshMain(mainAccountId,
|
|
678
|
+
const quota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
|
|
460
679
|
const entry = quotaManager.getMain(mainAccountId);
|
|
461
680
|
return Boolean(entry &&
|
|
462
681
|
entry.refreshAfter > Date.now() &&
|
|
@@ -467,11 +686,15 @@ async function executeWithFallback(options) {
|
|
|
467
686
|
}
|
|
468
687
|
}
|
|
469
688
|
function primaryCachedModelScopeExhausted() {
|
|
689
|
+
if (!primaryAvailable())
|
|
690
|
+
return false;
|
|
470
691
|
const entry = quotaManager.getMain(mainAccountId);
|
|
471
692
|
return Boolean(entry &&
|
|
472
693
|
quotaSnapshotModelScopeIsExhausted(entry.quota, options.model.id));
|
|
473
694
|
}
|
|
474
695
|
function primaryFreshModelScopeExhausted() {
|
|
696
|
+
if (!primaryAvailable())
|
|
697
|
+
return false;
|
|
475
698
|
const entry = quotaManager.getMain(mainAccountId);
|
|
476
699
|
return Boolean(entry &&
|
|
477
700
|
!quotaManager.isMainStale(options.model.id) &&
|
|
@@ -483,17 +706,21 @@ async function executeWithFallback(options) {
|
|
|
483
706
|
});
|
|
484
707
|
const usableOAuthById = new Map(usableOAuth.map((account) => [account.id, account]));
|
|
485
708
|
for (const configured of storage?.accounts ?? []) {
|
|
709
|
+
if (configured.enabled === false)
|
|
710
|
+
continue;
|
|
486
711
|
let response = null;
|
|
487
712
|
let account = isOAuthAccount(configured)
|
|
488
713
|
? usableOAuthById.get(configured.id)
|
|
489
714
|
: configured;
|
|
490
715
|
if (!account) {
|
|
491
716
|
if (!isOAuthAccount(configured) ||
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
configured.
|
|
495
|
-
|
|
496
|
-
|
|
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))))
|
|
497
724
|
continue;
|
|
498
725
|
account = configured;
|
|
499
726
|
const quota = quotaManager.getFallback(configured.id, configured)?.quota ??
|
|
@@ -510,11 +737,12 @@ async function executeWithFallback(options) {
|
|
|
510
737
|
continue;
|
|
511
738
|
}
|
|
512
739
|
if (isOAuthAccount(account)) {
|
|
513
|
-
if (routeOptions.apiOnly === true ||
|
|
740
|
+
if (routeOptions.apiOnly === true ||
|
|
741
|
+
(scoped ? !scopedReady(account) : !account.access))
|
|
514
742
|
continue;
|
|
515
743
|
response = await sendAnthropicRequest({
|
|
516
744
|
...options,
|
|
517
|
-
accessToken: account.access,
|
|
745
|
+
accessToken: scoped ? undefined : account.access,
|
|
518
746
|
oauthAccountId: account.id,
|
|
519
747
|
});
|
|
520
748
|
}
|
|
@@ -549,15 +777,19 @@ async function executeWithFallback(options) {
|
|
|
549
777
|
const router = getPiStickyRouter(options.storagePath);
|
|
550
778
|
const initialInputBytes = Math.max(1, Buffer.byteLength(JSON.stringify(options.context)));
|
|
551
779
|
let routes = await buildStickyRoutes(options.model.id);
|
|
552
|
-
const mainPermanentlyUnavailable =
|
|
780
|
+
const mainPermanentlyUnavailable = scoped
|
|
781
|
+
? !primaryAvailable()
|
|
782
|
+
: isPermanentRefreshError(storage?.refresh?.mainLastRefreshError);
|
|
553
783
|
const incompleteQuotaPool = (routes.allRoutes.length === 0 && !mainPermanentlyUnavailable) ||
|
|
554
|
-
routes.allRoutes.some((candidate) => !candidate.quota
|
|
555
|
-
!stickyQuotaSnapshotIsFresh(candidate.quota, storage, Date.now(), options.model.id));
|
|
784
|
+
routes.allRoutes.some((candidate) => !quotaObservationIsFresh(candidate.quota, options.model.id));
|
|
556
785
|
let resolution = await router.resolve({
|
|
557
786
|
sessionId,
|
|
558
787
|
family: stickyRouteFamilyForModel(options.model.id),
|
|
559
788
|
modelId: options.model.id,
|
|
560
|
-
|
|
789
|
+
affinityModelId: options.model.id,
|
|
790
|
+
// Existing affinity can survive a transient probe failure, but an
|
|
791
|
+
// incomplete pool cannot create a new balanced assignment.
|
|
792
|
+
candidates: incompleteQuotaPool ? [] : routes.candidates,
|
|
561
793
|
retainAccountIds: routes.retainAccountIds,
|
|
562
794
|
storage,
|
|
563
795
|
inputBytes: initialInputBytes,
|
|
@@ -572,7 +804,9 @@ async function executeWithFallback(options) {
|
|
|
572
804
|
}
|
|
573
805
|
if (!resolution) {
|
|
574
806
|
return createStickyNoRouteResponse({
|
|
575
|
-
mainRefreshError:
|
|
807
|
+
mainRefreshError: scoped
|
|
808
|
+
? undefined
|
|
809
|
+
: storage?.refresh?.mainLastRefreshError,
|
|
576
810
|
fallbackReauthLabels: getFallbackReauthLabels(storage),
|
|
577
811
|
routeQuotas: routes.allRoutes.flatMap((route) => route.quota ? [route.quota] : []),
|
|
578
812
|
modelId: options.model.id,
|
|
@@ -619,11 +853,12 @@ async function executeWithFallback(options) {
|
|
|
619
853
|
}
|
|
620
854
|
let permanentAuthFailure = preflight instanceof Response &&
|
|
621
855
|
preflight.status === 401 &&
|
|
622
|
-
route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID;
|
|
856
|
+
(Boolean(scoped) || route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID);
|
|
623
857
|
if (preflight instanceof Response &&
|
|
624
858
|
preflight.status === 401 &&
|
|
625
859
|
route.account &&
|
|
626
|
-
storage
|
|
860
|
+
storage &&
|
|
861
|
+
!scoped) {
|
|
627
862
|
const authRouteId = route.id;
|
|
628
863
|
try {
|
|
629
864
|
const refreshed = await manager.refreshAccount(route.account, storage, {
|
|
@@ -749,11 +984,24 @@ async function executeWithFallback(options) {
|
|
|
749
984
|
if (fallback)
|
|
750
985
|
return fallback;
|
|
751
986
|
}
|
|
987
|
+
if (!primaryAvailable()) {
|
|
988
|
+
if (!fallbackFirst) {
|
|
989
|
+
const fallback = await tryFallbackAccounts();
|
|
990
|
+
if (fallback)
|
|
991
|
+
return fallback;
|
|
992
|
+
}
|
|
993
|
+
return createStickyNoRouteResponse({
|
|
994
|
+
routeQuotas: (storage?.accounts ?? [])
|
|
995
|
+
.filter(isOAuthAccount)
|
|
996
|
+
.flatMap((account) => (account.quota ? [account.quota] : [])),
|
|
997
|
+
modelId: options.model.id,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
752
1000
|
if (isKillswitchEnabled(storage)) {
|
|
753
1001
|
let mainQuota = quotaManager.getMain(mainAccountId)?.quota;
|
|
754
1002
|
if (!mainQuota || quotaManager.isMainStale(options.model.id)) {
|
|
755
1003
|
try {
|
|
756
|
-
mainQuota = await quotaManager.refreshMain(mainAccountId,
|
|
1004
|
+
mainQuota = await quotaManager.refreshMain(mainAccountId, primaryCredential);
|
|
757
1005
|
}
|
|
758
1006
|
catch { }
|
|
759
1007
|
}
|
|
@@ -780,7 +1028,7 @@ async function executeWithFallback(options) {
|
|
|
780
1028
|
}
|
|
781
1029
|
const primary = await sendAnthropicRequest({
|
|
782
1030
|
...options,
|
|
783
|
-
accessToken:
|
|
1031
|
+
accessToken: primaryCredential,
|
|
784
1032
|
oauthAccountId: STICKY_ROUTING_MAIN_ACCOUNT_ID,
|
|
785
1033
|
});
|
|
786
1034
|
const primaryPreflight = await firstStreamingError(primary);
|
|
@@ -812,9 +1060,10 @@ export function streamCortexKitAnthropic(model, context, options, effortTransiti
|
|
|
812
1060
|
stream.push({ type: 'start', partial: output });
|
|
813
1061
|
try {
|
|
814
1062
|
const accessToken = options?.apiKey ?? '';
|
|
815
|
-
if (!accessToken)
|
|
816
|
-
throw new Error('Missing Anthropic OAuth access token');
|
|
817
1063
|
const storagePath = getPiAccountStoragePath();
|
|
1064
|
+
if (!accessToken &&
|
|
1065
|
+
getClaustrumMode(await loadAccounts(storagePath)) !== 'claustrum')
|
|
1066
|
+
throw new Error('Missing Anthropic OAuth access token');
|
|
818
1067
|
const response = await executeWithFallback({
|
|
819
1068
|
model,
|
|
820
1069
|
context,
|