@cortexkit/pi-anthropic-auth 1.15.1 → 1.16.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 +5 -2
- package/dist/commands.js +17 -2
- package/dist/stream.d.ts +2 -0
- package/dist/stream.js +338 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ Pi state is stored separately from OpenCode at:
|
|
|
42
42
|
|
|
43
43
|
Override the path with `PI_ANTHROPIC_AUTH_FILE`. The package also respects `PI_AGENT_DIR` when deriving the default sidecar path.
|
|
44
44
|
|
|
45
|
-
The sidecar uses the same JSON shape as the OpenCode package, including `routing`, `claudeCache`, `cacheKeep`, `claudeFast`, `dump`, `relay`, and fallback `accounts` blocks.
|
|
45
|
+
The sidecar uses the same JSON shape as the OpenCode package, including `routing`, `claudeCache`, `cacheKeep`, `claudeFast`, `dump`, `relay`, and fallback `accounts` blocks. Runtime OAuth/quota state is stored in `anthropic-auth-state.json`; sticky session assignments are stored separately in `anthropic-auth-routing-state.json` with SHA-256-hashed session IDs.
|
|
46
46
|
|
|
47
47
|
## Commands
|
|
48
48
|
|
|
@@ -55,6 +55,7 @@ The sidecar uses the same JSON shape as the OpenCode package, including `routing
|
|
|
55
55
|
/claude-cache mode hybrid
|
|
56
56
|
|
|
57
57
|
/claude-cachekeep
|
|
58
|
+
/claude-cachekeep always
|
|
58
59
|
/claude-cachekeep 09-23
|
|
59
60
|
/claude-cachekeep off
|
|
60
61
|
|
|
@@ -69,11 +70,13 @@ The sidecar uses the same JSON shape as the OpenCode package, including `routing
|
|
|
69
70
|
/claude-routing
|
|
70
71
|
/claude-routing main-first
|
|
71
72
|
/claude-routing fallback-first
|
|
73
|
+
/claude-routing sticky-balanced
|
|
74
|
+
/claude-routing reset
|
|
72
75
|
|
|
73
76
|
/claude-quota
|
|
74
77
|
```
|
|
75
78
|
|
|
76
|
-
`/claude-quota` reports sidecar OAuth fallback quota state from `~/.pi/agent/anthropic-auth.json`. `/claude-routing fallback-first` prefers usable OAuth fallback accounts before the main account; `/claude-routing main-first` restores the default. API-key routes use the same sidecar shape as OpenCode and are sent directly to their configured Anthropic-compatible base URL, such as Kie's `https://api.kie.ai/claude`, but Pi only uses them after the main OAuth model response reports HTTP 429 or a streaming rate-limit error and a live quota check confirms 0% remaining. `/claude-cachekeep
|
|
79
|
+
`/claude-quota` reports sidecar OAuth fallback quota state from `~/.pi/agent/anthropic-auth.json`. `/claude-routing fallback-first` prefers usable OAuth fallback accounts before the main account; `/claude-routing main-first` restores the default. `/claude-routing sticky-balanced` assigns each Pi session to an OAuth account according to current 5-hour, 7-day, and matching model-scoped quota headroom, then persists that assignment across transient failures and process restarts. `/claude-routing reset` clears the current Pi session's assignment. Direct Opus sessions prefer usable accounts whose Fable quota is exhausted. API-key routes use the same sidecar shape as OpenCode and are sent directly to their configured Anthropic-compatible base URL, such as Kie's `https://api.kie.ai/claude`, but Pi only uses them after the main OAuth model response reports HTTP 429 or a streaming rate-limit error and a live quota check confirms 0% remaining. `/claude-cachekeep always` keeps active hybrid caches warm while Pi remains open; `/claude-cachekeep HH-HH` limits prewarms to a local time window. Both send `max_tokens: 0` pre-warm requests about five minutes before the 1-hour TTL expires. Running `/claude-cachekeep` without arguments lists live tracked sessions across Pi processes through a temporary lease registry that stores only session IDs and cache timing. `/claude-fast on` adds Anthropic `speed: "fast"` plus the `fast-mode-2026-02-01` beta header for supported Opus models (`claude-opus-4-6`, `claude-opus-4-7`, and `claude-opus-4-8`).
|
|
77
80
|
|
|
78
81
|
## Relay
|
|
79
82
|
|
package/dist/commands.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { buildClaudeQuotaSummary, buildFallbackQuotaSummaries, CLAUDE_ACCOUNT_COMMAND_NAME, CLAUDE_CACHE_KEEP_COMMAND_NAME, CLAUDE_ROUTING_COMMAND_NAME, createEmptyStorage, executeAccountCommand, executeCache1hCommand, executeCacheKeepCommand, executeDumpCommand, executeFastModeCommand, executeLoggingCommand, executeRoutingCommand, getCache1hPersistentMode, getCacheKeepWindow, getPersistedLogLevel, getRoutingMode, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isCacheKeepPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, loadAccounts, parseCache1hCommandAction, parseCacheKeepCommandAction, parseDumpCommandAction, parseFastModeCommandAction, parseLoggingCommandAction, parseRoutingCommandAction, removeAccountPersistent, reorderAccountsPersistent, setAccountEnabledPersistent, setCache1hPersistentEnabled, setCache1hPersistentMode, setCacheKeepPersistentEnabled, setCacheKeepPersistentWindow, setCacheKeepSubagentsEnabled, setDumpEnabled, setDumpPersistentEnabled, setFastModePersistentEnabled, setLogLevelPersistent, setRoutingMode, } from '@cortexkit/anthropic-auth-core';
|
|
1
|
+
import { buildClaudeQuotaSummary, buildFallbackQuotaSummaries, CLAUDE_ACCOUNT_COMMAND_NAME, CLAUDE_CACHE_KEEP_COMMAND_NAME, CLAUDE_ROUTING_COMMAND_NAME, createEmptyStorage, executeAccountCommand, executeCache1hCommand, executeCacheKeepCommand, executeDumpCommand, executeFastModeCommand, executeLoggingCommand, executeRoutingCommand, getCache1hPersistentMode, getCacheKeepWindow, getPersistedLogLevel, getRoutingMode, isCache1hPersistentlyEnabled, isCacheKeepAlways, isCacheKeepHybridActive, isCacheKeepPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, loadAccounts, parseCache1hCommandAction, parseCacheKeepCommandAction, parseDumpCommandAction, parseFastModeCommandAction, parseLoggingCommandAction, parseRoutingCommandAction, removeAccountPersistent, reorderAccountsPersistent, setAccountEnabledPersistent, setCache1hPersistentEnabled, setCache1hPersistentMode, setCacheKeepPersistentAlways, setCacheKeepPersistentEnabled, setCacheKeepPersistentWindow, setCacheKeepSubagentsEnabled, setDumpEnabled, setDumpPersistentEnabled, setFastModePersistentEnabled, setLogLevelPersistent, setRoutingMode, } from '@cortexkit/anthropic-auth-core';
|
|
2
2
|
import { getPiAccountStoragePath } from "./paths.js";
|
|
3
|
+
import { clearPiStickyRoutingSession, getPiTrackedCacheKeepSessions, } from "./stream.js";
|
|
3
4
|
function notify(ctx, message, kind = 'info') {
|
|
4
5
|
ctx.ui.notify(message, kind);
|
|
5
6
|
}
|
|
@@ -35,7 +36,7 @@ export function registerCommands(pi) {
|
|
|
35
36
|
},
|
|
36
37
|
});
|
|
37
38
|
pi.registerCommand(CLAUDE_CACHE_KEEP_COMMAND_NAME, {
|
|
38
|
-
description: 'Keep hybrid Claude cache warm during a local time window',
|
|
39
|
+
description: 'Keep hybrid Claude cache warm always or during a local time window',
|
|
39
40
|
handler: async (args, ctx) => {
|
|
40
41
|
const path = getPiAccountStoragePath();
|
|
41
42
|
let storage = await loadAccounts(path);
|
|
@@ -43,17 +44,28 @@ export function registerCommands(pi) {
|
|
|
43
44
|
if (action.type === 'window') {
|
|
44
45
|
storage = await setCacheKeepPersistentWindow(action.startHour, action.endHour, path);
|
|
45
46
|
}
|
|
47
|
+
else if (action.type === 'always') {
|
|
48
|
+
storage = await setCacheKeepPersistentAlways(path);
|
|
49
|
+
}
|
|
46
50
|
else if (action.type === 'disable') {
|
|
47
51
|
storage = await setCacheKeepPersistentEnabled(false, path);
|
|
48
52
|
}
|
|
49
53
|
else if (action.type === 'subagents') {
|
|
50
54
|
storage = await setCacheKeepSubagentsEnabled(action.enabled, path);
|
|
51
55
|
}
|
|
56
|
+
const trackedSessionDetails = await getPiTrackedCacheKeepSessions();
|
|
57
|
+
const nextPrewarmAt = trackedSessionDetails.length
|
|
58
|
+
? Math.min(...trackedSessionDetails.map((session) => session.nextPrewarmAt))
|
|
59
|
+
: undefined;
|
|
52
60
|
notify(ctx, executeCacheKeepCommand({
|
|
53
61
|
argumentsText: args ?? '',
|
|
54
62
|
enabled: isCacheKeepPersistentlyEnabled(storage),
|
|
63
|
+
always: isCacheKeepAlways(storage),
|
|
55
64
|
window: getCacheKeepWindow(storage),
|
|
56
65
|
hybridActive: isCacheKeepHybridActive(storage),
|
|
66
|
+
trackedSessions: trackedSessionDetails.length,
|
|
67
|
+
trackedSessionDetails,
|
|
68
|
+
nextPrewarmAt,
|
|
57
69
|
}), action.type === 'usage' ? 'warning' : 'info');
|
|
58
70
|
},
|
|
59
71
|
});
|
|
@@ -116,6 +128,9 @@ export function registerCommands(pi) {
|
|
|
116
128
|
if (action.type === 'mode') {
|
|
117
129
|
storage = await setRoutingMode(action.mode, path);
|
|
118
130
|
}
|
|
131
|
+
else if (action.type === 'reset') {
|
|
132
|
+
await clearPiStickyRoutingSession(path, ctx.sessionManager.getSessionId());
|
|
133
|
+
}
|
|
119
134
|
notify(ctx, executeRoutingCommand({
|
|
120
135
|
argumentsText: args ?? '',
|
|
121
136
|
mode: getRoutingMode(storage),
|
package/dist/stream.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ApiKeyAccount } 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 clearPiStickyRoutingSession(storagePath: string, sessionId: string): Promise<void>;
|
|
4
|
+
export declare function getPiTrackedCacheKeepSessions(): Promise<import("@cortexkit/anthropic-auth-core").CacheKeepTrackedSession[]>;
|
|
3
5
|
type AnthropicEvent = {
|
|
4
6
|
type?: string;
|
|
5
7
|
index?: number;
|
package/dist/stream.js
CHANGED
|
@@ -1,12 +1,68 @@
|
|
|
1
|
-
import { applyClaudeCodeHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, dumpDirectRequest, FAST_MODE_BETA, FallbackAccountManager, getCache1hPersistentMode, getRelayConfig, getRoutingMode, isApiKeyAccount, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isOAuthAccount, isValidApiBaseURL, loadAccounts, mergeAnthropicBetas, QuotaManager, quotaSnapshotModelScopeIsExhausted, resolveClaudeCodeIdentity, sendViaRelay, setDumpEnabled, shouldFallbackStatus, } from '@cortexkit/anthropic-auth-core';
|
|
1
|
+
import { applyClaudeCodeHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, decideStickyQuotaFailure, dumpDirectRequest, FAST_MODE_BETA, FallbackAccountManager, getCache1hPersistentMode, getDefaultCacheKeepRegistryDirectory, getRelayConfig, getRoutingMode, getStickyRoutingStatePath, isApiKeyAccount, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isOAuthAccount, isPermanentRefreshError, isValidApiBaseURL, killswitchPassesPolicy, loadAccounts, mergeAnthropicBetas, QuotaManager, quotaSnapshotModelScopeIsExhausted, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, resolveClaudeCodeIdentity, STICKY_ROUTING_MAIN_ACCOUNT_ID, StickySessionRouter, sendViaRelay, setDumpEnabled, shouldFallbackStatus, stickyQuotaSnapshotIsFresh, stickyRetryAfterWithJitter, stickyRouteFamilyForModel, } from '@cortexkit/anthropic-auth-core';
|
|
2
2
|
import { calculateCost, createAssistantMessageEventStream, } from '@earendil-works/pi-ai';
|
|
3
3
|
import { buildAnthropicRequest, fromClaudeCodeToolName } from "./convert.js";
|
|
4
4
|
import { getPiAccountStoragePath } from "./paths.js";
|
|
5
5
|
function errorText(error) {
|
|
6
6
|
return error instanceof Error ? error.message : String(error);
|
|
7
7
|
}
|
|
8
|
+
let cacheKeepRegistry;
|
|
9
|
+
let cacheKeepRegistryDirectory;
|
|
10
|
+
const stickyRouters = new Map();
|
|
11
|
+
const quotaManagers = new Map();
|
|
12
|
+
const fallbackManagers = new Map();
|
|
13
|
+
const PI_SERVICE_CACHE_LIMIT = 16;
|
|
14
|
+
function setBoundedService(map, key, value) {
|
|
15
|
+
map.delete(key);
|
|
16
|
+
map.set(key, value);
|
|
17
|
+
while (map.size > PI_SERVICE_CACHE_LIMIT) {
|
|
18
|
+
const oldest = map.keys().next().value;
|
|
19
|
+
if (oldest === undefined)
|
|
20
|
+
break;
|
|
21
|
+
map.delete(oldest);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function getPiRoutingServices(storagePath, storage) {
|
|
25
|
+
let quotaManager = quotaManagers.get(storagePath);
|
|
26
|
+
let fallbackManager = fallbackManagers.get(storagePath);
|
|
27
|
+
if (!quotaManager || !fallbackManager) {
|
|
28
|
+
quotaManager = new QuotaManager({ storage });
|
|
29
|
+
fallbackManager = new FallbackAccountManager({
|
|
30
|
+
configPath: storagePath,
|
|
31
|
+
quotaManager,
|
|
32
|
+
});
|
|
33
|
+
setBoundedService(quotaManagers, storagePath, quotaManager);
|
|
34
|
+
setBoundedService(fallbackManagers, storagePath, fallbackManager);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
quotaManager.updateStorage(storage);
|
|
38
|
+
}
|
|
39
|
+
return { quotaManager, fallbackManager };
|
|
40
|
+
}
|
|
41
|
+
function getPiStickyRouter(storagePath) {
|
|
42
|
+
const path = process.env.PI_ANTHROPIC_AUTH_ROUTING_STATE_FILE ||
|
|
43
|
+
getStickyRoutingStatePath(storagePath);
|
|
44
|
+
let router = stickyRouters.get(path);
|
|
45
|
+
if (!router) {
|
|
46
|
+
router = new StickySessionRouter({ path });
|
|
47
|
+
setBoundedService(stickyRouters, path, router);
|
|
48
|
+
}
|
|
49
|
+
return router;
|
|
50
|
+
}
|
|
51
|
+
export async function clearPiStickyRoutingSession(storagePath, sessionId) {
|
|
52
|
+
await getPiStickyRouter(storagePath).clear(sessionId);
|
|
53
|
+
}
|
|
54
|
+
function getPiCacheKeepRegistry() {
|
|
55
|
+
const directory = process.env.PI_ANTHROPIC_AUTH_CACHEKEEP_REGISTRY_DIR ||
|
|
56
|
+
getDefaultCacheKeepRegistryDirectory('pi');
|
|
57
|
+
if (!cacheKeepRegistry || cacheKeepRegistryDirectory !== directory) {
|
|
58
|
+
cacheKeepRegistry = new CacheKeepSessionRegistry({ directory });
|
|
59
|
+
cacheKeepRegistryDirectory = directory;
|
|
60
|
+
}
|
|
61
|
+
return cacheKeepRegistry;
|
|
62
|
+
}
|
|
8
63
|
const cacheKeepManager = new CacheKeepManager({
|
|
9
64
|
loadStorage: () => loadAccounts(getPiAccountStoragePath()),
|
|
65
|
+
onTrackedSessionsChanged: (sessions) => getPiCacheKeepRegistry().publish(sessions),
|
|
10
66
|
prepareHeaders: async (headers, target) => {
|
|
11
67
|
const authorization = headers.get('authorization') ?? '';
|
|
12
68
|
const match = /^Bearer\s+(.+)$/i.exec(authorization);
|
|
@@ -31,6 +87,9 @@ const cacheKeepManager = new CacheKeepManager({
|
|
|
31
87
|
return headers;
|
|
32
88
|
},
|
|
33
89
|
});
|
|
90
|
+
export async function getPiTrackedCacheKeepSessions() {
|
|
91
|
+
return getPiCacheKeepRegistry().list(cacheKeepManager.trackedSessions());
|
|
92
|
+
}
|
|
34
93
|
function mapStopReason(reason) {
|
|
35
94
|
switch (reason) {
|
|
36
95
|
case 'end_turn':
|
|
@@ -178,13 +237,15 @@ async function sendAnthropicRequest(options) {
|
|
|
178
237
|
bodyText,
|
|
179
238
|
storage,
|
|
180
239
|
cacheMode: isCacheKeepHybridActive(storage) ? 'hybrid' : 'disabled',
|
|
240
|
+
oauthAccountId: options.oauthAccountId,
|
|
181
241
|
});
|
|
182
242
|
const directFetch = async () => {
|
|
183
243
|
try {
|
|
184
244
|
const response = await fetch(input, init);
|
|
185
245
|
await dumpDirectRequest({
|
|
186
246
|
affinity: relayAffinity,
|
|
187
|
-
route: options.
|
|
247
|
+
route: options.route ??
|
|
248
|
+
(options.apiAccount ? `api:${options.apiAccount.id}` : 'oauth'),
|
|
188
249
|
status: response.status,
|
|
189
250
|
bodyText,
|
|
190
251
|
url: input.toString(),
|
|
@@ -196,7 +257,8 @@ async function sendAnthropicRequest(options) {
|
|
|
196
257
|
catch (error) {
|
|
197
258
|
await dumpDirectRequest({
|
|
198
259
|
affinity: relayAffinity,
|
|
199
|
-
route: options.
|
|
260
|
+
route: options.route ??
|
|
261
|
+
(options.apiAccount ? `api:${options.apiAccount.id}` : 'oauth'),
|
|
200
262
|
error: errorText(error),
|
|
201
263
|
bodyText,
|
|
202
264
|
url: input.toString(),
|
|
@@ -245,11 +307,93 @@ async function firstStreamingError(response) {
|
|
|
245
307
|
return response;
|
|
246
308
|
}
|
|
247
309
|
async function executeWithFallback(options) {
|
|
248
|
-
const manager = new FallbackAccountManager({
|
|
249
|
-
configPath: options.storagePath,
|
|
250
|
-
});
|
|
251
310
|
const storage = await loadAccounts(options.storagePath);
|
|
252
|
-
const quotaManager =
|
|
311
|
+
const { quotaManager, fallbackManager: manager } = getPiRoutingServices(options.storagePath, storage);
|
|
312
|
+
quotaManager.seedMainFromStorage(storage, options.primaryAccessToken);
|
|
313
|
+
quotaManager.seedFallbacksFromAccounts((storage?.accounts ?? []).filter(isOAuthAccount));
|
|
314
|
+
async function buildStickyRoutes(modelId) {
|
|
315
|
+
const mainEntry = quotaManager.getMain(options.primaryAccessToken);
|
|
316
|
+
let mainQuota = mainEntry?.quota;
|
|
317
|
+
if (!stickyQuotaSnapshotIsFresh(mainEntry?.quota, storage, Date.now(), modelId)) {
|
|
318
|
+
try {
|
|
319
|
+
mainQuota = await quotaManager.refreshMain(options.primaryAccessToken);
|
|
320
|
+
}
|
|
321
|
+
catch { }
|
|
322
|
+
}
|
|
323
|
+
const usableFallbacks = await manager.getUsableFallbackAccounts(storage, {
|
|
324
|
+
modelId,
|
|
325
|
+
});
|
|
326
|
+
const usableById = new Map(usableFallbacks.map((account) => [account.id, account]));
|
|
327
|
+
const allRoutes = [];
|
|
328
|
+
if (!isPermanentRefreshError(storage?.refresh?.mainLastRefreshError)) {
|
|
329
|
+
allRoutes.push({
|
|
330
|
+
id: STICKY_ROUTING_MAIN_ACCOUNT_ID,
|
|
331
|
+
access: options.primaryAccessToken,
|
|
332
|
+
quota: mainQuota,
|
|
333
|
+
order: 0,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
for (const [index, configured] of (storage?.accounts ?? []).entries()) {
|
|
337
|
+
if (configured.enabled === false || !isOAuthAccount(configured))
|
|
338
|
+
continue;
|
|
339
|
+
const account = usableById.get(configured.id) ?? configured;
|
|
340
|
+
if (!account.access || isPermanentRefreshError(account.lastRefreshError))
|
|
341
|
+
continue;
|
|
342
|
+
let accountQuota = quotaManager.getFallback(account.id, account.access)?.quota ??
|
|
343
|
+
account.quota;
|
|
344
|
+
if (!stickyQuotaSnapshotIsFresh(accountQuota, storage, Date.now(), modelId)) {
|
|
345
|
+
try {
|
|
346
|
+
accountQuota = await quotaManager.refreshFallback(account.id, account.access);
|
|
347
|
+
}
|
|
348
|
+
catch { }
|
|
349
|
+
}
|
|
350
|
+
allRoutes.push({
|
|
351
|
+
id: account.id,
|
|
352
|
+
access: account.access,
|
|
353
|
+
quota: accountQuota,
|
|
354
|
+
order: index + 1,
|
|
355
|
+
account,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const retainAccountIds = new Set(allRoutes.flatMap((route) => {
|
|
359
|
+
const refreshError = route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID
|
|
360
|
+
? storage?.refresh?.mainLastRefreshError
|
|
361
|
+
: route.account?.lastRefreshError;
|
|
362
|
+
if (isPermanentRefreshError(refreshError))
|
|
363
|
+
return [];
|
|
364
|
+
if (stickyQuotaSnapshotIsFresh(route.quota, storage, Date.now(), modelId) &&
|
|
365
|
+
decideStickyQuotaFailure({ quota: route.quota, modelId }).action ===
|
|
366
|
+
'migrate') {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
if (isKillswitchEnabled(storage) &&
|
|
370
|
+
!killswitchPassesPolicy(route.quota, storage, route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID ? undefined : route.id, modelId)) {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
return [route.id];
|
|
374
|
+
}));
|
|
375
|
+
const usableIds = new Set(usableFallbacks.map((account) => account.id));
|
|
376
|
+
const candidates = allRoutes.flatMap((route) => {
|
|
377
|
+
if (!route.quota)
|
|
378
|
+
return [];
|
|
379
|
+
const accountId = route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID ? undefined : route.id;
|
|
380
|
+
const passes = quotaSnapshotPassesPolicy(route.quota, storage) &&
|
|
381
|
+
quotaSnapshotPassesModelScope(route.quota, modelId) &&
|
|
382
|
+
(!isKillswitchEnabled(storage) ||
|
|
383
|
+
killswitchPassesPolicy(route.quota, storage, accountId, modelId)) &&
|
|
384
|
+
(route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID || usableIds.has(route.id));
|
|
385
|
+
return passes
|
|
386
|
+
? [
|
|
387
|
+
{
|
|
388
|
+
accountId: route.id,
|
|
389
|
+
quota: route.quota,
|
|
390
|
+
order: route.order,
|
|
391
|
+
},
|
|
392
|
+
]
|
|
393
|
+
: [];
|
|
394
|
+
});
|
|
395
|
+
return { allRoutes, candidates, retainAccountIds };
|
|
396
|
+
}
|
|
253
397
|
async function primaryQuotaRefreshConfirmsExhausted() {
|
|
254
398
|
try {
|
|
255
399
|
const quota = await quotaManager.refreshMain(options.primaryAccessToken);
|
|
@@ -282,7 +426,7 @@ async function executeWithFallback(options) {
|
|
|
282
426
|
function primaryFreshModelScopeExhausted() {
|
|
283
427
|
const entry = quotaManager.getMain(options.primaryAccessToken);
|
|
284
428
|
return Boolean(entry &&
|
|
285
|
-
|
|
429
|
+
!quotaManager.isMainStale(options.model.id) &&
|
|
286
430
|
quotaSnapshotModelScopeIsExhausted(entry.quota, options.model.id));
|
|
287
431
|
}
|
|
288
432
|
async function tryFallbackAccounts(routeOptions = {}) {
|
|
@@ -298,7 +442,7 @@ async function executeWithFallback(options) {
|
|
|
298
442
|
if (!account)
|
|
299
443
|
continue;
|
|
300
444
|
if (isOAuthAccount(account)) {
|
|
301
|
-
if (!account.access)
|
|
445
|
+
if (routeOptions.apiOnly === true || !account.access)
|
|
302
446
|
continue;
|
|
303
447
|
response = await sendAnthropicRequest({
|
|
304
448
|
...options,
|
|
@@ -330,7 +474,191 @@ async function executeWithFallback(options) {
|
|
|
330
474
|
}
|
|
331
475
|
return null;
|
|
332
476
|
}
|
|
333
|
-
const
|
|
477
|
+
const routingMode = getRoutingMode(storage);
|
|
478
|
+
if (routingMode === 'sticky-balanced' && options.streamOptions?.sessionId) {
|
|
479
|
+
const sessionId = options.streamOptions.sessionId;
|
|
480
|
+
const router = getPiStickyRouter(options.storagePath);
|
|
481
|
+
const initialInputBytes = Math.max(1, Buffer.byteLength(JSON.stringify(options.context)));
|
|
482
|
+
let routes = await buildStickyRoutes(options.model.id);
|
|
483
|
+
const incompleteQuotaPool = routes.allRoutes.length === 0 ||
|
|
484
|
+
routes.allRoutes.some((candidate) => !candidate.quota ||
|
|
485
|
+
!stickyQuotaSnapshotIsFresh(candidate.quota, storage, Date.now(), options.model.id));
|
|
486
|
+
let resolution = await router.resolve({
|
|
487
|
+
sessionId,
|
|
488
|
+
family: stickyRouteFamilyForModel(options.model.id),
|
|
489
|
+
modelId: options.model.id,
|
|
490
|
+
candidates: routes.candidates,
|
|
491
|
+
retainAccountIds: routes.retainAccountIds,
|
|
492
|
+
storage,
|
|
493
|
+
inputBytes: initialInputBytes,
|
|
494
|
+
});
|
|
495
|
+
if (!resolution && incompleteQuotaPool) {
|
|
496
|
+
const error = new Error('Sticky-balanced routing is waiting for current OAuth quota snapshots');
|
|
497
|
+
Object.assign(error, {
|
|
498
|
+
code: 'ECONNRESET',
|
|
499
|
+
syscall: 'sticky-routing',
|
|
500
|
+
});
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
let route = routes.allRoutes.find((candidate) => candidate.id === resolution?.accountId);
|
|
504
|
+
if (resolution && route) {
|
|
505
|
+
const sendRoute = (selected) => sendAnthropicRequest({
|
|
506
|
+
...options,
|
|
507
|
+
accessToken: selected.access,
|
|
508
|
+
oauthAccountId: selected.id,
|
|
509
|
+
route: `sticky:${selected.id}`,
|
|
510
|
+
});
|
|
511
|
+
const completeRoute = async (selected, response, markUsed = true) => {
|
|
512
|
+
if (markUsed && selected.account)
|
|
513
|
+
await manager.markUsed(selected.account);
|
|
514
|
+
return response;
|
|
515
|
+
};
|
|
516
|
+
const proactiveQuotaDecision = stickyQuotaSnapshotIsFresh(route.quota, storage, Date.now(), options.model.id)
|
|
517
|
+
? decideStickyQuotaFailure({
|
|
518
|
+
quota: route.quota,
|
|
519
|
+
modelId: options.model.id,
|
|
520
|
+
})
|
|
521
|
+
: undefined;
|
|
522
|
+
if (proactiveQuotaDecision?.action === 'hold') {
|
|
523
|
+
return completeRoute(route, new Response(JSON.stringify({
|
|
524
|
+
type: 'error',
|
|
525
|
+
error: {
|
|
526
|
+
type: 'rate_limit_error',
|
|
527
|
+
message: 'Sticky OAuth account five-hour quota resets shortly; retaining session affinity.',
|
|
528
|
+
},
|
|
529
|
+
}), {
|
|
530
|
+
status: 429,
|
|
531
|
+
headers: {
|
|
532
|
+
'content-type': 'application/json',
|
|
533
|
+
'retry-after': String(stickyRetryAfterWithJitter(sessionId, proactiveQuotaDecision.retryAfterSeconds)),
|
|
534
|
+
},
|
|
535
|
+
}), false);
|
|
536
|
+
}
|
|
537
|
+
let response = await sendRoute(route);
|
|
538
|
+
let preflight = await firstStreamingError(response);
|
|
539
|
+
if (preflight instanceof Response && preflight.ok) {
|
|
540
|
+
return completeRoute(route, preflight);
|
|
541
|
+
}
|
|
542
|
+
let permanentAuthFailure = preflight instanceof Response &&
|
|
543
|
+
preflight.status === 401 &&
|
|
544
|
+
route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID;
|
|
545
|
+
if (preflight instanceof Response &&
|
|
546
|
+
preflight.status === 401 &&
|
|
547
|
+
route.account &&
|
|
548
|
+
storage) {
|
|
549
|
+
const authRouteId = route.id;
|
|
550
|
+
try {
|
|
551
|
+
const refreshed = await manager.refreshAccount(route.account, storage, {
|
|
552
|
+
force: true,
|
|
553
|
+
});
|
|
554
|
+
if (refreshed.access) {
|
|
555
|
+
route = { ...route, access: refreshed.access, account: refreshed };
|
|
556
|
+
}
|
|
557
|
+
await preflight.body?.cancel().catch(() => { });
|
|
558
|
+
response = await sendRoute(route);
|
|
559
|
+
preflight = await firstStreamingError(response);
|
|
560
|
+
if (preflight instanceof Response && preflight.ok) {
|
|
561
|
+
return completeRoute(route, preflight);
|
|
562
|
+
}
|
|
563
|
+
permanentAuthFailure =
|
|
564
|
+
preflight instanceof Response && preflight.status === 401;
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
const latest = await loadAccounts(options.storagePath);
|
|
568
|
+
const refreshError = latest?.accounts.find((account) => account.id === authRouteId && isOAuthAccount(account))?.lastRefreshError;
|
|
569
|
+
if (!isPermanentRefreshError(refreshError))
|
|
570
|
+
throw error;
|
|
571
|
+
permanentAuthFailure = true;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
let migrate = (preflight instanceof Response && preflight.status === 403) ||
|
|
575
|
+
permanentAuthFailure;
|
|
576
|
+
if (primaryResponseAllowsApiFallback(preflight)) {
|
|
577
|
+
let quota;
|
|
578
|
+
try {
|
|
579
|
+
quota =
|
|
580
|
+
route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID
|
|
581
|
+
? await quotaManager.refreshMain(route.access)
|
|
582
|
+
: await quotaManager.refreshFallback(route.id, route.access);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// Retain affinity when the quota probe itself is unavailable.
|
|
586
|
+
quota = undefined;
|
|
587
|
+
}
|
|
588
|
+
const decision = decideStickyQuotaFailure({
|
|
589
|
+
quota,
|
|
590
|
+
modelId: options.model.id,
|
|
591
|
+
});
|
|
592
|
+
if (decision.action === 'hold') {
|
|
593
|
+
const headers = new Headers(preflight instanceof Response
|
|
594
|
+
? preflight.headers
|
|
595
|
+
: response.headers);
|
|
596
|
+
headers.set('retry-after', String(stickyRetryAfterWithJitter(sessionId, decision.retryAfterSeconds)));
|
|
597
|
+
if (preflight instanceof Response) {
|
|
598
|
+
return completeRoute(route, new Response(preflight.body, {
|
|
599
|
+
status: preflight.status,
|
|
600
|
+
statusText: preflight.statusText,
|
|
601
|
+
headers,
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
await response.body?.cancel().catch(() => { });
|
|
605
|
+
headers.set('content-type', 'application/json');
|
|
606
|
+
return completeRoute(route, new Response(JSON.stringify({
|
|
607
|
+
type: 'error',
|
|
608
|
+
error: {
|
|
609
|
+
type: 'rate_limit_error',
|
|
610
|
+
message: 'Sticky OAuth account five-hour quota resets shortly; retaining session affinity.',
|
|
611
|
+
},
|
|
612
|
+
}), { status: 429, headers }));
|
|
613
|
+
}
|
|
614
|
+
migrate = decision.action === 'migrate';
|
|
615
|
+
}
|
|
616
|
+
if (migrate) {
|
|
617
|
+
const failedRouteId = route.id;
|
|
618
|
+
routes = await buildStickyRoutes(options.model.id);
|
|
619
|
+
if (routes.candidates.some((candidate) => candidate.accountId !== failedRouteId)) {
|
|
620
|
+
if (preflight instanceof Response) {
|
|
621
|
+
await preflight.body?.cancel().catch(() => { });
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
await response.body?.cancel().catch(() => { });
|
|
625
|
+
}
|
|
626
|
+
resolution = await router.resolve({
|
|
627
|
+
sessionId,
|
|
628
|
+
family: stickyRouteFamilyForModel(options.model.id),
|
|
629
|
+
modelId: options.model.id,
|
|
630
|
+
candidates: routes.candidates,
|
|
631
|
+
retainAccountIds: routes.retainAccountIds,
|
|
632
|
+
storage,
|
|
633
|
+
inputBytes: initialInputBytes,
|
|
634
|
+
excludeAccountIds: new Set([failedRouteId]),
|
|
635
|
+
});
|
|
636
|
+
const migrated = routes.allRoutes.find((candidate) => candidate.id === resolution?.accountId);
|
|
637
|
+
if (resolution && migrated) {
|
|
638
|
+
route = migrated;
|
|
639
|
+
return completeRoute(route, await sendRoute(route));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (primaryResponseAllowsApiFallback(preflight) &&
|
|
643
|
+
(await primaryQuotaRefreshConfirmsExhausted())) {
|
|
644
|
+
if (preflight instanceof Response) {
|
|
645
|
+
await preflight.body?.cancel().catch(() => { });
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
await response.body?.cancel().catch(() => { });
|
|
649
|
+
}
|
|
650
|
+
const apiFallback = await tryFallbackAccounts({
|
|
651
|
+
includeApiRoutes: true,
|
|
652
|
+
apiOnly: true,
|
|
653
|
+
});
|
|
654
|
+
if (apiFallback)
|
|
655
|
+
return apiFallback;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return completeRoute(route, preflight instanceof Response ? preflight : response);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const fallbackFirst = routingMode === 'fallback-first';
|
|
334
662
|
if (fallbackFirst) {
|
|
335
663
|
const fallback = await tryFallbackAccounts();
|
|
336
664
|
if (fallback)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/pi-anthropic-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"prepublishOnly": "bun run build"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cortexkit/anthropic-auth-core": "1.
|
|
39
|
+
"@cortexkit/anthropic-auth-core": "1.16.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@earendil-works/pi-ai": "*",
|