@cortexkit/pi-anthropic-auth 2.0.2 → 2.1.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
@@ -1,116 +1,54 @@
1
1
  # @cortexkit/pi-anthropic-auth
2
2
 
3
- Pi package for CortexKit Anthropic OAuth support. It replaces the implementation of Pi's built-in Anthropic provider, keeping the `anthropic` provider ID and model IDs, and is backed by the shared `@cortexkit/anthropic-auth-core` package.
3
+ Use Claude Pro/Max subscriptions in [Pi](https://pi.dev) with `@cortexkit/pi-anthropic-auth`. The companion `@cortexkit/opencode-anthropic-auth` package supports [OpenCode](https://opencode.ai) 1 and 2; both integrations use the shared `@cortexkit/anthropic-auth-core` package.
4
4
 
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 5.5 (`claude-opus-5-5`), Claude Haiku 5.5 (`claude-haiku-5-5`), Claude Opus 4.8, Claude Opus 4.5, Claude Sonnet 4.5, Claude Sonnet 5 (`claude-sonnet-5`), and Claude Sonnet 5.5 (`claude-sonnet-5-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. Opus 5.5 requires adaptive thinking, while Sonnet 5.5 also supports a `between_tools` mode for applications that explicitly turn off up-front thinking. This Pi provider sends adaptive thinking with summarized display for both models. For Sonnet 5.5 it exposes `low` through `max` effort, but not `minimal` (unsupported by the model) or `off` (Pi's simple-provider options cannot request `between_tools`). OAuth Fable 5.1 sessions add effort changes as system messages instead of rewriting earlier history, preserving cached prompt content.
5
+ ## Why use it
6
6
 
7
- Replayed thinking signatures refer to the messages, instructions and tools that preceded the thinking block. If those earlier inputs change, `thinkingBinding.prefixMismatchBehavior` controls how OAuth Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5 requests handle that mismatch: `account-default` sends no local override and uses Anthropic's account settings; `error` rejects the request; `drop_block` removes the affected thinking blocks. These controls require adaptive thinking.
7
+ - Multiple Claude accounts with ordered fallbacks, quota-aware routing and per-session account affinity.
8
+ - Prompt-cache controls and optional cache keepalive for active sessions.
9
+ - Optional Claustrum integration: Claustrum is a local credential-vault service that supplies Claude accounts instead of each application's local account pool.
10
+ - An optional user-owned Cloudflare relay to reduce repeated request uploads.
8
11
 
9
- Haiku 5.5 has a 1M context, 128K output and summarized adaptive thinking with `low` through `max` effort. Haiku 5.5 does not support `minimal` effort. The model accepts disabled thinking, but Pi's simple-provider options cannot request it, so Pi does not expose an `off` level. Standard input/output rates are $0.10/$0.50 per million tokens; all rates increase fivefold above 100K total input including cache. Pi's native pricing tiers apply the selected rates to the whole request, including both cache lifetimes.
12
+ ## Disclaimer
10
13
 
11
- 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.
12
-
13
- > [!IMPORTANT]
14
- > These instructions describe the upcoming native release, which is not yet published.
15
-
16
- ## Setup (recommended)
17
-
18
- Setup is an *offline* step: every OpenCode, LiteCode, and Pi process must be closed while it runs. Offline does not mean without network; setup still uses the network, for example to install packages and to sign in to Claude. Quit those processes, then run the setup command from the OpenCode package:
19
-
20
- ```bash
21
- bunx @cortexkit/opencode-anthropic-auth setup
22
- ```
23
-
24
- What setup does: it installs this extension with `pi install` and migrates Pi to its local account pool, or, if you choose it, enrolls Pi in the Claustrum vault (Claustrum is a local credential-vault service; `ck` is its command-line tool). Useful flags: `--yes` (no prompts), `--dry-run`, `--login` (sign in to Claude during setup), `--claustrum` / `--no-claustrum`, and `--remove-pi-auth` (consent, with `--yes`, to moving Pi's stored Anthropic login). `--login` and `--claustrum` cannot be combined.
25
-
26
- ---
14
+ This project is not affiliated with or endorsed by Anthropic. Using Claude subscriptions outside Anthropic's official clients may violate Anthropic's terms and can lead to account restrictions or suspension. Use it at your own risk.
27
15
 
28
16
  ## Install
29
17
 
30
- Install with Pi's package manager:
31
-
32
- ```bash
33
- pi install npm:@cortexkit/pi-anthropic-auth
34
- ```
35
-
36
- To pin a release, replace `VERSION` with its version number and keep the quotes: `pi install "npm:@cortexkit/pi-anthropic-auth@VERSION"`.
37
-
38
- To try it for one run without changing Pi settings:
39
-
40
18
  ```bash
41
- pi -e npm:@cortexkit/pi-anthropic-auth
19
+ npx @cortexkit/opencode-anthropic-auth setup
42
20
  ```
43
21
 
44
- The extension keeps Pi's `anthropic` provider ID and the model IDs listed above. It authenticates from the local account pool or the Claustrum vault, not from Pi's `/login anthropic` credential. Run setup before starting Pi.
45
-
46
- ## Accounts and custody
47
-
48
- Pi takes Claude credentials from one source, selected by Pi's own Claustrum enrollment record (`~/.local/state/cortexkit/anthropic-auth/pi-enrollment.json`, or `PI_ANTHROPIC_AUTH_CLAUSTRUM_ENROLLMENT_FILE`):
49
-
50
- - **Local custody** (default): the local account pool at `~/.pi/agent/anthropic-auth-pool.json`, with private credential state in `anthropic-auth-pool-state.json`. The pool holds Pi's Claude OAuth accounts (one main account, tried first by default, plus fallbacks) and API-key fallback routes. Its files are created next to the old sidecar path, so `PI_ANTHROPIC_AUTH_FILE` moves them too. Every Pi process shares this pool; OpenCode keeps a separate one.
51
- - **Vault custody**: the Claustrum vault, through the `anthropic-auth-pi` enrollment and read access to the `anthropic-native` credential category. Setup grants this with `ck auth grant --principal enrolled:anthropic-auth-pi --selector-kind category --selector anthropic-native --operation read`.
52
-
53
- If the enrollment record cannot be read, Pi refuses the requests this extension would serve until the record is repaired or Pi is disconnected. An approved enrollment keeps Pi on the vault even if its token file is missing, so Pi refuses rather than falling back to local accounts. See [the custody contract](https://github.com/cortexkit/anthropic-auth/blob/main/docs/custody-state-machine.md) for details.
54
-
55
- ### Local custody
56
-
57
- Setup imports your fallback Claude OAuth accounts, paid API-key fallback routes, and settings from the old sidecar file (`~/.pi/agent/anthropic-auth.json`, or `PI_ANTHROPIC_AUTH_FILE`) into the pool; the old file is not read afterwards. Pi normally uses its stored login instead of the extension's credentials, so with your consent setup also moves Pi's stored Anthropic OAuth login into the pool as the main account. Until then, Pi refuses to serve with `Pi still has a host Anthropic OAuth credential. Run offline setup to migrate it and approve its removal before using the native pool.`
22
+ After setup:
58
23
 
59
- Before importing a stored main OAuth login, migration checks the Claude account UUID in the legacy main profile. That profile's recorded token fingerprint (a hash) must match the hash of the host login's current access token; migration does not query Anthropic for identity. If it cannot, it changes nothing and asks you to run `bunx @cortexkit/opencode-anthropic-auth setup --login --no-claustrum`. After setup completes, you can add or replace logins from Pi: open `/claude` and use **Start OAuth sign-in** and **Finish OAuth sign-in**. The first account becomes the main account; later sign-ins add fallback accounts.
24
+ OpenCode 1: open `/claude` → **Accounts** to sign in.
60
25
 
61
- ### Vault custody
26
+ OpenCode 2: In OpenCode, sign in to Anthropic with "Claude Pro / Max (native account pool)".
62
27
 
63
- Run `bunx @cortexkit/opencode-anthropic-auth setup --claustrum` with the Claustrum daemon running and `ck` installed. Setup writes Pi's enrollment and, with your consent, Pi's auth file. It does not read, import, or change the local pool, its metadata, or routing assignments, and it never copies local OAuth accounts into the vault. If Pi has a stored Anthropic login (OAuth or API key), setup asks for consent, moves it into a private, owner-only stash (`pi-auth-slot/anthropic.json` in the directory of Pi's enrollment record), and leaves an inert placeholder API key in Pi's auth file. The placeholder is a fixed, non-secret value that no provider accepts.
28
+ Pi: open `/claude` and use **Start OAuth sign-in**, then **Finish OAuth sign-in**.
64
29
 
65
- While enrolled, Pi serves only vault accounts; vault accounts added with `ck auth login --provider anthropic` are discovered without a restart. Local accounts are not used, refreshed, quota-polled, kept warm by cache keepalive, or shown, and they return unchanged after disconnect. If the vault has no usable Claude account, requests refuse: add accounts with `ck auth login --provider anthropic`, or disconnect.
66
-
67
- **Disconnect from the vault** in `/claude` asks for confirmation, restores the stashed login byte-for-byte, then ends Pi's enrollment. If Pi's auth file already holds a different login, disconnect stops and keeps both logins and the enrollment so you can resolve them with setup. **Disconnect from the vault** is also offered when the enrollment record cannot be read. If the local pool has never been migrated, stop the hosts and run `setup --no-claustrum` before serving local accounts; disconnect does not perform migration.
68
-
69
- ## Settings
70
-
71
- The pool stores the same settings as the OpenCode package: `routing`, `claudeCache`, `cacheKeep`, `prime`, `claudeFast`, `thinkingBinding`, `dump`, `relay`, and the fallback accounts. While Pi is enrolled in the vault, settings are kept separately in `anthropic-auth-vault-settings.json`. `thinkingBinding.prefixMismatchBehavior` accepts `account-default` (default), `error`, or `drop_block` when a Fable 5.1 OAuth request reuses reasoning with an Anthropic signature from an earlier reply after the preceding messages, instructions, or tools change. Sticky session assignments are stored in `anthropic-auth-pool-routing-state.json` (`anthropic-auth-vault-routing-state.json` while enrolled) with SHA-256-hashed session IDs.
72
-
73
- ## The /claude menu
74
-
75
- Pi registers one command, `/claude`. It opens a menu; each action collects its own parameters, and typed arguments are rejected with a pointer to the menu. Without a UI, `/claude` prints the status sections only.
76
-
77
- | Group | Status | Actions |
78
- | --- | --- | --- |
79
- | Accounts | Account status and usage | Enable account, Disable account, Remove account, Move account up, Move account down, Reset account backoff, Reset host enrollment, Add API-key fallback, Start OAuth sign-in, Finish OAuth sign-in, Custody setup guidance, Disconnect from the vault |
80
- | Quota | Quota summary | — |
81
- | Routing | Routing status | Routing mode (`main-first`, `fallback-first`, `sticky-balanced`), Reset routing session |
82
- | Cache | Cache status, Cache keep status | Enable cache, Disable cache, Cache mode (`explicit`, `automatic`, `hybrid`), Keep cache warm always, Disable cache keep, Cache keep window, Cache keep for subagents (`on`/`off`) |
83
- | Diagnostics | Dump status, Logging status | Enable request dumps, Disable request dumps, Logging level (`error`, `warn`, `info`, `debug`, `trace`) |
84
- | Extras | Fast mode status, Prime quota window status | Enable fast mode, Disable fast mode |
85
-
86
- In local custody, every Accounts action except **Disconnect from the vault** is offered. While Pi is enrolled, or when its enrollment cannot be read, the Accounts group shows only enable, disable, **Reset account backoff**, and **Disconnect from the vault**. **Remove account**, **Reset host enrollment**, and **Disconnect from the vault** ask for confirmation. Killswitch, quota refresh, Prime on/off, and lane start are OpenCode-only.
87
-
88
- **Reset account 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 clears the backoff left by the old refresh token. **Quota summary** reports quota for the accounts of the active custody source. **Routing mode** → `fallback-first` prefers usable OAuth fallback accounts before the main account; `main-first` restores the default. `sticky-balanced` assigns each Pi session to an OAuth account according to current 5-hour, 7-day, and model-specific quota headroom, then keeps the session on that account (session affinity), persisting the assignment across transient failures and process restarts. Changing the session model discards the old assignment before quota-based reselection. **Reset routing session** clears the current Pi session's assignment. Direct Opus sessions prefer usable accounts whose Fable quota (the separate weekly limit for Claude Fable models) is exhausted, saving the other accounts for Fable sessions. API-key routes 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. **Keep cache warm always** keeps active hybrid caches warm while Pi remains open; **Cache keep window** limits prewarms to a local time window. Both send `max_tokens: 0` pre-warm requests about five minutes before the 1-hour TTL expires. **Cache keep status** lists live tracked sessions across Pi processes through a temporary lease registry that stores only session IDs and cache timing. **Enable fast mode** adds Anthropic `speed: "fast"` plus the `fast-mode-2026-02-01` beta header for supported Opus models (`claude-opus-4-8`, `claude-opus-5`, and `claude-opus-5-5`).
89
-
90
- ### Claude quota window priming
91
-
92
- Priming is off by default. When enabled from OpenCode's `/claude` menu with **Enable Prime**, 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.
93
-
94
- For an idle account with no cached reset time, one minimal request starts a usage window; the follow-up quota check learns its reset time. When several processes share an account store, each tries to create a claim file for that account and reset; creating the file is atomic, so exactly one process succeeds and sends the request.
95
-
96
- Claim files live in a temporary directory, separated by account store and credential lineage. Cleanup removes files older than six hours. Ordinary token refresh keeps the lineage; a new sign-in starts a new one. Private runtime state stores cumulative priming usage counters, not a history of primed windows. Legacy claim files are not migrated. Without the old claim, the new scheduler can treat an already primed window as unclaimed and send one extra request.
97
-
98
- Pi's menu shows **Prime quota window status** only; turn priming on or off with **Enable Prime** or **Disable Prime** in OpenCode's `/claude` menu.
99
-
100
- ## Anthropic-compatible proxy overrides
101
-
102
- API-key fallback routes honor `ANTHROPIC_CUSTOM_HEADERS`, `ANTHROPIC_MODEL`, and `ANTHROPIC_DEFAULT_{SONNET,OPUS,HAIKU,FABLE}_MODEL`. These variables never alter OAuth requests. Custom headers may add proxy metadata but cannot replace route authentication, Anthropic protocol headers, body framing, or internal correlation headers; a configuration containing an invalid or protected header is ignored as a whole. Versioned provider base paths are preserved without duplicating `/v1`.
103
-
104
- ## Relay
105
-
106
- The Pi package can use a user-owned Cloudflare relay over HTTP or WebSocket transport. When setup migrates Pi, it imports an existing `relay` block from Pi's old sidecar file (`~/.pi/agent/anthropic-auth.json`, or `PI_ANTHROPIC_AUTH_FILE`). The relay setup helper in the OpenCode package CLI provisions a Worker and saves its settings in OpenCode's local account pool; it does not configure Pi:
107
-
108
- ```bash
109
- CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... bunx @cortexkit/opencode-anthropic-auth relay setup
110
- ```
30
+ ## Documentation
111
31
 
112
- The `/claude` menu has no relay actions.
32
+ - [Installation, configuration and manual registration](../../docs/installation.md#installation-and-configuration)
33
+ - [Features and host support](../../docs/features.md#features-and-host-support)
34
+ - [Models, thinking and fast mode](../../docs/models.md#models-thinking-and-fast-mode)
35
+ - [Accounts, routing and quota](../../docs/accounts-and-routing.md#accounts-routing-and-quota)
36
+ - [Settings and routing reference](../../docs/configuration.md#settings-and-routing-reference)
37
+ - [Vault custody](../../docs/vault.md#vault-custody)
38
+ - [Credential custody contract](../../docs/custody-state-machine.md#anthropic-credential-custody)
39
+ - [Commands and menus](../../docs/commands.md#commands-and-menus)
40
+ - [Cache, keepalive and quota priming](../../docs/cache.md#cache-keepalive-and-quota-priming)
41
+ - [Cache diagnostics](../../docs/cache-diagnostics.md#cache-diagnostics)
42
+ - [OpenCode 2](../../docs/opencode-2.md#opencode-2)
43
+ - [OpenCode sidebar and TUI preferences](../../docs/sidebar.md#opencode-sidebar-and-tui-preferences)
44
+ - [Environment variables](../../docs/environment.md#environment-variables)
45
+ - [Request rewriting and safety fallback](../../docs/request-rewriting.md#request-rewriting-and-safety-fallback)
46
+ - [Transports and request diagnostics](../../docs/transports.md#transports-and-request-diagnostics)
47
+ - [Troubleshooting](../../docs/troubleshooting.md#troubleshooting)
48
+ - [Development](../../docs/development.md#development)
49
+ - [Build modes](../../docs/build-modes.md#build-modes)
50
+ - [Upgrades, recovery and releases](../../docs/releases.md#upgrades-recovery-and-releases)
113
51
 
114
52
  ## License
115
53
 
116
- MIT
54
+ [MIT](../../LICENSE)
package/dist/convert.js CHANGED
@@ -284,8 +284,11 @@ function addEphemeralCacheControl(body) {
284
284
  if (message?.role !== 'user')
285
285
  continue;
286
286
  const content = message.content;
287
- if (Array.isArray(content)) {
288
- const lastBlock = content.at(-1);
287
+ if (typeof content === 'string') {
288
+ message.content = [{ type: 'text', text: content }];
289
+ }
290
+ if (Array.isArray(message.content)) {
291
+ const lastBlock = message.content.at(-1);
289
292
  if (lastBlock && typeof lastBlock === 'object') {
290
293
  lastBlock.cache_control = { type: 'ephemeral' };
291
294
  }
package/dist/custody.js CHANGED
@@ -26,7 +26,7 @@ export function createPiCustodyCommands() {
26
26
  // Mode changes can remove host credentials and require offline consent.
27
27
  // A menu never enrolls, grants custody or writes the authority journal.
28
28
  return {
29
- text: `Refused: Changing Pi authentication to ${mode} requires offline setup. Run \`bunx @cortexkit/opencode-anthropic-auth setup\` and review the migration and host OAuth removal consent.`,
29
+ text: `Refused: Changing Pi authentication to ${mode} requires offline setup. Run \`npx @cortexkit/opencode-anthropic-auth setup\` and review the migration and host OAuth removal consent.`,
30
30
  };
31
31
  },
