@cortexkit/pi-anthropic-auth 1.19.1 → 1.21.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.js +20 -3
- package/dist/convert.js +120 -22
- package/dist/index.js +11 -2
- package/dist/stream.js +124 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Pi package for CortexKit Anthropic OAuth support. It overrides Pi's built-in `anthropic` provider with a CortexKit provider extension backed by the shared `@cortexkit/anthropic-auth-core` package.
|
|
4
4
|
|
|
5
|
-
The Pi provider catalog includes Claude Fable 5 (`claude-fable-5`), limited-access Claude Mythos 5 (`claude-mythos-5`), Claude Opus 4.8, Claude Opus 4.5, Claude Sonnet 4.5, and Claude Sonnet 5 (`claude-sonnet-5`). Fable/Mythos reasoning uses Anthropic adaptive thinking with `thinking.display: "summarized"` and `output_config.effort`; the package does not send rejected manual `thinking.budget_tokens` for those models.
|
|
5
|
+
The Pi provider catalog includes Claude Fable 5 (`claude-fable-5`), limited-access Claude Mythos 5 (`claude-mythos-5`), Claude Opus 5 (`claude-opus-5`), Claude Opus 4.8, Claude Opus 4.5, Claude Sonnet 4.5, and Claude Sonnet 5 (`claude-sonnet-5`). Fable/Mythos reasoning uses Anthropic adaptive thinking with `thinking.display: "summarized"` and `output_config.effort`; the package does not send rejected manual `thinking.budget_tokens` for those models.
|
|
6
6
|
|
|
7
7
|
This package is part of the CortexKit Anthropic Auth monorepo, which supports both OpenCode (`@cortexkit/opencode-anthropic-auth`) and Pi (`@cortexkit/pi-anthropic-auth`) through the same shared core logic.
|
|
8
8
|
|
|
@@ -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. 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.
|
|
45
|
+
The sidecar uses the same JSON shape as the OpenCode package, including `routing`, `claudeCache`, `cacheKeep`, `prime`, `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
|
|
|
@@ -59,6 +59,10 @@ The sidecar uses the same JSON shape as the OpenCode package, including `routing
|
|
|
59
59
|
/claude-cachekeep 09-23
|
|
60
60
|
/claude-cachekeep off
|
|
61
61
|
|
|
62
|
+
/claude-prime
|
|
63
|
+
/claude-prime on
|
|
64
|
+
/claude-prime off
|
|
65
|
+
|
|
62
66
|
/claude-dump
|
|
63
67
|
/claude-dump on
|
|
64
68
|
/claude-dump off
|
|
@@ -78,6 +82,16 @@ The sidecar uses the same JSON shape as the OpenCode package, including `routing
|
|
|
78
82
|
|
|
79
83
|
`/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`, `claude-opus-4-8`, and `claude-opus-5`).
|
|
80
84
|
|
|
85
|
+
### Claude quota window priming
|
|
86
|
+
|
|
87
|
+
Priming is off by default. When enabled from OpenCode with `/claude-prime on`, it watches each OAuth account's 5-hour quota reset and sends one minimal `claude-haiku-4-5` request about one minute after a confirmed reset. This starts the next window without waiting for a normal prompt. Usage is measured from response accounting.
|
|
88
|
+
|
|
89
|
+
For an idle account with no cached reset time, one bootstrap request establishes the first observed window. Atomic temporary-file claims limit multiple processes sharing an account config to one request per account and reset.
|
|
90
|
+
|
|
91
|
+
Prime marker identities live in `anthropic-auth-state.json`. Plugin-owned refresh rotations preserve the main account's lineage, while a host credential replacement creates a new lineage. An existing main lineage without a refresh-token binding attaches to the current credential on its first check without changing identity. On upgrade, an existing fallback account receives an identity during its first prime check; that one-time marker change can send one extra request in the current window.
|
|
92
|
+
|
|
93
|
+
Pi's `/claude-prime` command displays status only. The `on` and `off` arguments are ignored; toggling priming requires OpenCode.
|
|
94
|
+
|
|
81
95
|
## Relay
|
|
82
96
|
|
|
83
97
|
The Pi package can use the same user-owned Cloudflare relay config as the OpenCode package. The relay setup helper currently lives in the OpenCode package CLI:
|
package/dist/commands.js
CHANGED
|
@@ -1,6 +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, 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
|
-
import { getPiAccountStoragePath } from
|
|
3
|
-
import { clearPiStickyRoutingSession, getPiTrackedCacheKeepSessions, } from
|
|
1
|
+
import { buildClaudeQuotaSummary, buildFallbackQuotaSummaries, buildPrimeAccountStatuses, CLAUDE_ACCOUNT_COMMAND_NAME, CLAUDE_CACHE_KEEP_COMMAND_NAME, CLAUDE_PRIME_COMMAND_NAME, CLAUDE_ROUTING_COMMAND_NAME, createEmptyStorage, executeAccountCommand, executeCache1hCommand, executeCacheKeepCommand, executeDumpCommand, executeFastModeCommand, executeLoggingCommand, executePrimeCommand, executeRoutingCommand, getCache1hPersistentMode, getCacheKeepWindow, getPersistedLogLevel, getRoutingMode, isCache1hPersistentlyEnabled, isCacheKeepAlways, isCacheKeepHybridActive, isCacheKeepPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isPrimePersistentlyEnabled, 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
|
+
import { getPiAccountStoragePath } from './paths.js';
|
|
3
|
+
import { clearPiStickyRoutingSession, getPiTrackedCacheKeepSessions, } from './stream.js';
|
|
4
4
|
function notify(ctx, message, kind = 'info') {
|
|
5
5
|
ctx.ui.notify(message, kind);
|
|
6
6
|
}
|
|
@@ -200,4 +200,21 @@ export function registerCommands(pi) {
|
|
|
200
200
|
notify(ctx, executeLoggingCommand({ argumentsText: args ?? '', level }));
|
|
201
201
|
},
|
|
202
202
|
});
|
|
203
|
+
pi.registerCommand(CLAUDE_PRIME_COMMAND_NAME, {
|
|
204
|
+
description: "Show each OAuth account's five-hour quota window state",
|
|
205
|
+
handler: async (_args, ctx) => {
|
|
206
|
+
// Pi has no live PrimeManager; render persisted counters and the
|
|
207
|
+
// next-due timestamp from stored quota snapshots. Always pass
|
|
208
|
+
// `argumentsText: 'status'` to the pure executor so on/off args
|
|
209
|
+
// are display-only — Pi cannot toggle the setting.
|
|
210
|
+
const path = getPiAccountStoragePath();
|
|
211
|
+
const storage = await loadAccounts(path);
|
|
212
|
+
const accounts = buildPrimeAccountStatuses(storage, { now: Date.now() });
|
|
213
|
+
notify(ctx, executePrimeCommand({
|
|
214
|
+
argumentsText: 'status',
|
|
215
|
+
enabled: isPrimePersistentlyEnabled(storage),
|
|
216
|
+
accounts,
|
|
217
|
+
}).text);
|
|
218
|
+
},
|
|
219
|
+
});
|
|
203
220
|
}
|
package/dist/convert.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { applyClaudeCodeMetadata, buildBillingHeaderValue, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING, CLAUDE_OPUS_5_ADAPTIVE_THINKING, CLAUDE_SONNET_5_ADAPTIVE_THINKING, isClaudeFableOrMythos5Model, isClaudeOpus5Model, isClaudeSonnet5Model, isFastModeSupportedModel, isOpenAIReasoningSignature, orderClaudeCodeBody, signRequestBody, } from '@cortexkit/anthropic-auth-core';
|
|
2
|
+
// Anchor identifying Pi's documentation paragraph — the only part of the prompt
|
|
3
|
+
// that Anthropic currently rejects in system[]. Unknown prompt shapes take the
|
|
4
|
+
// service-preserving fallback below instead of returning the full prompt there.
|
|
5
|
+
const PI_DOCS_ANCHOR = 'Pi documentation';
|
|
6
|
+
const ANTHROPIC_REPLAY_APIS = new Set([
|
|
7
|
+
'anthropic-messages',
|
|
8
|
+
'cortexkit-anthropic-messages',
|
|
9
|
+
]);
|
|
2
10
|
const CLAUDE_CODE_TOOLS = new Map([
|
|
3
11
|
'Read',
|
|
4
12
|
'Write',
|
|
@@ -101,7 +109,7 @@ function convertTextAndImages(content) {
|
|
|
101
109
|
}
|
|
102
110
|
return blocks;
|
|
103
111
|
}
|
|
104
|
-
function convertMessages(messages) {
|
|
112
|
+
function convertMessages(messages, targetModelId) {
|
|
105
113
|
const result = [];
|
|
106
114
|
for (let index = 0; index < messages.length; index++) {
|
|
107
115
|
const message = messages[index];
|
|
@@ -136,33 +144,63 @@ function convertMessages(messages) {
|
|
|
136
144
|
if (block.type === 'text' && block.text.trim()) {
|
|
137
145
|
blocks.push({ type: 'text', text: sanitize(block.text) });
|
|
138
146
|
}
|
|
139
|
-
else if (block.type === 'thinking'
|
|
147
|
+
else if (block.type === 'thinking') {
|
|
140
148
|
const thinking = block;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
const signature = thinking.thinkingSignature;
|
|
150
|
+
const hasSignature = typeof signature === 'string' && signature.trim().length > 0;
|
|
151
|
+
const signatureBelongsToTarget = message.provider === 'anthropic' &&
|
|
152
|
+
ANTHROPIC_REPLAY_APIS.has(message.api) &&
|
|
153
|
+
message.model === targetModelId &&
|
|
154
|
+
!isOpenAIReasoningSignature(signature);
|
|
155
|
+
// Match Pi's built-in origin check: only replay signatures from the
|
|
156
|
+
// same Anthropic API/model. Preserve visible foreign reasoning as
|
|
157
|
+
// ordinary text, but drop opaque redacted/OpenAI encrypted state.
|
|
158
|
+
if (hasSignature && !signatureBelongsToTarget) {
|
|
159
|
+
if (!thinking.redacted &&
|
|
160
|
+
!isOpenAIReasoningSignature(signature) &&
|
|
161
|
+
thinking.thinking.trim()) {
|
|
162
|
+
blocks.push({ type: 'text', text: sanitize(thinking.thinking) });
|
|
163
|
+
}
|
|
145
164
|
continue;
|
|
146
165
|
}
|
|
147
|
-
if (thinking.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
if (thinking.redacted) {
|
|
167
|
+
if (hasSignature) {
|
|
168
|
+
blocks.push({
|
|
169
|
+
type: 'redacted_thinking',
|
|
170
|
+
data: signature,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!hasSignature) {
|
|
176
|
+
// Anthropic rejects unsigned thinking blocks. Preserve readable
|
|
177
|
+
// output from interrupted streams as ordinary assistant text.
|
|
178
|
+
if (thinking.thinking.trim()) {
|
|
179
|
+
blocks.push({
|
|
180
|
+
type: 'text',
|
|
181
|
+
text: sanitize(thinking.thinking),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (hasLoneSurrogate(thinking.thinking)) {
|
|
187
|
+
// Signature authenticates the thinking bytes exactly. Sanitizing the
|
|
188
|
+
// text would invalidate it, so downgrade malformed signed thinking
|
|
189
|
+
// to plain text and omit the signature instead.
|
|
190
|
+
if (thinking.thinking.trim()) {
|
|
191
|
+
blocks.push({
|
|
192
|
+
type: 'text',
|
|
193
|
+
text: sanitize(thinking.thinking),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
153
198
|
blocks.push({
|
|
154
199
|
type: 'thinking',
|
|
155
200
|
thinking: thinking.thinking,
|
|
156
|
-
signature
|
|
201
|
+
signature,
|
|
157
202
|
});
|
|
158
203
|
}
|
|
159
|
-
else {
|
|
160
|
-
// Either unsigned, or signed-but-contains a lone surrogate. In the
|
|
161
|
-
// latter case we cannot keep the signature: sanitizing breaks it and
|
|
162
|
-
// sending the raw lone surrogate is an invalid-UTF8 400. Drop the
|
|
163
|
-
// signature and downgrade to sanitized text.
|
|
164
|
-
blocks.push({ type: 'text', text: sanitize(thinking.thinking) });
|
|
165
|
-
}
|
|
166
204
|
}
|
|
167
205
|
else if (block.type === 'toolCall') {
|
|
168
206
|
blocks.push({
|
|
@@ -253,6 +291,38 @@ function addEphemeralCacheControl(body) {
|
|
|
253
291
|
break;
|
|
254
292
|
}
|
|
255
293
|
}
|
|
294
|
+
function splitPiSystemPrompt(prompt) {
|
|
295
|
+
const sanitized = sanitize(prompt);
|
|
296
|
+
const paragraphs = sanitized.split(/\n\n+/);
|
|
297
|
+
const docs = paragraphs.filter((paragraph) => paragraph.includes(PI_DOCS_ANCHOR));
|
|
298
|
+
// If Pi changes the heading or prompt layout, fail away from the request shape
|
|
299
|
+
// known to trigger billing rejection: carry the complete host prompt in the
|
|
300
|
+
// first user message until the new shape can be classified precisely.
|
|
301
|
+
if (docs.length === 0)
|
|
302
|
+
return { messageText: sanitized };
|
|
303
|
+
const keep = paragraphs.filter((paragraph) => !paragraph.includes(PI_DOCS_ANCHOR));
|
|
304
|
+
return {
|
|
305
|
+
...(keep.length ? { systemText: keep.join('\n\n') } : {}),
|
|
306
|
+
messageText: docs.join('\n\n'),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function prependCachedPromptBlock(messages, text) {
|
|
310
|
+
const firstUser = messages.find((message) => message.role === 'user');
|
|
311
|
+
if (!firstUser)
|
|
312
|
+
return;
|
|
313
|
+
const promptBlock = {
|
|
314
|
+
type: 'text',
|
|
315
|
+
text,
|
|
316
|
+
cache_control: { type: 'ephemeral' },
|
|
317
|
+
};
|
|
318
|
+
if (typeof firstUser.content === 'string') {
|
|
319
|
+
firstUser.content = [promptBlock, { type: 'text', text: firstUser.content }];
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (Array.isArray(firstUser.content)) {
|
|
323
|
+
firstUser.content.unshift(promptBlock);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
256
326
|
function applyCacheMode(body, enabled, mode) {
|
|
257
327
|
if (!enabled)
|
|
258
328
|
return;
|
|
@@ -282,7 +352,7 @@ function applyCacheMode(body, enabled, mode) {
|
|
|
282
352
|
addTtl(body);
|
|
283
353
|
}
|
|
284
354
|
export async function buildAnthropicRequest(modelId, context, options, cache, fastModeEnabled = false, identity) {
|
|
285
|
-
const messages = convertMessages(context.messages);
|
|
355
|
+
const messages = convertMessages(context.messages, modelId);
|
|
286
356
|
// Strip trailing assistant messages — Anthropic rejects prefill on some models
|
|
287
357
|
while (messages.length &&
|
|
288
358
|
messages[messages.length - 1]?.role === 'assistant') {
|
|
@@ -296,7 +366,35 @@ export async function buildAnthropicRequest(modelId, context, options, cache, fa
|
|
|
296
366
|
{ type: 'text', text: CLAUDE_CODE_IDENTITY },
|
|
297
367
|
];
|
|
298
368
|
if (context.systemPrompt?.trim()) {
|
|
299
|
-
system
|
|
369
|
+
// Pi's prompt cannot sit whole in the top-level system[] array: two lines of
|
|
370
|
+
// its documentation paragraph (the docs/*.md enumeration and the "follow .md
|
|
371
|
+
// cross-references" instruction) are each independently sufficient to make
|
|
372
|
+
// Anthropic reject the request with 400 "You're out of extra usage". Entry
|
|
373
|
+
// count and payload size are ruled out — 2697 bytes of neutral filler in the
|
|
374
|
+
// same position is accepted, and the same text is accepted inside messages[].
|
|
375
|
+
//
|
|
376
|
+
// For the recognized Pi prompt, keep the identity, tool contract, and
|
|
377
|
+
// guidelines in system[], where they carry system weight and survive context
|
|
378
|
+
// compaction, and carry only the documentation paragraph in messages[]. An
|
|
379
|
+
// unknown future prompt shape is moved whole rather than risking this 400.
|
|
380
|
+
//
|
|
381
|
+
// It goes in as its own content block ahead of the user's text, not merged
|
|
382
|
+
// into it and not as a separate message. A cache prefix matches contiguously
|
|
383
|
+
// from the start of the request, so a block boundary here lets the prefix end
|
|
384
|
+
// before the user's words: a new conversation with a different first message
|
|
385
|
+
// still reads the paragraph from cache instead of re-writing ~1.1k tokens.
|
|
386
|
+
// A role: "system" message cannot be used at messages[0] — Anthropic rejects
|
|
387
|
+
// that — and placing one after the first user message puts it behind content
|
|
388
|
+
// that varies, which defeats the caching.
|
|
389
|
+
//
|
|
390
|
+
// cache_control is set explicitly because addEphemeralCacheControl's
|
|
391
|
+
// message-level breakpoint only fires for array content on the *last* user
|
|
392
|
+
// message, which is not this one after the first turn.
|
|
393
|
+
const prompt = splitPiSystemPrompt(context.systemPrompt);
|
|
394
|
+
if (prompt.systemText) {
|
|
395
|
+
system.push({ type: 'text', text: prompt.systemText });
|
|
396
|
+
}
|
|
397
|
+
prependCachedPromptBlock(messages, prompt.messageText);
|
|
300
398
|
}
|
|
301
399
|
const body = {
|
|
302
400
|
model: modelId,
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { authorize, CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW, CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS, CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS, CLAUDE_FABLE_MYTHOS_5_PRICING, exchange, refreshClaudeOAuthToken, } from '@cortexkit/anthropic-auth-core';
|
|
2
|
-
import { registerCommands } from
|
|
3
|
-
import { streamCortexKitAnthropic } from
|
|
2
|
+
import { registerCommands } from './commands.js';
|
|
3
|
+
import { streamCortexKitAnthropic } from './stream.js';
|
|
4
4
|
async function loginAnthropic(callbacks) {
|
|
5
5
|
const auth = await authorize('max');
|
|
6
6
|
callbacks.onAuth({ url: auth.url });
|
|
@@ -51,6 +51,15 @@ export default function cortexKitPiAnthropicAuth(pi) {
|
|
|
51
51
|
contextWindow: CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW,
|
|
52
52
|
maxTokens: CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS,
|
|
53
53
|
})),
|
|
54
|
+
{
|
|
55
|
+
id: 'claude-opus-5',
|
|
56
|
+
name: 'Claude Opus 5',
|
|
57
|
+
reasoning: true,
|
|
58
|
+
input: textImageInput(),
|
|
59
|
+
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
60
|
+
contextWindow: 1_000_000,
|
|
61
|
+
maxTokens: 128_000,
|
|
62
|
+
},
|
|
54
63
|
{
|
|
55
64
|
id: 'claude-opus-4-8',
|
|
56
65
|
name: 'Claude Opus 4.8',
|
package/dist/stream.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { applyClaudeCodeHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, createStickyNoRouteResponse, 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';
|
|
1
|
+
import { applyClaudeCodeHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, createStickyNoRouteResponse, decideStickyQuotaFailure, dumpDirectRequest, FAST_MODE_BETA, FallbackAccountManager, getCache1hPersistentMode, getDefaultCacheKeepRegistryDirectory, getFallbackReauthLabels, getOrCreateMainAccountId, getRelayConfig, getRoutingMode, getStickyRoutingStatePath, isApiKeyAccount, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isOAuthAccount, isPermanentRefreshError, isValidApiBaseURL, killswitchPassesPolicy, loadAccounts, mergeAnthropicBetas, QuotaManager, quotaSnapshotHasStandardWindows, quotaSnapshotModelScopeIsExhausted, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, refreshBackoffActive, resolveClaudeCodeIdentity, STICKY_ROUTING_MAIN_ACCOUNT_ID, StickySessionRouter, sendViaRelay, setDumpEnabled, shouldFallbackStatus, stickyQuotaSnapshotIsFresh, stickyRetryAfterWithJitter, stickyRouteFamilyForModel, } from '@cortexkit/anthropic-auth-core';
|
|
2
2
|
import { calculateCost, createAssistantMessageEventStream, } from '@earendil-works/pi-ai';
|
|
3
|
-
import { buildAnthropicRequest, fromClaudeCodeToolName } from
|
|
4
|
-
import { getPiAccountStoragePath } from
|
|
3
|
+
import { buildAnthropicRequest, fromClaudeCodeToolName } from './convert.js';
|
|
4
|
+
import { getPiAccountStoragePath } from './paths.js';
|
|
5
5
|
function errorText(error) {
|
|
6
6
|
return error instanceof Error ? error.message : String(error);
|
|
7
7
|
}
|
|
@@ -10,6 +10,7 @@ let cacheKeepRegistryDirectory;
|
|
|
10
10
|
const stickyRouters = new Map();
|
|
11
11
|
const quotaManagers = new Map();
|
|
12
12
|
const fallbackManagers = new Map();
|
|
13
|
+
const mainAccountIdInitializations = new Map();
|
|
13
14
|
const PI_SERVICE_CACHE_LIMIT = 16;
|
|
14
15
|
function setBoundedService(map, key, value) {
|
|
15
16
|
map.delete(key);
|
|
@@ -21,6 +22,19 @@ function setBoundedService(map, key, value) {
|
|
|
21
22
|
map.delete(oldest);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
function ensurePiMainAccountId(storagePath) {
|
|
26
|
+
let initialization = mainAccountIdInitializations.get(storagePath);
|
|
27
|
+
if (!initialization) {
|
|
28
|
+
initialization = getOrCreateMainAccountId(storagePath).catch((error) => {
|
|
29
|
+
if (mainAccountIdInitializations.get(storagePath) === initialization) {
|
|
30
|
+
mainAccountIdInitializations.delete(storagePath);
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
});
|
|
34
|
+
setBoundedService(mainAccountIdInitializations, storagePath, initialization);
|
|
35
|
+
}
|
|
36
|
+
return initialization;
|
|
37
|
+
}
|
|
24
38
|
function getPiRoutingServices(storagePath, storage) {
|
|
25
39
|
let quotaManager = quotaManagers.get(storagePath);
|
|
26
40
|
let fallbackManager = fallbackManagers.get(storagePath);
|
|
@@ -71,7 +85,10 @@ const cacheKeepManager = new CacheKeepManager({
|
|
|
71
85
|
return headers;
|
|
72
86
|
try {
|
|
73
87
|
const body = JSON.parse(target.bodyText);
|
|
74
|
-
const
|
|
88
|
+
const storagePath = getPiAccountStoragePath();
|
|
89
|
+
await ensurePiMainAccountId(storagePath);
|
|
90
|
+
const storage = await loadAccounts(storagePath);
|
|
91
|
+
const identity = await resolveClaudeCodeIdentity(accessToken, typeof body.model === 'string' ? body.model : undefined, target.oauthAccountId ?? storage?.mainAccountId);
|
|
75
92
|
headers.delete('anthropic-beta');
|
|
76
93
|
applyClaudeCodeHeaders(headers, accessToken, { body, identity });
|
|
77
94
|
headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [
|
|
@@ -201,10 +218,14 @@ export async function* parseSse(response) {
|
|
|
201
218
|
}
|
|
202
219
|
}
|
|
203
220
|
async function sendAnthropicRequest(options) {
|
|
221
|
+
await ensurePiMainAccountId(options.storagePath);
|
|
204
222
|
const storage = await loadAccounts(options.storagePath);
|
|
205
223
|
setDumpEnabled(isDumpPersistentlyEnabled(storage));
|
|
224
|
+
const accountIdentity = options.oauthAccountId === STICKY_ROUTING_MAIN_ACCOUNT_ID
|
|
225
|
+
? storage?.mainAccountId
|
|
226
|
+
: options.oauthAccountId;
|
|
206
227
|
const identity = options.accessToken
|
|
207
|
-
? await resolveClaudeCodeIdentity(options.accessToken, options.model.id)
|
|
228
|
+
? await resolveClaudeCodeIdentity(options.accessToken, options.model.id, accountIdentity)
|
|
208
229
|
: undefined;
|
|
209
230
|
const { body, bodyText } = await buildAnthropicRequest(options.model.id, options.context, options.streamOptions, {
|
|
210
231
|
enabled: isCache1hPersistentlyEnabled(storage),
|
|
@@ -307,16 +328,18 @@ async function firstStreamingError(response) {
|
|
|
307
328
|
return response;
|
|
308
329
|
}
|
|
309
330
|
async function executeWithFallback(options) {
|
|
331
|
+
await ensurePiMainAccountId(options.storagePath);
|
|
310
332
|
const storage = await loadAccounts(options.storagePath);
|
|
311
333
|
const { quotaManager, fallbackManager: manager } = getPiRoutingServices(options.storagePath, storage);
|
|
312
|
-
|
|
334
|
+
const mainAccountId = storage?.mainAccountId;
|
|
335
|
+
quotaManager.seedMainFromStorage(storage, mainAccountId);
|
|
313
336
|
quotaManager.seedFallbacksFromAccounts((storage?.accounts ?? []).filter(isOAuthAccount));
|
|
314
337
|
async function buildStickyRoutes(modelId) {
|
|
315
|
-
const mainEntry = quotaManager.getMain(
|
|
338
|
+
const mainEntry = quotaManager.getMain(mainAccountId);
|
|
316
339
|
let mainQuota = mainEntry?.quota;
|
|
317
340
|
if (!stickyQuotaSnapshotIsFresh(mainEntry?.quota, storage, Date.now(), modelId)) {
|
|
318
341
|
try {
|
|
319
|
-
mainQuota = await quotaManager.refreshMain(options.primaryAccessToken);
|
|
342
|
+
mainQuota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
|
|
320
343
|
}
|
|
321
344
|
catch { }
|
|
322
345
|
}
|
|
@@ -339,11 +362,10 @@ async function executeWithFallback(options) {
|
|
|
339
362
|
const account = usableById.get(configured.id) ?? configured;
|
|
340
363
|
if (!account.access || isPermanentRefreshError(account.lastRefreshError))
|
|
341
364
|
continue;
|
|
342
|
-
let accountQuota = quotaManager.getFallback(account.id, account
|
|
343
|
-
account.quota;
|
|
365
|
+
let accountQuota = quotaManager.getFallback(account.id, account)?.quota ?? account.quota;
|
|
344
366
|
if (!stickyQuotaSnapshotIsFresh(accountQuota, storage, Date.now(), modelId)) {
|
|
345
367
|
try {
|
|
346
|
-
accountQuota = await quotaManager.refreshFallback(account.id, account.access);
|
|
368
|
+
accountQuota = await quotaManager.refreshFallback(account.id, account.access, account);
|
|
347
369
|
}
|
|
348
370
|
catch { }
|
|
349
371
|
}
|
|
@@ -374,19 +396,26 @@ async function executeWithFallback(options) {
|
|
|
374
396
|
}));
|
|
375
397
|
const usableIds = new Set(usableFallbacks.map((account) => account.id));
|
|
376
398
|
const candidates = allRoutes.flatMap((route) => {
|
|
377
|
-
|
|
378
|
-
|
|
399
|
+
const quota = quotaSnapshotHasStandardWindows(route.quota)
|
|
400
|
+
? route.quota
|
|
401
|
+
: undefined;
|
|
379
402
|
const accountId = route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID ? undefined : route.id;
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
(
|
|
403
|
+
const quotaState = quota
|
|
404
|
+
? { kind: 'known', quota }
|
|
405
|
+
: { kind: 'unknown' };
|
|
406
|
+
const passesKillswitch = !isKillswitchEnabled(storage) ||
|
|
407
|
+
killswitchPassesPolicy(quotaState.kind === 'known' ? quotaState.quota : undefined, storage, accountId, modelId);
|
|
408
|
+
const passes = passesKillswitch &&
|
|
409
|
+
(quotaState.kind === 'unknown' ||
|
|
410
|
+
(quotaSnapshotPassesPolicy(quotaState.quota, storage) &&
|
|
411
|
+
quotaSnapshotPassesModelScope(quotaState.quota, modelId) &&
|
|
412
|
+
(route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID ||
|
|
413
|
+
usableIds.has(route.id))));
|
|
385
414
|
return passes
|
|
386
415
|
? [
|
|
387
416
|
{
|
|
388
417
|
accountId: route.id,
|
|
389
|
-
quota:
|
|
418
|
+
quota: quotaState,
|
|
390
419
|
order: route.order,
|
|
391
420
|
},
|
|
392
421
|
]
|
|
@@ -396,8 +425,8 @@ async function executeWithFallback(options) {
|
|
|
396
425
|
}
|
|
397
426
|
async function primaryQuotaRefreshConfirmsExhausted() {
|
|
398
427
|
try {
|
|
399
|
-
const quota = await quotaManager.refreshMain(options.primaryAccessToken);
|
|
400
|
-
const entry = quotaManager.getMain(
|
|
428
|
+
const quota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
|
|
429
|
+
const entry = quotaManager.getMain(mainAccountId);
|
|
401
430
|
return Boolean(entry &&
|
|
402
431
|
entry.refreshAfter > Date.now() &&
|
|
403
432
|
quotaSnapshotIsExhausted(quota));
|
|
@@ -408,8 +437,8 @@ async function executeWithFallback(options) {
|
|
|
408
437
|
}
|
|
409
438
|
async function primaryQuotaRefreshConfirmsModelScopeExhausted() {
|
|
410
439
|
try {
|
|
411
|
-
const quota = await quotaManager.refreshMain(options.primaryAccessToken);
|
|
412
|
-
const entry = quotaManager.getMain(
|
|
440
|
+
const quota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
|
|
441
|
+
const entry = quotaManager.getMain(mainAccountId);
|
|
413
442
|
return Boolean(entry &&
|
|
414
443
|
entry.refreshAfter > Date.now() &&
|
|
415
444
|
quotaSnapshotModelScopeIsExhausted(quota, options.model.id));
|
|
@@ -419,12 +448,12 @@ async function executeWithFallback(options) {
|
|
|
419
448
|
}
|
|
420
449
|
}
|
|
421
450
|
function primaryCachedModelScopeExhausted() {
|
|
422
|
-
const entry = quotaManager.getMain(
|
|
451
|
+
const entry = quotaManager.getMain(mainAccountId);
|
|
423
452
|
return Boolean(entry &&
|
|
424
453
|
quotaSnapshotModelScopeIsExhausted(entry.quota, options.model.id));
|
|
425
454
|
}
|
|
426
455
|
function primaryFreshModelScopeExhausted() {
|
|
427
|
-
const entry = quotaManager.getMain(
|
|
456
|
+
const entry = quotaManager.getMain(mainAccountId);
|
|
428
457
|
return Boolean(entry &&
|
|
429
458
|
!quotaManager.isMainStale(options.model.id) &&
|
|
430
459
|
quotaSnapshotModelScopeIsExhausted(entry.quota, options.model.id));
|
|
@@ -436,17 +465,38 @@ async function executeWithFallback(options) {
|
|
|
436
465
|
const usableOAuthById = new Map(usableOAuth.map((account) => [account.id, account]));
|
|
437
466
|
for (const configured of storage?.accounts ?? []) {
|
|
438
467
|
let response = null;
|
|
439
|
-
|
|
468
|
+
let account = isOAuthAccount(configured)
|
|
440
469
|
? usableOAuthById.get(configured.id)
|
|
441
470
|
: configured;
|
|
442
|
-
if (!account)
|
|
471
|
+
if (!account) {
|
|
472
|
+
if (!isOAuthAccount(configured) ||
|
|
473
|
+
!configured.access ||
|
|
474
|
+
(configured.expires !== undefined &&
|
|
475
|
+
configured.expires <= Date.now()) ||
|
|
476
|
+
isPermanentRefreshError(configured.lastRefreshError) ||
|
|
477
|
+
refreshBackoffActive(configured.lastRefreshError, configured.id, Date.now()))
|
|
478
|
+
continue;
|
|
479
|
+
account = configured;
|
|
480
|
+
const quota = quotaManager.getFallback(configured.id, configured)?.quota ??
|
|
481
|
+
configured.quota;
|
|
482
|
+
if (quotaSnapshotHasStandardWindows(quota))
|
|
483
|
+
continue;
|
|
484
|
+
if (isKillswitchEnabled(storage) &&
|
|
485
|
+
!killswitchPassesPolicy(quota, storage, configured.id, options.model.id))
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
else if (isOAuthAccount(account) &&
|
|
489
|
+
isKillswitchEnabled(storage) &&
|
|
490
|
+
!killswitchPassesPolicy(quotaManager.getFallback(account.id, account)?.quota ?? account.quota, storage, account.id, options.model.id)) {
|
|
443
491
|
continue;
|
|
492
|
+
}
|
|
444
493
|
if (isOAuthAccount(account)) {
|
|
445
494
|
if (routeOptions.apiOnly === true || !account.access)
|
|
446
495
|
continue;
|
|
447
496
|
response = await sendAnthropicRequest({
|
|
448
497
|
...options,
|
|
449
498
|
accessToken: account.access,
|
|
499
|
+
oauthAccountId: account.id,
|
|
450
500
|
});
|
|
451
501
|
}
|
|
452
502
|
else if (routeOptions.includeApiRoutes === true &&
|
|
@@ -504,6 +554,7 @@ async function executeWithFallback(options) {
|
|
|
504
554
|
if (!resolution) {
|
|
505
555
|
return createStickyNoRouteResponse({
|
|
506
556
|
mainRefreshError: storage?.refresh?.mainLastRefreshError,
|
|
557
|
+
fallbackReauthLabels: getFallbackReauthLabels(storage),
|
|
507
558
|
routeQuotas: routes.allRoutes.flatMap((route) => route.quota ? [route.quota] : []),
|
|
508
559
|
modelId: options.model.id,
|
|
509
560
|
});
|
|
@@ -586,8 +637,8 @@ async function executeWithFallback(options) {
|
|
|
586
637
|
try {
|
|
587
638
|
quota =
|
|
588
639
|
route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID
|
|
589
|
-
? await quotaManager.refreshMain(route.access)
|
|
590
|
-
: await quotaManager.refreshFallback(route.id, route.access);
|
|
640
|
+
? await quotaManager.refreshMain(mainAccountId, route.access)
|
|
641
|
+
: await quotaManager.refreshFallback(route.id, route.access, route.account);
|
|
591
642
|
}
|
|
592
643
|
catch {
|
|
593
644
|
// Retain affinity when the quota probe itself is unavailable.
|
|
@@ -679,9 +730,39 @@ async function executeWithFallback(options) {
|
|
|
679
730
|
if (fallback)
|
|
680
731
|
return fallback;
|
|
681
732
|
}
|
|
733
|
+
if (isKillswitchEnabled(storage)) {
|
|
734
|
+
let mainQuota = quotaManager.getMain(mainAccountId)?.quota;
|
|
735
|
+
if (!mainQuota || quotaManager.isMainStale(options.model.id)) {
|
|
736
|
+
try {
|
|
737
|
+
mainQuota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
|
|
738
|
+
}
|
|
739
|
+
catch { }
|
|
740
|
+
}
|
|
741
|
+
if (!killswitchPassesPolicy(mainQuota, storage, undefined, options.model.id)) {
|
|
742
|
+
if (!fallbackFirst) {
|
|
743
|
+
const fallback = await tryFallbackAccounts();
|
|
744
|
+
if (fallback)
|
|
745
|
+
return fallback;
|
|
746
|
+
}
|
|
747
|
+
return new Response(JSON.stringify({
|
|
748
|
+
type: 'error',
|
|
749
|
+
error: {
|
|
750
|
+
type: 'rate_limit_error',
|
|
751
|
+
message: 'Killswitch blocked all OAuth routes',
|
|
752
|
+
},
|
|
753
|
+
}), {
|
|
754
|
+
status: 429,
|
|
755
|
+
headers: {
|
|
756
|
+
'content-type': 'application/json',
|
|
757
|
+
'retry-after': '60',
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
682
762
|
const primary = await sendAnthropicRequest({
|
|
683
763
|
...options,
|
|
684
764
|
accessToken: options.primaryAccessToken,
|
|
765
|
+
oauthAccountId: STICKY_ROUTING_MAIN_ACCOUNT_ID,
|
|
685
766
|
});
|
|
686
767
|
const primaryPreflight = await firstStreamingError(primary);
|
|
687
768
|
if (primaryPreflight instanceof Response) {
|
|
@@ -757,6 +838,20 @@ export function streamCortexKitAnthropic(model, context, options) {
|
|
|
757
838
|
partial: output,
|
|
758
839
|
});
|
|
759
840
|
}
|
|
841
|
+
else if (block?.type === 'redacted_thinking') {
|
|
842
|
+
output.content.push({
|
|
843
|
+
type: 'thinking',
|
|
844
|
+
thinking: '[Reasoning redacted]',
|
|
845
|
+
thinkingSignature: String(block.data ?? ''),
|
|
846
|
+
redacted: true,
|
|
847
|
+
index: event.index,
|
|
848
|
+
});
|
|
849
|
+
stream.push({
|
|
850
|
+
type: 'thinking_start',
|
|
851
|
+
contentIndex: output.content.length - 1,
|
|
852
|
+
partial: output,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
760
855
|
else if (block?.type === 'tool_use') {
|
|
761
856
|
output.content.push({
|
|
762
857
|
type: 'toolCall',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/pi-anthropic-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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.21.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@earendil-works/pi-ai": "*",
|