@cortexkit/pi-anthropic-auth 1.20.0 → 1.22.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 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 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.
5
+ The Pi provider catalog includes Claude Fable 5 (`claude-fable-5`) and 5.1 (`claude-fable-5-1`), limited-access Claude Mythos 5 (`claude-mythos-5`) and 5.1 (`claude-mythos-5-1`), 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. OAuth Fable 5.1 sessions preserve Pi thinking-level changes as mid-conversation effort markers, so switching effort does not rewrite the cached prefix. Replayed signed/redacted thinking follows the configured prefix-mismatch behavior.
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`, `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.
45
+ The sidecar uses the same JSON shape as the OpenCode package, including `routing`, `claudeCache`, `cacheKeep`, `prime`, `claudeFast`, `thinkingBinding`, `dump`, `relay`, and fallback `accounts` blocks. `thinkingBinding.prefixMismatchBehavior` accepts `account-default` (default), `error`, or `drop_block` for OAuth Fable 5.1 replay. 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
 
@@ -77,10 +77,13 @@ The sidecar uses the same JSON shape as the OpenCode package, including `routing
77
77
  /claude-routing sticky-balanced
78
78
  /claude-routing reset
79
79
 
80
+ /claude-account
81
+ /claude-account reset-backoff
82
+
80
83
  /claude-quota
81
84
  ```