32
32
  };
@@ -1,4 +1,4 @@
1
- import { type MidConversationEffortTransition } from '@cortexkit/anthropic-auth-core';
1
+ import { type AdaptiveEffort, type MidConversationEffortTransition } from '@cortexkit/anthropic-auth-core';
2
2
  type SessionEntryLike = {
3
3
  id?: unknown;
4
4
  type?: unknown;
@@ -22,6 +22,11 @@ type SessionEntryLike = {
22
22
  * `firstKeptEntryId`, then everything appended after the compaction.
23
23
  */
24
24
  export declare function deriveContextEntries<Entry extends SessionEntryLike>(branch: readonly Entry[]): Entry[];
25
- /** Build Pi's active, compaction-aware Fable 5.1 effort timeline. */
26
- export declare function collectPiEffortHistory(contextEntries: readonly SessionEntryLike[], fullBranch: readonly SessionEntryLike[]): MidConversationEffortTransition[];
25
+ /**
26
+ * Build Pi's active, compaction-aware effort timeline for per-message effort.
27
+ * A thinking level without an adaptive effort (such as `off`) and a branch
28
+ * without any level change both count as `defaultEffort`, the effort the
29
+ * request model applies when no top-level effort is sent.
30
+ */
31
+ export declare function collectPiEffortHistory(contextEntries: readonly SessionEntryLike[], fullBranch: readonly SessionEntryLike[], defaultEffort?: AdaptiveEffort): MidConversationEffortTransition[];
27
32
  export {};
@@ -57,9 +57,14 @@ function appendTransition(transitions, afterAssistantMessages, effort) {
57
57
  }
58
58
  transitions.push({ afterAssistantMessages, effort });
59
59
  }
60
- /** Build Pi's active, compaction-aware Fable 5.1 effort timeline. */
61
- export function collectPiEffortHistory(contextEntries, fullBranch) {
62
- let activeEffort = 'high';
60
+ /**
61
+ * Build Pi's active, compaction-aware effort timeline for per-message effort.
62
+ * A thinking level without an adaptive effort (such as `off`) and a branch
63
+ * without any level change both count as `defaultEffort`, the effort the
64
+ * request model applies when no top-level effort is sent.
65
+ */
66
+ export function collectPiEffortHistory(contextEntries, fullBranch, defaultEffort = 'high') {
67
+ let activeEffort = defaultEffort;
63
68
  let assistantMessages = 0;
64
69
  const transitions = [];
65
70
  const first = contextEntries[0];
@@ -71,7 +76,8 @@ export function collectPiEffortHistory(contextEntries, fullBranch) {
71
76
  const entry = fullBranch[index];
72
77
  if (entry?.type !== 'thinking_level_change')
73
78
  continue;
74
- activeEffort = normalizeAdaptiveEffort(entry.thinkingLevel) ?? 'high';
79
+ activeEffort =
80
+ normalizeAdaptiveEffort(entry.thinkingLevel) ?? defaultEffort;
75
81
  }
76
82
  appendTransition(transitions, 0, activeEffort);
77
83
  }
@@ -85,7 +91,8 @@ export function collectPiEffortHistory(contextEntries, fullBranch) {
85
91
  branchIndex < compactionIndex;
86
92
  if (entry.type === 'thinking_level_change') {
87
93
  if (!retainedBeforeCompaction) {
88
- activeEffort = normalizeAdaptiveEffort(entry.thinkingLevel) ?? 'high';
94
+ activeEffort =
95
+ normalizeAdaptiveEffort(entry.thinkingLevel) ?? defaultEffort;
89
96
  }
90
97
  continue;
91
98
  }
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { 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, CLAUDE_HAIKU_5_5_CONTEXT_WINDOW, CLAUDE_HAIKU_5_5_LONG_CONTEXT_PRICING, CLAUDE_HAIKU_5_5_LONG_CONTEXT_THRESHOLD, CLAUDE_HAIKU_5_5_MAX_OUTPUT_TOKENS, CLAUDE_HAIKU_5_5_MODEL_ID, CLAUDE_HAIKU_5_5_PRICING, CLAUDE_SONNET_5_5_CONTEXT_WINDOW, CLAUDE_SONNET_5_5_MAX_OUTPUT_TOKENS, CLAUDE_SONNET_5_5_MODEL_ID, CLAUDE_SONNET_5_5_PRICING, isClaudeFableOrMythos51Model, readNativeEnrollmentMode, } from '@cortexkit/anthropic-auth-core';
1
+ import { 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, CLAUDE_HAIKU_5_5_CONTEXT_WINDOW, CLAUDE_HAIKU_5_5_LONG_CONTEXT_PRICING, CLAUDE_HAIKU_5_5_LONG_CONTEXT_THRESHOLD, CLAUDE_HAIKU_5_5_MAX_OUTPUT_TOKENS, CLAUDE_HAIKU_5_5_MODEL_ID, CLAUDE_HAIKU_5_5_PRICING, CLAUDE_SONNET_5_5_CONTEXT_WINDOW, CLAUDE_SONNET_5_5_MAX_OUTPUT_TOKENS, CLAUDE_SONNET_5_5_MODEL_ID, CLAUDE_SONNET_5_5_PRICING, defaultAdaptiveEffort, isClaudeFableOrMythos51Model, nativeAccountCanServe, readNativeEnrollmentMode, } from '@cortexkit/anthropic-auth-core';
2
2
  import { registerCommands } from './commands.js';
3
3
  import { collectPiEffortHistory, deriveContextEntries, } from './effort-history.js';
4
4
  import { closePiNativeRuntime, getPiNativeRuntime, isNativeMigrationRequired, } from './native.js';
@@ -43,11 +43,12 @@ export default async function cortexKitPiAnthropicAuth(pi, options = {}) {
43
43
  const sessionId = ctx.sessionManager.getSessionId();
44
44
  if (!sessionId)
45
45
  return;
46
- // This hook only adds mid-conversation effort markers to Fable/Mythos 5.1
47
- // requests. A host whose session entries do not match what this reads must
48
- // cost the session its transitions and nothing else: the handler runs
49
- // before every turn, and an exception here surfaced as a per-turn extension
50
- // error while collecting no effort history at all (issue #200).
46
+ // This hook collects effort history for the per-message effort markers
47
+ // that core adds to Fable 5.1, Opus 5.5 and Sonnet 5.5 requests. A host
48
+ // whose session entries do not match what this reads must cost the
49
+ // session its transitions and nothing else: the handler runs before every
50
+ // turn, and an exception here surfaced as a per-turn extension error while
51
+ // collecting no effort history at all (issue #200).
51
52
  //
52
53
  // The catch stays quiet: `ExtensionAPI` carries no log surface on either
53
54
  // host, and writing to stdout from a per-turn hook corrupts the host's
@@ -56,7 +57,7 @@ export default async function cortexKitPiAnthropicAuth(pi, options = {}) {
56
57
  let transitions;
57
58
  try {
58
59
  const branch = ctx.sessionManager.getBranch();
59
- transitions = collectPiEffortHistory(deriveContextEntries(branch), branch);
60
+ transitions = collectPiEffortHistory(deriveContextEntries(branch), branch, defaultAdaptiveEffort(ctx.model?.id));
60
61
  }
61
62
  catch {
62
63
  transitions = [];
@@ -251,9 +252,7 @@ export default async function cortexKitPiAnthropicAuth(pi, options = {}) {
251
252
  return true;
252
253
  throw error;
253
254
  }
254
- return snapshot.accounts.some((account) => account.type === 'oauth' &&
255
- account.enabled &&
256
- (account.source !== 'vault' || account.state === 'active'));
255
+ return snapshot.accounts.some((account) => account.type === 'oauth' && nativeAccountCanServe(account));
257
256
  };
258
257
  const provider = {
259
258
  id: 'anthropic',
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { authorize, buildClaudeQuotaSummary, buildPrimeAccountStatuses, DEFAULT_KILLSWITCH_THRESHOLDS, exchange, executeCacheKeepCommand, executePrimeCommand, formatEnrollmentStatus, formatNativeAccountState, formatOAuthAccountTier, getCache1hPersistentMode, getCacheKeepWindow, getPersistedLogLevel, getRoutingMode, isCache1hPersistentlyEnabled, isCacheKeepAlways, isCacheKeepHybridActive, isCacheKeepPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isPrimePersistentlyEnabled, setDumpEnabled, } from '@cortexkit/anthropic-auth-core';
2
+ import { authorize, buildClaudeQuotaSummary, buildPrimeAccountStatuses, DEFAULT_KILLSWITCH_THRESHOLDS, exchange, executeCacheKeepCommand, executePrimeCommand, formatEnrollmentStatus, formatNativeAccountState, formatOAuthAccountTier, getCache1hPersistentMode, getCacheKeepWindow, getPersistedLogLevel, getRoutingMode, isCache1hPersistentlyEnabled, isCacheKeepAlways, isCacheKeepHybridActive, isCacheKeepPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isPrimePersistentlyEnabled, nativeAccountCanServe, setDumpEnabled, } from '@cortexkit/anthropic-auth-core';
3
3
  import { createPiCustodyCommands } from './custody.js';
4
4
  import { getPiNativeRuntime } from './native.js';
5
5
  import { getPiAccountStoragePath } from './paths.js';
@@ -174,7 +174,7 @@ export function createPiNativeCommands(storagePath = getPiAccountStoragePath())
174
174
  case 'quota-refresh': {
175
175
  const snapshot = await native.view();
176
176
  for (const account of snapshot.accounts) {
177
- if (!account.enabled || account.type !== 'oauth')
177
+ if (!nativeAccountCanServe(account) || account.type !== 'oauth')
178
178
  continue;
179
179
  await runtime.fetchQuota(account.id, fetchPiOAuth, options.signal);
180
180
  await runtime.fetchProfile(account.id, fetchPiOAuth, options.signal);
@@ -299,7 +299,10 @@ export function createPiNativeCommands(storagePath = getPiAccountStoragePath())
299
299
  case 'killswitch':
300
300
  return `Killswitch: ${isKillswitchEnabled(storage) ? 'enabled' : 'disabled'}\n${JSON.stringify(storage.killswitch ?? {})}`;
301
301
  case 'cache':
302
- return `1-hour cache: ${isCache1hPersistentlyEnabled(storage) ? 'enabled' : 'disabled'}; mode: ${getCache1hPersistentMode(storage)}`;
302
+ return [
303
+ `1-hour cache: ${isCache1hPersistentlyEnabled(storage) ? 'enabled' : 'disabled'}; mode: ${getCache1hPersistentMode(storage)}`,
304
+ `- Persisted: ${await runtime.getSettingsPath()}`,
305
+ ].join('\n');
303
306
  case 'cachekeep': {
304
307
  const sessions = await getPiTrackedCacheKeepSessions();
305
308
  return executeCacheKeepCommand({
@@ -316,11 +319,20 @@ export function createPiNativeCommands(storagePath = getPiAccountStoragePath())
316
319
  });
317
320
  }
318
321
  case 'dump':
319
- return `Request dumping: ${isDumpPersistentlyEnabled(storage) ? 'enabled' : 'disabled'}`;
322
+ return [
323
+ `Request dumping: ${isDumpPersistentlyEnabled(storage) ? 'enabled' : 'disabled'}`,
324
+ `- Persisted: ${await runtime.getSettingsPath()}`,
325
+ ].join('\n');
320
326
  case 'logging':
321
- return `Logging: ${getPersistedLogLevel(storage) ?? 'info'}`;
327
+ return [
328
+ `Logging: ${getPersistedLogLevel(storage) ?? 'info'}`,
329
+ `- Persisted: ${await runtime.getSettingsPath()}`,
330
+ ].join('\n');
322
331
  case 'fast':
323
- return `Fast mode: ${isFastModePersistentlyEnabled(storage) ? 'enabled' : 'disabled'}`;
332
+ return [
333
+ `Fast mode: ${isFastModePersistentlyEnabled(storage) ? 'enabled' : 'disabled'}`,
334
+ `- Persisted: ${await runtime.getSettingsPath()}`,
335
+ ].join('\n');
324
336
  case 'prime':
325
337
  return executePrimeCommand({
326
338
  argumentsText: 'status',
package/dist/native.d.ts CHANGED
@@ -29,6 +29,7 @@ export interface PiVaultPollOptions {
29
29
  export declare class PiNativeRuntime {
30
30
  private readonly paths;
31
31
  private readonly initialization;
32
+ private readonly env;
32
33
  private vaultWatch;
33
34
  private closed;
34
35
  private closing;
package/dist/native.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { acquireNativeAccountRuntime, createNativeUsagePublisher, getAccountStatePath, getHostClaustrumEnrollmentPaths, isKillswitchEnabled, killswitchPassesPolicy, logger, nativeMigrationAuthorityPhase, nativeRuntimeOptionIdentity, quotaSnapshotHasStandardWindows, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, readNativeAccountsForRequest, readNativeMigrationJournal, resolveClaustrumConnectionPath, resolveNativePoolPaths, } from '@cortexkit/anthropic-auth-core';
2
+ import { acquireNativeAccountRuntime, createNativeUsagePublisher, getAccountStatePath, getHostClaustrumEnrollmentPaths, isKillswitchEnabled, killswitchPassesPolicy, logger, nativeAccountCanServe, nativeMigrationAuthorityPhase, nativeRuntimeOptionIdentity, quotaSnapshotHasStandardWindows, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, readNativeAccountsForRequest, readNativeMigrationJournal, resolveClaustrumConnectionPath, resolveLocalRoutingDestination, resolveNativePoolPaths, } from '@cortexkit/anthropic-auth-core';
3
3
  import { getPiAccountStoragePath, getPiClaustrumConnectionOptions, getPiConfigDir, requirePiNativeHostAuth, } from './paths.js';
4
4
  export function piAttemptAccessToken(attempt) {
5
5
  return attempt.source === 'vault'
@@ -8,7 +8,7 @@ export function piAttemptAccessToken(attempt) {
8
8
  }
9
9
  function requestPolicy(routeId, modelId) {
10
10
  return (account, current) => {
11
- if (!account.enabled)
11
+ if (!nativeAccountCanServe(account))
12
12
  return { status: 'blocked', reason: 'account-disabled' };
13
13
  if (account.source === 'local' && current.mode !== 'local')
14
14
  return { status: 'blocked', reason: 'local-mode-unavailable' };
@@ -61,12 +61,14 @@ export const PI_VAULT_POLL_INTERVAL_MS = 2000;
61
61
  export class PiNativeRuntime {
62
62
  paths;
63
63
  initialization;
64
+ env;
64
65
  vaultWatch;
65
66
  closed = false;
66
67
  closing;
67
68
  usagePublications = createNativeUsagePublisher('native-auth');
68
69
  constructor(storagePath, options = {}) {
69
70
  const env = { ...process.env };
71
+ this.env = env;
70
72
  const poll = options.vaultPoll ?? { intervalMs: PI_VAULT_POLL_INTERVAL_MS };
71
73
  const identity = getPiClaustrumConnectionOptions(storagePath).identity;
72
74
  this.paths = resolveNativePoolPaths(storagePath);
@@ -124,10 +126,13 @@ export class PiNativeRuntime {
124
126
  }
125
127
  // Keep local session-to-account assignments at the exact file destination
126
128
  // recorded by migration; guessing a new path would lose those assignments.
127
- const journal = await readNativeMigrationJournal(await this.paths);
128
- if (!journal ||
129
- !nativeMigrationAuthorityPhase(journal) ||
130
- !journal.routingPaths.destination)
129
+ const paths = await this.paths;
130
+ const journal = await readNativeMigrationJournal(paths);
131
+ if (!journal || !nativeMigrationAuthorityPhase(journal))
132
+ throw new Error('Pi native routing requires committed offline migration');
133
+ if (journal.kind === 'empty-init')
134
+ return { path: resolveLocalRoutingDestination('pi', paths, this.env) };
135
+ if (!journal.routingPaths.destination)
131
136
  throw new Error('Pi native routing requires committed offline migration');
132
137
  return { path: journal.routingPaths.destination };
133
138
  }
@@ -164,7 +169,7 @@ export class PiNativeRuntime {
164
169
  const runtime = await this.service();
165
170
  const snapshot = await runtime.read();
166
171
  const account = snapshot.accounts.find((candidate) => candidate.id === routeId);
167
- if (!account?.enabled || account.type !== 'oauth')
172
+ if (!nativeAccountCanServe(account) || account?.type !== 'oauth')
168
173
  throw new Error('Pi native OAuth route is unavailable');
169
174
  const policy = requestPolicy(routeId, modelId);
170
175
  if (policy(account, snapshot).status === 'blocked')
@@ -264,7 +269,10 @@ export class PiNativeRuntime {
264
269
  this.vaultWatch?.close();
265
270
  this.closing ??= (async () => {
266
271
  await this.usagePublications.drain();
267
- await this.initialization.then((runtime) => runtime.close(), () => { });
272
+ await this.initialization.then(async (runtime) => {
273
+ await runtime.drainQuotaPublications();
274
+ runtime.close();
275
+ }, () => { });
268
276
  })();
269
277
  return this.closing;
270
278
  }
package/dist/stream.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { type ApiKeyAccount, CacheKeepManager, type MidConversationEffortTransition } from '@cortexkit/anthropic-auth-core';
1
+ import { type ApiKeyAccount, CacheKeepManager, type MidConversationEffortTransition, type QuotaManagerOptions } from '@cortexkit/anthropic-auth-core';
2
2
  import { type Api, type AssistantMessageEventStream, type Context, type Model, type SimpleStreamOptions } from '@earendil-works/pi-ai';
3
+ /** Set or clear the test replacement for the wait between quota usage polls. */
4
+ export declare function __setPiQuotaCallPauseForTest(pause: QuotaManagerOptions['pauseBetweenQuotaCalls']): void;
3
5
  export declare function clearPiStickyRoutingSession(storagePath: string, sessionId: string): Promise<void>;
4
6
  export declare function getPiTrackedCacheKeepSessions(): Promise<import("@cortexkit/anthropic-auth-core").CacheKeepTrackedSession[]>;
5
7
  /** Call prewarmNow directly to exercise its provider request without clock or timer mocks. */
package/dist/stream.js CHANGED
@@ -1,4 +1,4 @@
1
- import { applyClaudeCodeHeaders, applyCustomHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, createProviderErrorResponseScope, createSseFramer, createStickyNoRouteResponse, decideStickyQuotaFailure, decodeSseFrame, dumpDirectRequest, FAST_MODE_BETA, getCache1hPersistentMode, getClaudeCodeIdentityForVerifiedAccount, getDefaultCacheKeepRegistryDirectory, getFallbackReauthLabels, getQuotaCheckIntervalMs, getRoutingMode, getScopedQuotaWindowForModel, getThinkingPrefixMismatchBehavior, hasThinkingBindingControls, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isNativeTransientAccountError, isOAuthAccount, isPermanentRefreshError, isValidApiBaseURL, killswitchPassesPolicy, logger, MID_CONVERSATION_OUTPUT_CONFIG_BETA, mergeAnthropicBetas, NativeVaultNoAccountError, parseSseFrame, QuotaManager, quotaBackoffActive, quotaSnapshotHasStandardWindows, quotaSnapshotModelScopeIsExhausted, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, STICKY_ROUTING_MAIN_ACCOUNT_ID, StickySessionRouter, sendViaRelay, setDumpEnabled, shouldFallbackStatus, stickyQuotaSnapshotIsFresh, stickyRetryAfterWithJitter, stickyRouteFamilyForModel, THINKING_BINDING_CONTROLS_BETA, usesMidConversationOutputConfig, } from '@cortexkit/anthropic-auth-core';
1
+ import { applyClaudeCodeHeaders, applyCustomHeaders, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CacheKeepSessionRegistry, createProviderErrorResponseScope, createSseFramer, createStickyNoRouteResponse, decideStickyQuotaFailure, decodeSseFrame, dumpDirectRequest, FAST_MODE_BETA, getCache1hPersistentMode, getClaudeCodeIdentityForVerifiedAccount, getDefaultCacheKeepRegistryDirectory, getFallbackReauthLabels, getQuotaCheckIntervalMs, getRequestGzipLatch, getRequestGzipLatchPath, getRoutingMode, getScopedQuotaWindowForModel, getThinkingPrefixMismatchBehavior, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isDumpPersistentlyEnabled, isFastModePersistentlyEnabled, isKillswitchEnabled, isNativeTransientAccountError, isOAuthAccount, isPermanentRefreshError, isValidApiBaseURL, killswitchPassesPolicy, logger, mergeAnthropicBetas, NativeVaultNoAccountError, nativeAccountCanServe, parseSseFrame, QuotaManager, quotaBackoffActive, quotaSnapshotHasStandardWindows, quotaSnapshotModelScopeIsExhausted, quotaSnapshotPassesModelScope, quotaSnapshotPassesPolicy, STICKY_ROUTING_MAIN_ACCOUNT_ID, StickySessionRouter, sendViaRelay, sendWithRequestGzip, 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 { getPiNativeRuntime, piAttemptAccessToken, } from './native.js';
@@ -11,6 +11,15 @@ let cacheKeepRegistryDirectory;
11
11
  const stickyRouters = new Map();
12
12
  const quotaManagers = new Map();
13
13
  const PI_SERVICE_CACHE_LIMIT = 16;
14
+ /**
15
+ * Tests only: replaces the real-time wait between quota usage polls in quota
16
+ * managers created after it is set. Unset, managers keep Core's real timer.
17
+ */
18
+ let quotaCallPauseForTest;
19
+ /** Set or clear the test replacement for the wait between quota usage polls. */
20
+ export function __setPiQuotaCallPauseForTest(pause) {
21
+ quotaCallPauseForTest = pause;
22
+ }
14
23
  function setBoundedService(map, key, value) {
15
24
  map.delete(key);
16
25
  map.set(key, value);
@@ -37,11 +46,12 @@ function getPiRoutingServices(storagePath, snapshot) {
37
46
  if (!quotaManager) {
38
47
  quotaManager = new QuotaManager({
39
48
  storage,
49
+ pauseBetweenQuotaCalls: quotaCallPauseForTest,
40
50
  fetchQuotaSnapshot: async (request, dispatch) => {
41
51
  const id = request.kind === 'main' ? 'main' : request.accountId;
42
52
  if (!id)
43
53
  throw new Error('Native quota request has no account identity');
44
- return (await native.service()).fetchQuota(id, fetchPiOAuth, undefined, dispatch);
54
+ return (await native.service()).fetchQuota(id, fetchPiOAuth, undefined, dispatch, request.purpose);
45
55
  },
46
56
  });
47
57
  setBoundedService(quotaManagers, storagePath, {
@@ -59,9 +69,9 @@ function getPiRoutingServices(storagePath, snapshot) {
59
69
  const usable = [];
60
70
  for (const account of current.accounts) {
61
71
  const view = accounts.get(account.id);
62
- if (!isOAuthAccount(account) || !view?.enabled || view.type !== 'oauth')
63
- continue;
64
- if (view.source === 'vault' && view.state !== 'active')
72
+ if (!isOAuthAccount(account) ||
73
+ !nativeAccountCanServe(view) ||
74
+ view?.type !== 'oauth')
65
75
  continue;
66
76
  if (isPermanentRefreshError(view.lastRefreshError) ||
67
77
  (view.source === 'local' &&
@@ -74,7 +84,7 @@ function getPiRoutingServices(storagePath, snapshot) {
74
84
  quotas.isFallbackStale(account.id, undefined, options.modelId, account) &&
75
85
  !quotaBackoffActive(account.lastQuotaRefreshError, Date.now())) {
76
86
  try {
77
- await quotas.refreshFallback(account.id, '', account);
87
+ await quotas.refreshFallback(account.id, '', account, 'admission');
78
88
  }
79
89
  catch (error) {
80
90
  // A transient account error means another writer is still saving
@@ -117,6 +127,25 @@ function getPiCacheKeepRegistry() {
117
127
  }
118
128
  return cacheKeepRegistry;
119
129
  }
130
+ /**
131
+ * Sets the Claude Code headers for a Pi OAuth request to Anthropic, with the
132
+ * betas this body needs. Model requests and CacheKeep prewarms both call it:
133
+ * a prewarm replays a model request's body, and Anthropic needs the same
134
+ * betas to accept it (for example, the effort beta for the empty
135
+ * `role: "system"` effort markers in a Fable 5.1 body). `leadingBetas` appends
136
+ * adapter-requested feature betas without changing Core's model-beta selection.
137
+ */
138
+ function applyPiOAuthRequestHeaders(headers, accessToken, options) {
139
+ const { body } = options;
140
+ applyClaudeCodeHeaders(headers, accessToken, {
141
+ body,
142
+ identity: options.identity,
143
+ extraBetas: options.leadingBetas,
144
+ });
145
+ if (body.speed === 'fast')
146
+ headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
147
+ return headers;
148
+ }
120
149
  const nativePrewarmAttempts = new Map();
121
150
  const cacheKeepManager = new CacheKeepManager({
122
151
  fetchImpl: fetchPiOAuth,
@@ -136,18 +165,11 @@ const cacheKeepManager = new CacheKeepManager({
136
165
  ? (current.policyStorage.mainAccountId ?? id)
137
166
  : id, served.accountIdentity);
138
167
  headers.delete('anthropic-beta');
139
- applyClaudeCodeHeaders(headers, piAttemptAccessToken(served), {
168
+ applyPiOAuthRequestHeaders(headers, piAttemptAccessToken(served), {
140
169
  body,
141
170
  identity,
142
- extraBetas: [
143
- CACHE_KEEP_EXTENDED_TTL_BETA,
144
- ...(hasThinkingBindingControls(body)
145
- ? [THINKING_BINDING_CONTROLS_BETA]
146
- : []),
147
- ],
171
+ leadingBetas: [CACHE_KEEP_EXTENDED_TTL_BETA],
148
172
  });
149
- if (body.speed === 'fast')
150
- headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
151
173
  nativePrewarmAttempts.set(attempt.id, { runtime, served, reported: false });
152
174
  return headers;
153
175
  },
@@ -343,20 +365,7 @@ async function sendAnthropicRequest(options) {
343
365
  const fastMode = body.speed === 'fast';
344
366
  const headers = options.apiAccount
345
367
  ? new Headers()
346
- : applyClaudeCodeHeaders(new Headers(), '', {
347
- body,
348
- identity,
349
- extraBetas: [
350
- ...(hasThinkingBindingControls(body)
351
- ? [THINKING_BINDING_CONTROLS_BETA]
352
- : []),
353
- ...(usesMidConversationOutputConfig(body)
354
- ? [MID_CONVERSATION_OUTPUT_CONFIG_BETA]
355
- : []),
356
- ],
357
- });
358
- if (!options.apiAccount && fastMode)
359
- headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA]));
368
+ : applyPiOAuthRequestHeaders(new Headers(), '', { body, identity });
360
369
  const relayAffinity = options.streamOptions?.sessionId ?? null;
361
370
  let input = options.apiAccount
362
371
  ? new URL('https://api.anthropic.com/v1/messages')
@@ -435,7 +444,17 @@ async function sendAnthropicRequest(options) {
435
444
  const send = async () => {
436
445
  const scope = createProviderErrorResponseScope([secret], 'event-stream');
437
446
  physical.scope = scope;
438
- return scope.redactResponse(await fetch(input, init));
447
+ // OAuth bodies go gzipped as Claude Code sends them; `bodyText` stays
448
+ // the signed identity text for dumps and CacheKeep.
449
+ if (options.apiAccount || typeof init.body !== 'string')
450
+ return scope.redactResponse(await fetch(input, init));
451
+ return scope.redactResponse(await sendWithRequestGzip({
452
+ url: input,
453
+ body: init.body,
454
+ headers,
455
+ latch: getRequestGzipLatch(getRequestGzipLatchPath(options.storagePath)),
456
+ send: (wireBody, wireHeaders) => fetch(input, { ...init, body: wireBody, headers: wireHeaders }),
457
+ }));
439
458
  };
440
459
  let response;
441
460
  try {
@@ -471,6 +490,11 @@ async function sendAnthropicRequest(options) {
471
490
  }
472
491
  if (attempt)
473
492
  await report(attempt, response.status, 'direct');
493
+ // The vault has been told about the 401 above. If the request was
494
+ // cancelled after the 401 arrived (before or while the vault was asked
495
+ // for a newer credential), end it as cancelled here.
496
+ if (attempt && response.status === 401)
497
+ await endIfCancelled(response, init.signal);
474
498
  await dumpDirectRequest({
475
499
  affinity: relayAffinity,
476
500
  route: options.route ??
@@ -565,10 +589,27 @@ async function sendAnthropicRequest(options) {
565
589
  await Promise.all(relayReports);
566
590
  }
567
591
  }
592
+ // A relayed 401 is reported to the vault as soon as the relay returns it.
593
+ // If the request was cancelled since, end it as cancelled here.
594
+ if (relay401Attempt && response.status === 401)
595
+ await endIfCancelled(response, init.signal);
568
596
  if (response.ok && lastTransport === 'relay' && relayAttempt)
569
597
  markServed(relayAttempt);
570
598
  return response;
571
599
  }
600
+ /**
601
+ * If the request's signal is aborted, cancels the response body and throws the
602
+ * signal's abort error; otherwise does nothing. Call it before a rejected
603
+ * response is handed to account routing. Routing treats a returned 401 as a
604
+ * failure of the account and may move the session to another account (sticky
605
+ * migration); a request the user cancelled must end as cancelled instead.
606
+ */
607
+ async function endIfCancelled(response, signal) {
608
+ if (!signal?.aborted)
609
+ return;
610
+ await response.body?.cancel().catch(() => { });
611
+ signal.throwIfAborted();
612
+ }
572
613
  function quotaSnapshotIsExhausted(quota) {
573
614
  return ['five_hour', 'seven_day'].some((key) => (quota?.[key]?.remainingPercent ?? 1) <= 0);
574
615
  }
@@ -708,8 +749,7 @@ async function executeWithFallback(options) {
708
749
  const nativeReady = (id) => {
709
750
  const account = accountView(id);
710
751
  return Boolean(account?.type === 'oauth' &&
711
- account.enabled &&
712
- (account.source !== 'vault' || account.state === 'active') &&
752
+ nativeAccountCanServe(account) &&
713
753
  !isPermanentRefreshError(account.lastRefreshError) &&
714
754
  (account.source !== 'local' ||
715
755
  (account.lastRefreshError?.nextRetryAt ?? 0) <= Date.now()));
@@ -750,7 +790,7 @@ async function executeWithFallback(options) {
750
790
  if (primaryAvailable() &&
751
791
  !stickyQuotaSnapshotIsFresh(mainEntry?.quota, storage, Date.now(), modelId)) {
752
792
  try {
753
- mainQuota = await quotaManager.refreshMain(mainAccountId, '');
793
+ mainQuota = await quotaManager.refreshMain(mainAccountId, '', undefined, 'admission');
754
794
  }
755
795
  catch { }
756
796
  }
@@ -767,7 +807,7 @@ async function executeWithFallback(options) {
767
807
  });
768
808
  }
769
809
  for (const [index, configured] of (storage?.accounts ?? []).entries()) {
770
- if (configured.enabled === false || !isOAuthAccount(configured))
810
+ if (!nativeAccountCanServe(configured) || !isOAuthAccount(configured))
771
811
  continue;
772
812
  const account = usableById.get(configured.id) ?? configured;
773
813
  if (!nativeReady(account.id))
@@ -775,7 +815,7 @@ async function executeWithFallback(options) {
775
815
  let accountQuota = quotaManager.getFallback(account.id, account)?.quota ?? account.quota;
776
816
  if (!stickyQuotaSnapshotIsFresh(accountQuota, storage, Date.now(), modelId)) {
777
817
  try {
778
- accountQuota = await quotaManager.refreshFallback(account.id, '', account);
818
+ accountQuota = await quotaManager.refreshFallback(account.id, '', account, 'admission');
779
819
  }
780
820
  catch { }
781
821
  }
@@ -883,7 +923,7 @@ async function executeWithFallback(options) {
883
923
  });
884
924
  const usableOAuthById = new Map(usableOAuth.map((account) => [account.id, account]));
885
925
  for (const configured of storage.accounts) {
886
- if (configured.enabled === false)
926
+ if (!nativeAccountCanServe(configured))
887
927
  continue;
888
928
  let response;
889
929
  if (isOAuthAccount(configured)) {
@@ -904,8 +944,8 @@ async function executeWithFallback(options) {
904
944
  }
905
945
  else if (routeOptions.includeApiRoutes) {
906
946
  const account = accountView(configured.id);
907
- if (!account?.enabled ||
908
- account.type !== 'api' ||
947
+ if (!nativeAccountCanServe(account) ||
948
+ account?.type !== 'api' ||
909
949
  !isValidApiBaseURL(account.baseURL))
910
950
  continue;
911
951
  response = await sendAnthropicRequest({
@@ -1043,6 +1083,8 @@ async function executeWithFallback(options) {
1043
1083
  migrate = decision.action === 'migrate';
1044
1084
  }
1045
1085
  if (migrate) {
1086
+ // A cancelled request must not move the session's account.
1087
+ await endIfCancelled(preflight instanceof Response ? preflight : response, options.streamOptions?.signal);
1046
1088
  const failedRouteId = route.id;
1047
1089
  routes = await buildStickyRoutes(options.model.id);
1048
1090
  if (routes.candidates.some((candidate) => candidate.accountId !== failedRouteId)) {
@@ -1120,7 +1162,7 @@ async function executeWithFallback(options) {
1120
1162
  let mainQuota = quotaManager.getMain(mainAccountId)?.quota;
1121
1163
  if (!mainQuota || quotaManager.isMainStale(options.model.id)) {
1122
1164
  try {
1123
- mainQuota = await quotaManager.refreshMain(mainAccountId, '');
1165
+ mainQuota = await quotaManager.refreshMain(mainAccountId, '', undefined, 'admission');
1124
1166
  }
1125
1167
  catch { }
1126
1168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/pi-anthropic-auth",
3
- "version": "2.0.2",
3
+ "version": "2.1.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": "2.0.2"
39
+ "@cortexkit/anthropic-auth-core": "2.1.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@earendil-works/pi-ai": ">=0.86.1 <2",