@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 +34 -96
- package/dist/convert.js +5 -2
- package/dist/custody.js +1 -1
- package/dist/effort-history.d.ts +8 -3
- package/dist/effort-history.js +12 -5
- package/dist/index.js +9 -10
- package/dist/native-commands.js +18 -6
- package/dist/native.d.ts +1 -0
- package/dist/native.js +16 -8
- package/dist/stream.d.ts +3 -1
- package/dist/stream.js +81 -39
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,116 +1,54 @@
|
|
|
1
1
|
# @cortexkit/pi-anthropic-auth
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
## Why use it
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
12
|
+
## Disclaimer
|
|
10
13
|
|
|
11
|
-
This
|
|
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
|
-
|
|
19
|
+
npx @cortexkit/opencode-anthropic-auth setup
|
|
42
20
|
```
|
|
43
21
|
|
|
44
|
-
|
|
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
|
-
|
|
24
|
+
OpenCode 1: open `/claude` → **Accounts** to sign in.
|
|
60
25
|
|
|
61
|
-
|
|
26
|
+
OpenCode 2: In OpenCode, sign in to Anthropic with "Claude Pro / Max (native account pool)".
|
|
62
27
|
|
|
63
|
-
|
|
28
|
+
Pi: open `/claude` and use **Start OAuth sign-in**, then **Finish OAuth sign-in**.
|
|
64
29
|
|
|
65
|
-
|
|
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
|
-
|
|
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 (
|
|
288
|
-
|
|
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 \`
|
|
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
|
};
|
package/dist/effort-history.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
26
|
-
|
|
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 {};
|
package/dist/effort-history.js
CHANGED
|
@@ -57,9 +57,14 @@ function appendTransition(transitions, afterAssistantMessages, effort) {
|
|
|
57
57
|
}
|
|
58
58
|
transitions.push({ afterAssistantMessages, effort });
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
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',
|
package/dist/native-commands.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
322
|
+
return [
|
|
323
|
+
`Request dumping: ${isDumpPersistentlyEnabled(storage) ? 'enabled' : 'disabled'}`,
|
|
324
|
+
`- Persisted: ${await runtime.getSettingsPath()}`,
|
|
325
|
+
].join('\n');
|
|
320
326
|
case 'logging':
|
|
321
|
-
return
|
|
327
|
+
return [
|
|
328
|
+
`Logging: ${getPersistedLogLevel(storage) ?? 'info'}`,
|
|
329
|
+
`- Persisted: ${await runtime.getSettingsPath()}`,
|
|
330
|
+
].join('\n');
|
|
322
331
|
case 'fast':
|
|
323
|
-
return
|
|
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
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
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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) =>
|
|
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,
|
|
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) ||
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
168
|
+
applyPiOAuthRequestHeaders(headers, piAttemptAccessToken(served), {
|
|
140
169
|
body,
|
|
141
170
|
identity,
|
|
142
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
908
|
-
account
|
|
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
|
|
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
|
|
39
|
+
"@cortexkit/anthropic-auth-core": "2.1.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@earendil-works/pi-ai": ">=0.86.1 <2",
|