82
85
 
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`).
86
+ `/claude-account reset-backoff` clears the main OAuth refresh backoff and its matching quota backoff. Persisted refresh backoffs are bound to the refresh token that produced them, so replacing a rejected credential immediately escapes its stale latch. `/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`).
84
87
 
85
88
  ### Claude quota window priming
86
89
 
package/dist/commands.js CHANGED
@@ -1,4 +1,4 @@
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';
1
+ import { buildClaudeQuotaSummary, buildFallbackQuotaSummaries, buildPrimeAccountStatuses, CLAUDE_ACCOUNT_COMMAND_NAME, CLAUDE_CACHE_KEEP_COMMAND_NAME, CLAUDE_PRIME_COMMAND_NAME, CLAUDE_ROUTING_COMMAND_NAME, createEmptyStorage, detectClaustrumConnection, executeAccountCommand, executeCache1hCommand, executeCacheKeepCommand, executeDumpCommand, executeFastModeCommand, executeLoggingCommand, executePrimeCommand, executeRoutingCommand, getCache1hPersistentMode, getCacheKeepWindow, getPersistedLogLevel, getRoutingMode, isCache1hPersistentlyEnabled, isCacheKeepAlways, isCacheKeepHybridActive, isCacheKeepPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isPrimePersistentlyEnabled, loadAccounts, mergeMainQuotaErrorClearedAt, mergeMainRefreshErrorClearedAt, parseAccountCommandAction, parseCache1hCommandAction, parseCacheKeepCommandAction, parseDumpCommandAction, parseFastModeCommandAction, parseLoggingCommandAction, parseRoutingCommandAction, removeAccountPersistent, reorderAccountsPersistent, saveAccountState, 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
3
  import { clearPiStickyRoutingSession, getPiTrackedCacheKeepSessions, } from './stream.js';
4
4
  function notify(ctx, message, kind = 'info') {
@@ -152,9 +152,13 @@ export function registerCommands(pi) {
152
152
  handler: async (args, ctx) => {
153
153
  const path = getPiAccountStoragePath();
154
154
  const storage = await loadAccounts(path);
155
+ const action = parseAccountCommandAction(args ?? '');
155
156
  const result = executeAccountCommand({
156
157
  argumentsText: args ?? '',
157
158
  storage: storage ?? createEmptyStorage(),
159
+ claustrum: action.type === 'status'
160
+ ? await detectClaustrumConnection()
161
+ : undefined,
158
162
  });
159
163
  if (!result.updated) {
160
164
  notify(ctx, result.text);
@@ -181,6 +185,29 @@ export function registerCommands(pi) {
181
185
  await reorderAccountsPersistent(newOrder, path);
182
186
  }
183
187
  }
188
+ else if (mutationAction === 'reset-backoff') {
189
+ const nextStorage = storage ?? createEmptyStorage();
190
+ nextStorage.refresh = nextStorage.refresh ?? {};
191
+ nextStorage.refresh.mainLastRefreshError = undefined;
192
+ nextStorage.refresh.mainRefreshErrorClearedAt =
193
+ mergeMainRefreshErrorClearedAt(nextStorage.refresh.mainRefreshErrorClearedAt, Date.now());
194
+ const mainIdentity = nextStorage.mainAccountId;
195
+ const quotaError = nextStorage.quota?.mainLastQuotaApiError;
196
+ if (!quotaError?.accountIdentity ||
197
+ !mainIdentity ||
198
+ quotaError.accountIdentity === mainIdentity) {
199
+ nextStorage.quota = nextStorage.quota ?? {};
200
+ nextStorage.quota.mainLastQuotaApiError = undefined;
201
+ const nextQuotaErrorGeneration = (nextStorage.quota.mainQuotaErrorGeneration ?? 0) + 1;
202
+ nextStorage.quota.mainQuotaErrorGeneration = Math.max(nextStorage.quota.mainQuotaErrorGeneration ?? 0, nextQuotaErrorGeneration);
203
+ nextStorage.quota.mainQuotaErrorClearedAt =
204
+ mergeMainQuotaErrorClearedAt(nextStorage.quota.mainQuotaErrorClearedAt, Date.now());
205
+ }
206
+ await saveAccountState(nextStorage, path, {
207
+ mainRefresh: true,
208
+ mainQuota: true,
209
+ });
210
+ }
184
211
  notify(ctx, result.text);
185
212
  },
186
213
  });
package/dist/convert.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Cache1hMode, type ClaudeCodeIdentity } from '@cortexkit/anthropic-auth-core';
1
+ import { type Cache1hMode, type ClaudeCodeIdentity, type MidConversationEffortTransition, type ThinkingPrefixMismatchBehavior } from '@cortexkit/anthropic-auth-core';
2
2
  import type { Context, SimpleStreamOptions, Tool } from '@earendil-works/pi-ai';
3
3
  export type AnthropicRequestBody = {
4
4
  model: string;
@@ -26,7 +26,10 @@ export declare function fromClaudeCodeToolName(name: string, tools?: Tool[]): st
26
26
  export declare function buildAnthropicRequest(modelId: string, context: Context, options: SimpleStreamOptions | undefined, cache: {
27
27
  enabled: boolean;
28
28
  mode: Cache1hMode;
29
- }, fastModeEnabled?: boolean, identity?: ClaudeCodeIdentity): Promise<{
29
+ }, fastModeEnabled?: boolean, identity?: ClaudeCodeIdentity, controls?: {
30
+ effortTransitions?: readonly MidConversationEffortTransition[];
31
+ thinkingPrefixMismatchBehavior?: ThinkingPrefixMismatchBehavior;
32
+ }): Promise<{
30
33
  body: AnthropicRequestBody;
31
34
  bodyText: string;
32
35
  }>;
package/dist/convert.js CHANGED
@@ -1,8 +1,13 @@
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';
1
+ import { applyClaudeCodeMetadata, applyMidConversationOutputConfig, applyThinkingBindingControls, buildBillingHeaderValue, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING, CLAUDE_OPUS_5_ADAPTIVE_THINKING, CLAUDE_SONNET_5_ADAPTIVE_THINKING, ClaudeCodeFirstUserTextTracker, isClaudeFableOrMythos5Model, isClaudeOpus5Model, isClaudeSonnet5Model, isFastModeSupportedModel, isOpenAIReasoningSignature, orderClaudeCodeBody, signRequestBody, } from '@cortexkit/anthropic-auth-core';
2
2
  // Anchor identifying Pi's documentation paragraph — the only part of the prompt
3
3
  // that Anthropic currently rejects in system[]. Unknown prompt shapes take the
4
4
  // service-preserving fallback below instead of returning the full prompt there.
5
5
  const PI_DOCS_ANCHOR = 'Pi documentation';
6
+ const firstUserTextTracker = new ClaudeCodeFirstUserTextTracker();
7
+ const ANTHROPIC_REPLAY_APIS = new Set([
8
+ 'anthropic-messages',
9
+ 'cortexkit-anthropic-messages',
10
+ ]);
6
11
  const CLAUDE_CODE_TOOLS = new Map([
7
12
  'Read',
8
13
  'Write',
@@ -105,7 +110,7 @@ function convertTextAndImages(content) {
105
110
  }
106
111
  return blocks;
107
112
  }
108
- function convertMessages(messages) {
113
+ function convertMessages(messages, targetModelId) {
109
114
  const result = [];
110
115
  for (let index = 0; index < messages.length; index++) {
111
116
  const message = messages[index];
@@ -140,33 +145,63 @@ function convertMessages(messages) {
140
145
  if (block.type === 'text' && block.text.trim()) {
141
146
  blocks.push({ type: 'text', text: sanitize(block.text) });
142
147
  }
143
- else if (block.type === 'thinking' && block.thinking.trim()) {
148
+ else if (block.type === 'thinking') {
144
149
  const thinking = block;
145
- if (isOpenAIReasoningSignature(thinking.thinkingSignature)) {
146
- // OpenAI reasoningEncryptedContent is also opaque encrypted state,
147
- // but it is not an Anthropic thinking signature. Sending it as
148
- // `thinking.signature` makes Anthropic reject the request.
150
+ const signature = thinking.thinkingSignature;
151
+ const hasSignature = typeof signature === 'string' && signature.trim().length > 0;
152
+ const signatureBelongsToTarget = message.provider === 'anthropic' &&
153
+ ANTHROPIC_REPLAY_APIS.has(message.api) &&
154
+ message.model === targetModelId &&
155
+ !isOpenAIReasoningSignature(signature);
156
+ // Match Pi's built-in origin check: only replay signatures from the
157
+ // same Anthropic API/model. Preserve visible foreign reasoning as
158
+ // ordinary text, but drop opaque redacted/OpenAI encrypted state.
159
+ if (hasSignature && !signatureBelongsToTarget) {
160
+ if (!thinking.redacted &&
161
+ !isOpenAIReasoningSignature(signature) &&
162
+ thinking.thinking.trim()) {
163
+ blocks.push({ type: 'text', text: sanitize(thinking.thinking) });
164
+ }
149
165
  continue;
150
166
  }
151
- if (thinking.thinkingSignature &&
152
- !hasLoneSurrogate(thinking.thinking)) {
153
- // Signed thinking blocks must be sent back verbatim — the signature
154
- // is computed over the original text. Sanitizing would alter it and
155
- // Anthropic rejects the block as "modified". Anthropic-origin
156
- // thinking is valid UTF-8, so this is the normal path.
167
+ if (thinking.redacted) {
168
+ if (hasSignature) {
169
+ blocks.push({
170
+ type: 'redacted_thinking',
171
+ data: signature,
172
+ });
173
+ }
174
+ continue;
175
+ }
176
+ if (!hasSignature) {
177
+ // Anthropic rejects unsigned thinking blocks. Preserve readable
178
+ // output from interrupted streams as ordinary assistant text.
179
+ if (thinking.thinking.trim()) {
180
+ blocks.push({
181
+ type: 'text',
182
+ text: sanitize(thinking.thinking),
183
+ });
184
+ }
185
+ continue;
186
+ }
187
+ if (hasLoneSurrogate(thinking.thinking)) {
188
+ // Signature authenticates the thinking bytes exactly. Sanitizing the
189
+ // text would invalidate it, so downgrade malformed signed thinking
190
+ // to plain text and omit the signature instead.
191
+ if (thinking.thinking.trim()) {
192
+ blocks.push({
193
+ type: 'text',
194
+ text: sanitize(thinking.thinking),
195
+ });
196
+ }
197
+ }
198
+ else {
157
199
  blocks.push({
158
200
  type: 'thinking',
159
201
  thinking: thinking.thinking,
160
- signature: thinking.thinkingSignature,
202
+ signature,
161
203
  });
162
204
  }
163
- else {
164
- // Either unsigned, or signed-but-contains a lone surrogate. In the
165
- // latter case we cannot keep the signature: sanitizing breaks it and
166
- // sending the raw lone surrogate is an invalid-UTF8 400. Drop the
167
- // signature and downgrade to sanitized text.
168
- blocks.push({ type: 'text', text: sanitize(thinking.thinking) });
169
- }
170
205
  }
171
206
  else if (block.type === 'toolCall') {
172
207
  blocks.push({
@@ -317,8 +352,8 @@ function applyCacheMode(body, enabled, mode) {
317
352
  body.cache_control = { type: 'ephemeral' };
318
353
  addTtl(body);
319
354
  }
320
- export async function buildAnthropicRequest(modelId, context, options, cache, fastModeEnabled = false, identity) {
321
- const messages = convertMessages(context.messages);
355
+ export async function buildAnthropicRequest(modelId, context, options, cache, fastModeEnabled = false, identity, controls = {}) {
356
+ const messages = convertMessages(context.messages, modelId);
322
357
  // Strip trailing assistant messages — Anthropic rejects prefill on some models
323
358
  while (messages.length &&
324
359
  messages[messages.length - 1]?.role === 'assistant') {
@@ -327,7 +362,9 @@ export async function buildAnthropicRequest(modelId, context, options, cache, fa
327
362
  const system = [
328
363
  {
329
364
  type: 'text',
330
- text: buildBillingHeaderValue(messages, undefined, CLAUDE_CODE_ENTRYPOINT),
365
+ text: buildBillingHeaderValue(messages, undefined, CLAUDE_CODE_ENTRYPOINT, options?.sessionId
366
+ ? firstUserTextTracker.resolve(options.sessionId, messages)
367
+ : undefined),
331
368
  },
332
369
  { type: 'text', text: CLAUDE_CODE_IDENTITY },
333
370
  ];
@@ -414,10 +451,13 @@ export async function buildAnthropicRequest(modelId, context, options, cache, fa
414
451
  };
415
452
  }
416
453
  }
454
+ applyMidConversationOutputConfig(body, controls.effortTransitions ?? []);
417
455
  addEphemeralCacheControl(body);
418
456
  applyCacheMode(body, cache.enabled, cache.mode);
419
- if (identity)
457
+ if (identity) {
458
+ applyThinkingBindingControls(body, controls.thinkingPrefixMismatchBehavior ?? 'account-default');
420
459
  applyClaudeCodeMetadata(body, identity);
460
+ }
421
461
  const unsigned = JSON.stringify(orderClaudeCodeBody(body));
422
462
  const bodyText = await signRequestBody(unsigned);
423
463
  return { body, bodyText };
@@ -0,0 +1,12 @@
1
+ import { type MidConversationEffortTransition } from '@cortexkit/anthropic-auth-core';
2
+ type SessionEntryLike = {
3
+ id?: unknown;
4
+ type?: unknown;
5
+ thinkingLevel?: unknown;
6
+ message?: {
7
+ role?: unknown;
8
+ };
9
+ };
10
+ /** Build Pi's active, compaction-aware Fable 5.1 effort timeline. */
11
+ export declare function collectPiEffortHistory(contextEntries: readonly SessionEntryLike[], fullBranch: readonly SessionEntryLike[]): MidConversationEffortTransition[];
12
+ export {};
@@ -0,0 +1,63 @@
1
+ import { normalizeAdaptiveEffort, } from '@cortexkit/anthropic-auth-core';
2
+ function entryRole(entry) {
3
+ if (entry.type === 'compaction' || entry.type === 'branch_summary') {
4
+ return 'user';
5
+ }
6
+ if (entry.type === 'custom_message')
7
+ return 'user';
8
+ return entry.type === 'message' ? entry.message?.role : undefined;
9
+ }
10
+ function appendTransition(transitions, afterAssistantMessages, effort) {
11
+ const previous = transitions.at(-1);
12
+ if (previous?.effort === effort)
13
+ return;
14
+ if (previous?.afterAssistantMessages === afterAssistantMessages) {
15
+ previous.effort = effort;
16
+ return;
17
+ }
18
+ transitions.push({ afterAssistantMessages, effort });
19
+ }
20
+ /** Build Pi's active, compaction-aware Fable 5.1 effort timeline. */
21
+ export function collectPiEffortHistory(contextEntries, fullBranch) {
22
+ let activeEffort = 'high';
23
+ let assistantMessages = 0;
24
+ const transitions = [];
25
+ const first = contextEntries[0];
26
+ const compactionIndex = first?.type === 'compaction'
27
+ ? fullBranch.findIndex((entry) => entry.id === first.id)
28
+ : -1;
29
+ if (compactionIndex >= 0) {
30
+ for (let index = 0; index <= compactionIndex; index++) {
31
+ const entry = fullBranch[index];
32
+ if (entry?.type !== 'thinking_level_change')
33
+ continue;
34
+ activeEffort = normalizeAdaptiveEffort(entry.thinkingLevel) ?? 'high';
35
+ }
36
+ appendTransition(transitions, 0, activeEffort);
37
+ }
38
+ const branchIndexById = new Map(fullBranch.map((entry, index) => [entry.id, index]));
39
+ for (const entry of contextEntries) {
40
+ if (entry === first && entry.type === 'compaction')
41
+ continue;
42
+ const branchIndex = branchIndexById.get(entry.id);
43
+ const retainedBeforeCompaction = compactionIndex >= 0 &&
44
+ typeof branchIndex === 'number' &&
45
+ branchIndex < compactionIndex;
46
+ if (entry.type === 'thinking_level_change') {
47
+ if (!retainedBeforeCompaction) {
48
+ activeEffort = normalizeAdaptiveEffort(entry.thinkingLevel) ?? 'high';
49
+ }
50
+ continue;
51
+ }
52
+ const role = entryRole(entry);
53
+ if (role === 'assistant') {
54
+ assistantMessages++;
55
+ }
56
+ else if (role === 'user' ||
57
+ role === 'toolResult' ||
58
+ role === 'tool_result') {
59
+ appendTransition(transitions, assistantMessages, activeEffort);
60
+ }
61
+ }
62
+ return transitions;
63
+ }
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
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';
1
+ import { authorize, CLAUDE_FABLE_MYTHOS_5_1_PRICING, 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, isClaudeFableOrMythos51Model, refreshClaudeOAuthToken, } from '@cortexkit/anthropic-auth-core';
2
2
  import { registerCommands } from './commands.js';
3
+ import { collectPiEffortHistory } from './effort-history.js';
3
4
  import { streamCortexKitAnthropic } from './stream.js';
4
5
  async function loginAnthropic(callbacks) {
5
6
  const auth = await authorize('max');
@@ -32,25 +33,51 @@ async function refreshAnthropicToken(credentials) {
32
33
  }
33
34
  export default function cortexKitPiAnthropicAuth(pi) {
34
35
  registerCommands(pi);
36
+ const effortHistoryBySession = new Map();
37
+ pi.on('turn_start', async (_event, ctx) => {
38
+ const sessionId = ctx.sessionManager.getSessionId();
39
+ if (!sessionId)
40
+ return;
41
+ const transitions = collectPiEffortHistory(ctx.sessionManager.buildContextEntries(), ctx.sessionManager.getBranch());
42
+ effortHistoryBySession.delete(sessionId);
43
+ effortHistoryBySession.set(sessionId, transitions);
44
+ while (effortHistoryBySession.size > 128) {
45
+ const oldest = effortHistoryBySession.keys().next().value;
46
+ if (oldest)
47
+ effortHistoryBySession.delete(oldest);
48
+ else
49
+ break;
50
+ }
51
+ });
52
+ pi.on('session_shutdown', async (_event, ctx) => {
53
+ const sessionId = ctx.sessionManager.getSessionId();
54
+ if (sessionId)
55
+ effortHistoryBySession.delete(sessionId);
56
+ });
35
57
  pi.registerProvider('anthropic', {
36
58
  name: 'Anthropic (CortexKit OAuth)',
37
59
  baseUrl: 'https://api.anthropic.com',
38
60
  api: 'cortexkit-anthropic-messages',
39
61
  models: [
40
- ...Object.values(CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS).map((model) => ({
41
- id: model.id,
42
- name: model.name,
43
- reasoning: true,
44
- input: textImageInput(),
45
- cost: {
46
- input: CLAUDE_FABLE_MYTHOS_5_PRICING.input,
47
- output: CLAUDE_FABLE_MYTHOS_5_PRICING.output,
48
- cacheRead: CLAUDE_FABLE_MYTHOS_5_PRICING.cacheRead,
49
- cacheWrite: CLAUDE_FABLE_MYTHOS_5_PRICING.cacheWrite5m,
50
- },
51
- contextWindow: CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW,
52
- maxTokens: CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS,
53
- })),
62
+ ...Object.values(CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS).map((model) => {
63
+ const pricing = isClaudeFableOrMythos51Model(model.id)
64
+ ? CLAUDE_FABLE_MYTHOS_5_1_PRICING
65
+ : CLAUDE_FABLE_MYTHOS_5_PRICING;
66
+ return {
67
+ id: model.id,
68
+ name: model.name,
69
+ reasoning: true,
70
+ input: textImageInput(),
71
+ cost: {
72
+ input: pricing.input,
73
+ output: pricing.output,
74
+ cacheRead: pricing.cacheRead,
75
+ cacheWrite: pricing.cacheWrite5m,
76
+ },
77
+ contextWindow: CLAUDE_FABLE_MYTHOS_5_CONTEXT_WINDOW,
78
+ maxTokens: CLAUDE_FABLE_MYTHOS_5_MAX_OUTPUT_TOKENS,
79
+ };
80
+ }),
54
81
  {
55
82
  id: 'claude-opus-5',
56
83
  name: 'Claude Opus 5',
@@ -103,6 +130,8 @@ export default function cortexKitPiAnthropicAuth(pi) {
103
130
  refreshToken: refreshAnthropicToken,
104
131
  getApiKey: (credentials) => credentials.access,
105
132
  },
106
- streamSimple: streamCortexKitAnthropic,
133
+ streamSimple: (model, context, options) => streamCortexKitAnthropic(model, context, options, options?.sessionId
134
+ ? effortHistoryBySession.get(options.sessionId)
135
+ : undefined),
107
136
  });
108
137
  }
package/dist/stream.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ApiKeyAccount } from '@cortexkit/anthropic-auth-core';
1
+ import { type ApiKeyAccount, 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
3
  export declare function clearPiStickyRoutingSession(storagePath: string, sessionId: string): Promise<void>;
4
4
  export declare function getPiTrackedCacheKeepSessions(): Promise<import("@cortexkit/anthropic-auth-core").CacheKeepTrackedSession[]>;
@@ -16,5 +16,5 @@ export declare function buildExplicitBaseMessagesUrl(baseURL: string): URL;
16
16
  export declare function configureApiRouteHeaders(account: ApiKeyAccount, fastMode: boolean): Headers;
17
17
  export declare function parseSse(response: Response): AsyncGenerator<AnthropicEvent>;
18
18
  export declare function primaryResponseAllowsApiFallback(preflight: Response | string): boolean;
19
- export declare function streamCortexKitAnthropic(model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream;
19
+ export declare function streamCortexKitAnthropic(model: Model<Api>, context: Context, options?: SimpleStreamOptions, effortTransitions?: readonly MidConversationEffortTransition[]): AssistantMessageEventStream;
20
20
  export {};
package/dist/stream.js CHANGED
@@ -1,4 +1,4 @@
1
- import { applyClaudeCodeHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, createStickyNoRouteResponse, decideStickyQuotaFailure, dumpDirectRequest, FAST_MODE_BETA, FallbackAccountManager, getCache1hPersistentMode, getDefaultCacheKeepRegistryDirectory, getFallbackReauthLabels, 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, 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';
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';
@@ -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,12 +85,21 @@ const cacheKeepManager = new CacheKeepManager({
71
85
  return headers;
72
86
  try {
73
87
  const body = JSON.parse(target.bodyText);
74
- const identity = await resolveClaudeCodeIdentity(accessToken, typeof body.model === 'string' ? body.model : undefined);
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
- applyClaudeCodeHeaders(headers, accessToken, { body, identity });
77
- headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [
78
- CACHE_KEEP_EXTENDED_TTL_BETA,
79
- ]));
93
+ applyClaudeCodeHeaders(headers, accessToken, {
94
+ body,
95
+ identity,
96
+ extraBetas: [
97
+ CACHE_KEEP_EXTENDED_TTL_BETA,
98
+ ...(hasThinkingBindingControls(body)
99
+ ? [THINKING_BINDING_CONTROLS_BETA]
100
+ : []),
101
+ ],
102
+ });
80
103
  if (body.speed === 'fast') {
81
104
  headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
82
105
  }
@@ -201,21 +224,38 @@ export async function* parseSse(response) {
201
224
  }
202
225
  }
203
226
  async function sendAnthropicRequest(options) {
227
+ await ensurePiMainAccountId(options.storagePath);
204
228
  const storage = await loadAccounts(options.storagePath);
205
229
  setDumpEnabled(isDumpPersistentlyEnabled(storage));
230
+ const accountIdentity = options.oauthAccountId === STICKY_ROUTING_MAIN_ACCOUNT_ID
231
+ ? storage?.mainAccountId
232
+ : options.oauthAccountId;
206
233
  const identity = options.accessToken
207
- ? await resolveClaudeCodeIdentity(options.accessToken, options.model.id)
234
+ ? await resolveClaudeCodeIdentity(options.accessToken, options.model.id, accountIdentity)
208
235
  : undefined;
209
236
  const { body, bodyText } = await buildAnthropicRequest(options.model.id, options.context, options.streamOptions, {
210
237
  enabled: isCache1hPersistentlyEnabled(storage),
211
238
  mode: getCache1hPersistentMode(storage),
212
- }, isFastModePersistentlyEnabled(storage), identity);
239
+ }, isFastModePersistentlyEnabled(storage), identity, {
240
+ effortTransitions: options.apiAccount ? [] : options.effortTransitions,
241
+ thinkingPrefixMismatchBehavior: options.apiAccount
242
+ ? 'account-default'
243
+ : getThinkingPrefixMismatchBehavior(storage),
244
+ });
213
245
  const fastMode = body.speed === 'fast';
214
246
  const headers = options.apiAccount
215
247
  ? configureApiRouteHeaders(options.apiAccount, fastMode)
216
248
  : applyClaudeCodeHeaders(new Headers(), options.accessToken ?? '', {
217
249
  body,
218
250
  identity,
251
+ extraBetas: [
252
+ ...(hasThinkingBindingControls(body)
253
+ ? [THINKING_BINDING_CONTROLS_BETA]
254
+ : []),
255
+ ...(usesMidConversationOutputConfig(body)
256
+ ? [MID_CONVERSATION_OUTPUT_CONFIG_BETA]
257
+ : []),
258
+ ],
219
259
  });
220
260
  if (!options.apiAccount && fastMode) {
221
261
  headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
@@ -307,16 +347,18 @@ async function firstStreamingError(response) {
307
347
  return response;
308
348
  }
309
349
  async function executeWithFallback(options) {
350
+ await ensurePiMainAccountId(options.storagePath);
310
351
  const storage = await loadAccounts(options.storagePath);
311
352
  const { quotaManager, fallbackManager: manager } = getPiRoutingServices(options.storagePath, storage);
312
- quotaManager.seedMainFromStorage(storage, options.primaryAccessToken);
353
+ const mainAccountId = storage?.mainAccountId;
354
+ quotaManager.seedMainFromStorage(storage, mainAccountId);
313
355
  quotaManager.seedFallbacksFromAccounts((storage?.accounts ?? []).filter(isOAuthAccount));
314
356
  async function buildStickyRoutes(modelId) {
315
- const mainEntry = quotaManager.getMain(options.primaryAccessToken);
357
+ const mainEntry = quotaManager.getMain(mainAccountId);
316
358
  let mainQuota = mainEntry?.quota;
317
359
  if (!stickyQuotaSnapshotIsFresh(mainEntry?.quota, storage, Date.now(), modelId)) {
318
360
  try {
319
- mainQuota = await quotaManager.refreshMain(options.primaryAccessToken);
361
+ mainQuota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
320
362
  }
321
363
  catch { }
322
364
  }
@@ -339,11 +381,10 @@ async function executeWithFallback(options) {
339
381
  const account = usableById.get(configured.id) ?? configured;
340
382
  if (!account.access || isPermanentRefreshError(account.lastRefreshError))
341
383
  continue;
342
- let accountQuota = quotaManager.getFallback(account.id, account.access)?.quota ??
343
- account.quota;
384
+ let accountQuota = quotaManager.getFallback(account.id, account)?.quota ?? account.quota;
344
385
  if (!stickyQuotaSnapshotIsFresh(accountQuota, storage, Date.now(), modelId)) {
345
386
  try {
346
- accountQuota = await quotaManager.refreshFallback(account.id, account.access);
387
+ accountQuota = await quotaManager.refreshFallback(account.id, account.access, account);
347
388
  }
348
389
  catch { }
349
390
  }
@@ -374,19 +415,26 @@ async function executeWithFallback(options) {
374
415
  }));
375
416
  const usableIds = new Set(usableFallbacks.map((account) => account.id));
376
417
  const candidates = allRoutes.flatMap((route) => {
377
- if (!route.quota)
378
- return [];
418
+ const quota = quotaSnapshotHasStandardWindows(route.quota)
419
+ ? route.quota
420
+ : undefined;
379
421
  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));
422
+ const quotaState = quota
423
+ ? { kind: 'known', quota }
424
+ : { kind: 'unknown' };
425
+ const passesKillswitch = !isKillswitchEnabled(storage) ||
426
+ killswitchPassesPolicy(quotaState.kind === 'known' ? quotaState.quota : undefined, storage, accountId, modelId);
427
+ const passes = passesKillswitch &&
428
+ (quotaState.kind === 'unknown' ||
429
+ (quotaSnapshotPassesPolicy(quotaState.quota, storage) &&
430
+ quotaSnapshotPassesModelScope(quotaState.quota, modelId) &&
431
+ (route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID ||
432
+ usableIds.has(route.id))));
385
433
  return passes
386
434
  ? [
387
435
  {
388
436
  accountId: route.id,
389
- quota: route.quota,
437
+ quota: quotaState,
390
438
  order: route.order,
391
439
  },
392
440
  ]
@@ -396,8 +444,8 @@ async function executeWithFallback(options) {
396
444
  }
397
445
  async function primaryQuotaRefreshConfirmsExhausted() {
398
446
  try {
399
- const quota = await quotaManager.refreshMain(options.primaryAccessToken);
400
- const entry = quotaManager.getMain(options.primaryAccessToken);
447
+ const quota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
448
+ const entry = quotaManager.getMain(mainAccountId);
401
449
  return Boolean(entry &&
402
450
  entry.refreshAfter > Date.now() &&
403
451
  quotaSnapshotIsExhausted(quota));
@@ -408,8 +456,8 @@ async function executeWithFallback(options) {
408
456
  }
409
457
  async function primaryQuotaRefreshConfirmsModelScopeExhausted() {
410
458
  try {
411
- const quota = await quotaManager.refreshMain(options.primaryAccessToken);
412
- const entry = quotaManager.getMain(options.primaryAccessToken);
459
+ const quota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
460
+ const entry = quotaManager.getMain(mainAccountId);
413
461
  return Boolean(entry &&
414
462
  entry.refreshAfter > Date.now() &&
415
463
  quotaSnapshotModelScopeIsExhausted(quota, options.model.id));
@@ -419,12 +467,12 @@ async function executeWithFallback(options) {
419
467
  }
420
468
  }
421
469
  function primaryCachedModelScopeExhausted() {
422
- const entry = quotaManager.getMain(options.primaryAccessToken);
470
+ const entry = quotaManager.getMain(mainAccountId);
423
471
  return Boolean(entry &&
424
472
  quotaSnapshotModelScopeIsExhausted(entry.quota, options.model.id));
425
473
  }
426
474
  function primaryFreshModelScopeExhausted() {
427
- const entry = quotaManager.getMain(options.primaryAccessToken);
475
+ const entry = quotaManager.getMain(mainAccountId);
428
476
  return Boolean(entry &&
429
477
  !quotaManager.isMainStale(options.model.id) &&
430
478
  quotaSnapshotModelScopeIsExhausted(entry.quota, options.model.id));
@@ -436,17 +484,38 @@ async function executeWithFallback(options) {
436
484
  const usableOAuthById = new Map(usableOAuth.map((account) => [account.id, account]));
437
485
  for (const configured of storage?.accounts ?? []) {
438
486
  let response = null;
439
- const account = isOAuthAccount(configured)
487
+ let account = isOAuthAccount(configured)
440
488
  ? usableOAuthById.get(configured.id)
441
489
  : configured;
442
- if (!account)
490
+ if (!account) {
491
+ if (!isOAuthAccount(configured) ||
492
+ !configured.access ||
493
+ (configured.expires !== undefined &&
494
+ configured.expires <= Date.now()) ||
495
+ isPermanentRefreshError(configured.lastRefreshError) ||
496
+ refreshBackoffActive(configured.lastRefreshError, configured.id, Date.now(), tokenFingerprint(configured.refresh)))
497
+ continue;
498
+ account = configured;
499
+ const quota = quotaManager.getFallback(configured.id, configured)?.quota ??
500
+ configured.quota;
501
+ if (quotaSnapshotHasStandardWindows(quota))
502
+ continue;
503
+ if (isKillswitchEnabled(storage) &&
504
+ !killswitchPassesPolicy(quota, storage, configured.id, options.model.id))
505
+ continue;
506
+ }
507
+ else if (isOAuthAccount(account) &&
508
+ isKillswitchEnabled(storage) &&
509
+ !killswitchPassesPolicy(quotaManager.getFallback(account.id, account)?.quota ?? account.quota, storage, account.id, options.model.id)) {
443
510
  continue;
511
+ }
444
512
  if (isOAuthAccount(account)) {
445
513
  if (routeOptions.apiOnly === true || !account.access)
446
514
  continue;
447
515
  response = await sendAnthropicRequest({
448
516
  ...options,
449
517
  accessToken: account.access,
518
+ oauthAccountId: account.id,
450
519
  });
451
520
  }
452
521
  else if (routeOptions.includeApiRoutes === true &&
@@ -587,8 +656,8 @@ async function executeWithFallback(options) {
587
656
  try {
588
657
  quota =
589
658
  route.id === STICKY_ROUTING_MAIN_ACCOUNT_ID
590
- ? await quotaManager.refreshMain(route.access)
591
- : await quotaManager.refreshFallback(route.id, route.access);
659
+ ? await quotaManager.refreshMain(mainAccountId, route.access)
660
+ : await quotaManager.refreshFallback(route.id, route.access, route.account);
592
661
  }
593
662
  catch {
594
663
  // Retain affinity when the quota probe itself is unavailable.
@@ -680,9 +749,39 @@ async function executeWithFallback(options) {
680
749
  if (fallback)
681
750
  return fallback;
682
751
  }
752
+ if (isKillswitchEnabled(storage)) {
753
+ let mainQuota = quotaManager.getMain(mainAccountId)?.quota;
754
+ if (!mainQuota || quotaManager.isMainStale(options.model.id)) {
755
+ try {
756
+ mainQuota = await quotaManager.refreshMain(mainAccountId, options.primaryAccessToken);
757
+ }
758
+ catch { }
759
+ }
760
+ if (!killswitchPassesPolicy(mainQuota, storage, undefined, options.model.id)) {
761
+ if (!fallbackFirst) {
762
+ const fallback = await tryFallbackAccounts();
763
+ if (fallback)
764
+ return fallback;
765
+ }
766
+ return new Response(JSON.stringify({
767
+ type: 'error',
768
+ error: {
769
+ type: 'rate_limit_error',
770
+ message: 'Killswitch blocked all OAuth routes',
771
+ },
772
+ }), {
773
+ status: 429,
774
+ headers: {
775
+ 'content-type': 'application/json',
776
+ 'retry-after': '60',
777
+ },
778
+ });
779
+ }
780
+ }
683
781
  const primary = await sendAnthropicRequest({
684
782
  ...options,
685
783
  accessToken: options.primaryAccessToken,
784
+ oauthAccountId: STICKY_ROUTING_MAIN_ACCOUNT_ID,
686
785
  });
687
786
  const primaryPreflight = await firstStreamingError(primary);
688
787
  if (primaryPreflight instanceof Response) {
@@ -706,7 +805,7 @@ async function executeWithFallback(options) {
706
805
  }
707
806
  return primaryPreflight instanceof Response ? primaryPreflight : primary;
708
807
  }
709
- export function streamCortexKitAnthropic(model, context, options) {
808
+ export function streamCortexKitAnthropic(model, context, options, effortTransitions) {
710
809
  const stream = createAssistantMessageEventStream();
711
810
  void (async () => {
712
811
  const output = createOutput(model);
@@ -722,6 +821,7 @@ export function streamCortexKitAnthropic(model, context, options) {
722
821
  streamOptions: options,
723
822
  primaryAccessToken: accessToken,
724
823
  storagePath,
824
+ effortTransitions,
725
825
  });
726
826
  if (!response.ok) {
727
827
  throw new Error(`Anthropic request failed: HTTP ${response.status} ${await response.text()}`);
@@ -758,6 +858,20 @@ export function streamCortexKitAnthropic(model, context, options) {
758
858
  partial: output,
759
859
  });
760
860
  }
861
+ else if (block?.type === 'redacted_thinking') {
862
+ output.content.push({
863
+ type: 'thinking',
864
+ thinking: '[Reasoning redacted]',
865
+ thinkingSignature: String(block.data ?? ''),
866
+ redacted: true,
867
+ index: event.index,
868
+ });
869
+ stream.push({
870
+ type: 'thinking_start',
871
+ contentIndex: output.content.length - 1,
872
+ partial: output,
873
+ });
874
+ }
761
875
  else if (block?.type === 'tool_use') {
762
876
  output.content.push({
763
877
  type: 'toolCall',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/pi-anthropic-auth",
3
- "version": "1.20.0",
3
+ "version": "1.22.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.20.0"
39
+ "@cortexkit/anthropic-auth-core": "1.22.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@earendil-works/pi-ai": "*",