@cortexkit/pi-anthropic-auth 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ex Machina
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @cortexkit/pi-anthropic-auth
2
+
3
+ Pi package for CortexKit Anthropic OAuth support. It overrides Pi's built-in `anthropic` provider with a CortexKit provider extension backed by the shared `@cortexkit/anthropic-auth-core` package.
4
+
5
+ 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.
6
+
7
+ ## Install
8
+
9
+ Install with Pi's package manager:
10
+
11
+ ```bash
12
+ pi install npm:@cortexkit/pi-anthropic-auth@1.0.0
13
+ ```
14
+
15
+ For an unpinned install:
16
+
17
+ ```bash
18
+ pi install npm:@cortexkit/pi-anthropic-auth
19
+ ```
20
+
21
+ To try it for one run without changing Pi settings:
22
+
23
+ ```bash
24
+ pi -e npm:@cortexkit/pi-anthropic-auth
25
+ ```
26
+
27
+ Restart Pi after installing, then authenticate through Pi's normal login flow:
28
+
29
+ ```text
30
+ /login anthropic
31
+ ```
32
+
33
+ ## Sidecar config
34
+
35
+ Pi state is stored separately from OpenCode at:
36
+
37
+ ```text
38
+ ~/.pi/agent/anthropic-auth.json
39
+ ```
40
+
41
+ Override the path with `PI_ANTHROPIC_AUTH_FILE`. The package also respects `PI_AGENT_DIR` when deriving the default sidecar path.
42
+
43
+ The sidecar uses the same JSON shape as the OpenCode package, including `claudeCache`, `dump`, `relay`, and fallback `accounts` blocks.
44
+
45
+ ## Commands
46
+
47
+ ```text
48
+ /claude-cache
49
+ /claude-cache on
50
+ /claude-cache off
51
+ /claude-cache mode explicit
52
+ /claude-cache mode automatic
53
+ /claude-cache mode hybrid
54
+
55
+ /claude-dump
56
+ /claude-dump on
57
+ /claude-dump off
58
+
59
+ /claude-quota
60
+ ```
61
+
62
+ `/claude-quota` reports sidecar fallback account quota state from `~/.pi/agent/anthropic-auth.json`.
63
+
64
+ ## Relay
65
+
66
+ The Pi package can use the same user-owned Cloudflare relay config as the OpenCode package. The relay setup helper currently lives in the OpenCode package CLI:
67
+
68
+ ```bash
69
+ CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... bunx @cortexkit/opencode-anthropic-auth relay setup
70
+ ```
71
+
72
+ For Pi, copy the generated `relay` block into `~/.pi/agent/anthropic-auth.json`.
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
2
+ export declare function registerCommands(pi: ExtensionAPI): void;
@@ -0,0 +1,68 @@
1
+ import { buildClaudeQuotaSummary, buildFallbackQuotaSummaries, executeCache1hCommand, executeDumpCommand, getCache1hPersistentMode, isCache1hPersistentlyEnabled, isDumpPersistentlyEnabled, loadAccounts, parseCache1hCommandAction, parseDumpCommandAction, setCache1hPersistentEnabled, setCache1hPersistentMode, setDumpPersistentEnabled, } from '@cortexkit/anthropic-auth-core';
2
+ import { getPiAccountStoragePath } from "./paths.js";
3
+ function notify(ctx, message, kind = 'info') {
4
+ ctx.ui.notify(message, kind);
5
+ }
6
+ export function registerCommands(pi) {
7
+ pi.registerCommand('claude-cache', {
8
+ description: 'Show or configure Claude 1-hour prompt cache mode',
9
+ handler: async (args, ctx) => {
10
+ const path = getPiAccountStoragePath();
11
+ const storage = await loadAccounts(path);
12
+ const action = parseCache1hCommandAction(args ?? '');
13
+ const enabled = isCache1hPersistentlyEnabled(storage);
14
+ const mode = getCache1hPersistentMode(storage);
15
+ const nextEnabled = action.type === 'enable'
16
+ ? true
17
+ : action.type === 'disable'
18
+ ? false
19
+ : enabled;
20
+ const nextMode = action.type === 'mode' ? action.mode : mode;
21
+ if (action.type === 'enable') {
22
+ await setCache1hPersistentEnabled(true, undefined, path);
23
+ }
24
+ else if (action.type === 'disable') {
25
+ await setCache1hPersistentEnabled(false, undefined, path);
26
+ }
27
+ else if (action.type === 'mode') {
28
+ await setCache1hPersistentMode(action.mode, path);
29
+ }
30
+ notify(ctx, executeCache1hCommand({
31
+ argumentsText: args ?? '',
32
+ enabled: nextEnabled,
33
+ mode: nextMode,
34
+ }), action.type === 'usage' ? 'warning' : 'info');
35
+ },
36
+ });
37
+ pi.registerCommand('claude-dump', {
38
+ description: 'Show or configure Anthropic request/relay dumping',
39
+ handler: async (args, ctx) => {
40
+ const path = getPiAccountStoragePath();
41
+ const storage = await loadAccounts(path);
42
+ const action = parseDumpCommandAction(args ?? '');
43
+ const enabled = isDumpPersistentlyEnabled(storage);
44
+ const nextEnabled = action.type === 'enable'
45
+ ? true
46
+ : action.type === 'disable'
47
+ ? false
48
+ : enabled;
49
+ if (action.type === 'enable') {
50
+ await setDumpPersistentEnabled(true, path);
51
+ }
52
+ else if (action.type === 'disable') {
53
+ await setDumpPersistentEnabled(false, path);
54
+ }
55
+ notify(ctx, executeDumpCommand({ argumentsText: args ?? '', enabled: nextEnabled }), action.type === 'usage' ? 'warning' : 'info');
56
+ },
57
+ });
58
+ pi.registerCommand('claude-quota', {
59
+ description: 'Show Claude quota state for fallback accounts',
60
+ handler: async (_args, ctx) => {
61
+ const storage = await loadAccounts(getPiAccountStoragePath());
62
+ notify(ctx, buildClaudeQuotaSummary({
63
+ accounts: buildFallbackQuotaSummaries(storage),
64
+ now: Date.now(),
65
+ }));
66
+ },
67
+ });
68
+ }
@@ -0,0 +1,25 @@
1
+ import { type Cache1hMode } from '@cortexkit/anthropic-auth-core';
2
+ import type { Context, SimpleStreamOptions, Tool } from '@mariozechner/pi-ai';
3
+ export type AnthropicRequestBody = {
4
+ model: string;
5
+ max_tokens: number;
6
+ stream: true;
7
+ system?: Array<Record<string, unknown>>;
8
+ messages: Array<Record<string, unknown>>;
9
+ tools?: Array<Record<string, unknown>>;
10
+ thinking?: {
11
+ type: 'enabled';
12
+ budget_tokens: number;
13
+ };
14
+ cache_control?: {
15
+ type: 'ephemeral';
16
+ };
17
+ };
18
+ export declare function fromClaudeCodeToolName(name: string, tools?: Tool[]): string;
19
+ export declare function buildAnthropicRequest(modelId: string, context: Context, options: SimpleStreamOptions | undefined, cache: {
20
+ enabled: boolean;
21
+ mode: Cache1hMode;
22
+ }): Promise<{
23
+ body: AnthropicRequestBody;
24
+ bodyText: string;
25
+ }>;
@@ -0,0 +1,232 @@
1
+ import { buildBillingHeaderValue, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, signRequestBody, } from '@cortexkit/anthropic-auth-core';
2
+ const CLAUDE_CODE_TOOLS = new Map([
3
+ 'Read',
4
+ 'Write',
5
+ 'Edit',
6
+ 'Bash',
7
+ 'Grep',
8
+ 'Glob',
9
+ 'AskUserQuestion',
10
+ 'TodoWrite',
11
+ 'WebFetch',
12
+ 'WebSearch',
13
+ ].map((name) => [name.toLowerCase(), name]));
14
+ function sanitize(text) {
15
+ return text.replace(/[\uD800-\uDFFF]/g, '\uFFFD');
16
+ }
17
+ function toClaudeCodeToolName(name) {
18
+ return CLAUDE_CODE_TOOLS.get(name.toLowerCase()) ?? name;
19
+ }
20
+ export function fromClaudeCodeToolName(name, tools) {
21
+ const lower = name.toLowerCase();
22
+ return tools?.find((tool) => tool.name.toLowerCase() === lower)?.name ?? name;
23
+ }
24
+ function convertTextAndImages(content) {
25
+ if (!content.some((item) => item.type === 'image')) {
26
+ return content
27
+ .filter((item) => item.type === 'text')
28
+ .map((item) => sanitize(item.text))
29
+ .join('\n');
30
+ }
31
+ const blocks = content.map((item) => {
32
+ if (item.type === 'text') {
33
+ return { type: 'text', text: sanitize(item.text) };
34
+ }
35
+ return {
36
+ type: 'image',
37
+ source: {
38
+ type: 'base64',
39
+ media_type: item.mimeType,
40
+ data: item.data,
41
+ },
42
+ };
43
+ });
44
+ if (!blocks.some((block) => block.type === 'text')) {
45
+ blocks.unshift({ type: 'text', text: '(see attached image)' });
46
+ }
47
+ return blocks;
48
+ }
49
+ function convertMessages(messages) {
50
+ const result = [];
51
+ for (let index = 0; index < messages.length; index++) {
52
+ const message = messages[index];
53
+ if (!message)
54
+ continue;
55
+ if (message.role === 'user') {
56
+ if (typeof message.content === 'string') {
57
+ if (message.content.trim()) {
58
+ result.push({ role: 'user', content: sanitize(message.content) });
59
+ }
60
+ }
61
+ else {
62
+ result.push({
63
+ role: 'user',
64
+ content: convertTextAndImages(message.content),
65
+ });
66
+ }
67
+ continue;
68
+ }
69
+ if (message.role === 'assistant') {
70
+ const blocks = [];
71
+ for (const block of message.content) {
72
+ if (block.type === 'text' && block.text.trim()) {
73
+ blocks.push({ type: 'text', text: sanitize(block.text) });
74
+ }
75
+ else if (block.type === 'thinking' && block.thinking.trim()) {
76
+ const thinking = block;
77
+ if (thinking.thinkingSignature) {
78
+ blocks.push({
79
+ type: 'thinking',
80
+ thinking: sanitize(thinking.thinking),
81
+ signature: thinking.thinkingSignature,
82
+ });
83
+ }
84
+ else {
85
+ blocks.push({ type: 'text', text: sanitize(thinking.thinking) });
86
+ }
87
+ }
88
+ else if (block.type === 'toolCall') {
89
+ blocks.push({
90
+ type: 'tool_use',
91
+ id: block.id,
92
+ name: toClaudeCodeToolName(block.name),
93
+ input: block.arguments,
94
+ });
95
+ }
96
+ }
97
+ if (blocks.length)
98
+ result.push({ role: 'assistant', content: blocks });
99
+ continue;
100
+ }
101
+ if (message.role === 'toolResult') {
102
+ const toolResult = message;
103
+ const toolResults = [];
104
+ toolResults.push({
105
+ type: 'tool_result',
106
+ tool_use_id: toolResult.toolCallId,
107
+ content: convertTextAndImages(toolResult.content),
108
+ is_error: toolResult.isError,
109
+ });
110
+ let nextIndex = index + 1;
111
+ while (nextIndex < messages.length &&
112
+ messages[nextIndex]?.role === 'toolResult') {
113
+ const next = messages[nextIndex];
114
+ toolResults.push({
115
+ type: 'tool_result',
116
+ tool_use_id: next.toolCallId,
117
+ content: convertTextAndImages(next.content),
118
+ is_error: next.isError,
119
+ });
120
+ nextIndex += 1;
121
+ }
122
+ index = nextIndex - 1;
123
+ result.push({ role: 'user', content: toolResults });
124
+ }
125
+ }
126
+ return result;
127
+ }
128
+ function convertTools(tools) {
129
+ if (!tools?.length)
130
+ return undefined;
131
+ return tools.map((tool) => ({
132
+ name: toClaudeCodeToolName(tool.name),
133
+ description: tool.description,
134
+ input_schema: {
135
+ type: 'object',
136
+ properties: tool.parameters.properties ?? {},
137
+ required: tool.parameters.required ?? [],
138
+ },
139
+ }));
140
+ }
141
+ function addEphemeralCacheControl(body) {
142
+ const lastTool = body.tools?.at(-1);
143
+ if (lastTool)
144
+ lastTool.cache_control = { type: 'ephemeral' };
145
+ const lastSystem = body.system?.at(-1);
146
+ if (lastSystem)
147
+ lastSystem.cache_control = { type: 'ephemeral' };
148
+ for (let index = body.messages.length - 1; index >= 0; index--) {
149
+ const message = body.messages[index];
150
+ if (message?.role !== 'user')
151
+ continue;
152
+ const content = message.content;
153
+ if (Array.isArray(content)) {
154
+ const lastBlock = content.at(-1);
155
+ if (lastBlock && typeof lastBlock === 'object') {
156
+ lastBlock.cache_control = { type: 'ephemeral' };
157
+ }
158
+ }
159
+ break;
160
+ }
161
+ }
162
+ function applyCacheMode(body, enabled, mode) {
163
+ if (!enabled)
164
+ return;
165
+ if (mode === 'automatic') {
166
+ body.cache_control = { type: 'ephemeral' };
167
+ return;
168
+ }
169
+ const addTtl = (value) => {
170
+ if (!value || typeof value !== 'object')
171
+ return;
172
+ if (Array.isArray(value)) {
173
+ for (const item of value)
174
+ addTtl(item);
175
+ return;
176
+ }
177
+ const record = value;
178
+ const cacheControl = record.cache_control;
179
+ if (cacheControl && typeof cacheControl === 'object') {
180
+ ;
181
+ cacheControl.ttl = '1h';
182
+ }
183
+ for (const child of Object.values(record))
184
+ addTtl(child);
185
+ };
186
+ if (mode === 'hybrid')
187
+ body.cache_control = { type: 'ephemeral' };
188
+ addTtl(body);
189
+ }
190
+ export async function buildAnthropicRequest(modelId, context, options, cache) {
191
+ const messages = convertMessages(context.messages);
192
+ const system = [
193
+ {
194
+ type: 'text',
195
+ text: buildBillingHeaderValue(messages, undefined, CLAUDE_CODE_ENTRYPOINT),
196
+ },
197
+ { type: 'text', text: CLAUDE_CODE_IDENTITY },
198
+ ];
199
+ if (context.systemPrompt?.trim()) {
200
+ system.push({ type: 'text', text: sanitize(context.systemPrompt) });
201
+ }
202
+ const body = {
203
+ model: modelId,
204
+ max_tokens: options?.maxTokens ?? 16_384,
205
+ stream: true,
206
+ system,
207
+ messages,
208
+ };
209
+ const tools = convertTools(context.tools);
210
+ if (tools?.length)
211
+ body.tools = tools;
212
+ if (options?.reasoning) {
213
+ const budgets = {
214
+ minimal: 1024,
215
+ low: 4096,
216
+ medium: 10_240,
217
+ high: 20_480,
218
+ xhigh: 32_000,
219
+ };
220
+ body.thinking = {
221
+ type: 'enabled',
222
+ budget_tokens: options.thinkingBudgets?.[options.reasoning] ??
223
+ budgets[options.reasoning] ??
224
+ 10_240,
225
+ };
226
+ }
227
+ addEphemeralCacheControl(body);
228
+ applyCacheMode(body, cache.enabled, cache.mode);
229
+ const unsigned = JSON.stringify(body);
230
+ const bodyText = await signRequestBody(unsigned);
231
+ return { body, bodyText };
232
+ }
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
2
+ export default function cortexKitPiAnthropicAuth(pi: ExtensionAPI): void;
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ import { authorize, CLIENT_ID, exchange, TOKEN_URL, } from '@cortexkit/anthropic-auth-core';
2
+ import { registerCommands } from "./commands.js";
3
+ import { streamCortexKitAnthropic } from "./stream.js";
4
+ async function loginAnthropic(callbacks) {
5
+ const auth = await authorize('max');
6
+ callbacks.onAuth({ url: auth.url });
7
+ const callback = await callbacks.onPrompt({
8
+ message: 'Paste the Claude OAuth callback URL or code:',
9
+ });
10
+ const result = await exchange(callback, auth.verifier, auth.redirectUri, auth.state);
11
+ if (result.type !== 'success') {
12
+ throw new Error('Anthropic OAuth exchange failed');
13
+ }
14
+ return {
15
+ refresh: result.refresh,
16
+ access: result.access,
17
+ expires: result.expires,
18
+ };
19
+ }
20
+ async function refreshAnthropicToken(credentials) {
21
+ const response = await fetch(TOKEN_URL, {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ Accept: 'application/json, text/plain, */*',
26
+ 'User-Agent': 'axios/1.13.6',
27
+ },
28
+ body: JSON.stringify({
29
+ grant_type: 'refresh_token',
30
+ client_id: CLIENT_ID,
31
+ refresh_token: credentials.refresh,
32
+ }),
33
+ });
34
+ if (!response.ok) {
35
+ throw new Error(`Anthropic OAuth refresh failed: ${await response.text()}`);
36
+ }
37
+ const json = (await response.json());
38
+ return {
39
+ refresh: json.refresh_token ?? credentials.refresh,
40
+ access: json.access_token,
41
+ expires: Date.now() + json.expires_in * 1000,
42
+ };
43
+ }
44
+ export default function cortexKitPiAnthropicAuth(pi) {
45
+ registerCommands(pi);
46
+ pi.registerProvider('anthropic', {
47
+ name: 'Anthropic (CortexKit OAuth)',
48
+ baseUrl: 'https://api.anthropic.com',
49
+ api: 'cortexkit-anthropic-messages',
50
+ models: [
51
+ {
52
+ id: 'claude-opus-4-5',
53
+ name: 'Claude Opus 4.5',
54
+ reasoning: true,
55
+ input: ['text', 'image'],
56
+ cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
57
+ contextWindow: 200_000,
58
+ maxTokens: 64_000,
59
+ },
60
+ {
61
+ id: 'claude-sonnet-4-5',
62
+ name: 'Claude Sonnet 4.5',
63
+ reasoning: true,
64
+ input: ['text', 'image'],
65
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
66
+ contextWindow: 200_000,
67
+ maxTokens: 64_000,
68
+ },
69
+ ],
70
+ oauth: {
71
+ name: 'Anthropic Claude Pro/Max (CortexKit)',
72
+ login: loginAnthropic,
73
+ refreshToken: refreshAnthropicToken,
74
+ getApiKey: (credentials) => credentials.access,
75
+ },
76
+ streamSimple: streamCortexKitAnthropic,
77
+ });
78
+ }
@@ -0,0 +1,2 @@
1
+ export declare function getPiConfigDir(): string;
2
+ export declare function getPiAccountStoragePath(): string;
package/dist/paths.js ADDED
@@ -0,0 +1,10 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { ACCOUNT_FILE_NAME } from '@cortexkit/anthropic-auth-core';
4
+ export function getPiConfigDir() {
5
+ return process.env.PI_AGENT_DIR?.trim() || join(homedir(), '.pi', 'agent');
6
+ }
7
+ export function getPiAccountStoragePath() {
8
+ return (process.env.PI_ANTHROPIC_AUTH_FILE?.trim() ||
9
+ join(getPiConfigDir(), ACCOUNT_FILE_NAME));
10
+ }
@@ -0,0 +1,2 @@
1
+ import { type Api, type AssistantMessageEventStream, type Context, type Model, type SimpleStreamOptions } from '@mariozechner/pi-ai';
2
+ export declare function streamCortexKitAnthropic(model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream;
package/dist/stream.js ADDED
@@ -0,0 +1,348 @@
1
+ import { FallbackAccountManager, getCache1hPersistentMode, getRelayConfig, isCache1hPersistentlyEnabled, loadAccounts, REQUIRED_BETAS, sendViaRelay, shouldFallbackStatus, USER_AGENT, } from '@cortexkit/anthropic-auth-core';
2
+ import { calculateCost, createAssistantMessageEventStream, } from '@mariozechner/pi-ai';
3
+ import { buildAnthropicRequest, fromClaudeCodeToolName } from "./convert.js";
4
+ import { getPiAccountStoragePath } from "./paths.js";
5
+ function mapStopReason(reason) {
6
+ switch (reason) {
7
+ case 'end_turn':
8
+ case 'pause_turn':
9
+ case 'stop_sequence':
10
+ return 'stop';
11
+ case 'max_tokens':
12
+ return 'length';
13
+ case 'tool_use':
14
+ return 'toolUse';
15
+ default:
16
+ return 'error';
17
+ }
18
+ }
19
+ function createOutput(model) {
20
+ return {
21
+ role: 'assistant',
22
+ content: [],
23
+ api: model.api,
24
+ provider: model.provider,
25
+ model: model.id,
26
+ usage: {
27
+ input: 0,
28
+ output: 0,
29
+ cacheRead: 0,
30
+ cacheWrite: 0,
31
+ totalTokens: 0,
32
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
33
+ },
34
+ stopReason: 'stop',
35
+ timestamp: Date.now(),
36
+ };
37
+ }
38
+ function updateUsage(model, output, usage) {
39
+ if (!usage)
40
+ return;
41
+ output.usage.input = usage.input_tokens ?? output.usage.input;
42
+ output.usage.output = usage.output_tokens ?? output.usage.output;
43
+ output.usage.cacheRead =
44
+ usage.cache_read_input_tokens ?? output.usage.cacheRead;
45
+ output.usage.cacheWrite =
46
+ usage.cache_creation_input_tokens ?? output.usage.cacheWrite;
47
+ output.usage.totalTokens =
48
+ output.usage.input +
49
+ output.usage.output +
50
+ output.usage.cacheRead +
51
+ output.usage.cacheWrite;
52
+ calculateCost(model, output.usage);
53
+ }
54
+ async function* parseSse(response) {
55
+ if (!response.body)
56
+ return;
57
+ const reader = response.body.getReader();
58
+ const decoder = new TextDecoder();
59
+ let buffer = '';
60
+ while (true) {
61
+ const { done, value } = await reader.read();
62
+ if (done)
63
+ break;
64
+ buffer += decoder.decode(value, { stream: true });
65
+ let boundary = buffer.indexOf('\n\n');
66
+ while (boundary !== -1) {
67
+ const frame = buffer.slice(0, boundary);
68
+ buffer = buffer.slice(boundary + 2);
69
+ boundary = buffer.indexOf('\n\n');
70
+ for (const line of frame.split('\n')) {
71
+ if (!line.startsWith('data:'))
72
+ continue;
73
+ const data = line.slice(5).trim();
74
+ if (!data || data === '[DONE]')
75
+ continue;
76
+ yield JSON.parse(data);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ async function sendAnthropicRequest(options) {
82
+ const storage = await loadAccounts(options.storagePath);
83
+ const headers = new Headers({
84
+ accept: 'application/json',
85
+ 'content-type': 'application/json',
86
+ authorization: `Bearer ${options.accessToken}`,
87
+ 'anthropic-beta': REQUIRED_BETAS.join(','),
88
+ 'user-agent': USER_AGENT,
89
+ 'x-app': 'cli',
90
+ });
91
+ if (options.streamOptions?.sessionId) {
92
+ headers.set('x-session-affinity', options.streamOptions.sessionId);
93
+ }
94
+ const input = new URL('/v1/messages?beta=true', options.model.baseUrl);
95
+ const init = {
96
+ method: 'POST',
97
+ headers,
98
+ body: options.bodyText,
99
+ signal: options.streamOptions?.signal,
100
+ };
101
+ return sendViaRelay({
102
+ config: getRelayConfig(storage),
103
+ input,
104
+ init,
105
+ headers,
106
+ body: options.bodyText,
107
+ fallback: () => fetch(input, init),
108
+ });
109
+ }
110
+ async function firstStreamingError(response) {
111
+ if (!response.ok)
112
+ return response;
113
+ const clone = response.clone();
114
+ try {
115
+ for await (const event of parseSse(clone)) {
116
+ if (event.type === 'error' &&
117
+ typeof event.delta?.type === 'string' &&
118
+ event.delta.type === 'rate_limit_error') {
119
+ return 'rate_limit_error';
120
+ }
121
+ return response;
122
+ }
123
+ }
124
+ catch {
125
+ return response;
126
+ }
127
+ return response;
128
+ }
129
+ async function executeWithFallback(options) {
130
+ const manager = new FallbackAccountManager({
131
+ configPath: options.storagePath,
132
+ });
133
+ const storage = await loadAccounts(options.storagePath);
134
+ const primary = await sendAnthropicRequest({
135
+ ...options,
136
+ accessToken: options.primaryAccessToken,
137
+ });
138
+ const primaryPreflight = await firstStreamingError(primary);
139
+ if (primaryPreflight instanceof Response) {
140
+ if (!shouldFallbackStatus(primaryPreflight.status, storage))
141
+ return primaryPreflight;
142
+ }
143
+ for (const account of await manager.getUsableFallbackAccounts()) {
144
+ if (!account.access)
145
+ continue;
146
+ const response = await sendAnthropicRequest({
147
+ ...options,
148
+ accessToken: account.access,
149
+ });
150
+ const preflight = await firstStreamingError(response);
151
+ if (preflight instanceof Response && preflight.ok) {
152
+ await manager.markUsed(account);
153
+ return preflight;
154
+ }
155
+ if (preflight instanceof Response &&
156
+ !shouldFallbackStatus(preflight.status, storage)) {
157
+ return preflight;
158
+ }
159
+ }
160
+ return primaryPreflight instanceof Response ? primaryPreflight : primary;
161
+ }
162
+ export function streamCortexKitAnthropic(model, context, options) {
163
+ const stream = createAssistantMessageEventStream();
164
+ void (async () => {
165
+ const output = createOutput(model);
166
+ stream.push({ type: 'start', partial: output });
167
+ try {
168
+ const accessToken = options?.apiKey ?? '';
169
+ if (!accessToken)
170
+ throw new Error('Missing Anthropic OAuth access token');
171
+ const storagePath = getPiAccountStoragePath();
172
+ const storage = await loadAccounts(storagePath);
173
+ const { bodyText } = await buildAnthropicRequest(model.id, context, options, {
174
+ enabled: isCache1hPersistentlyEnabled(storage),
175
+ mode: getCache1hPersistentMode(storage),
176
+ });
177
+ const response = await executeWithFallback({
178
+ model,
179
+ context,
180
+ streamOptions: options,
181
+ bodyText,
182
+ primaryAccessToken: accessToken,
183
+ storagePath,
184
+ });
185
+ if (!response.ok) {
186
+ throw new Error(`Anthropic request failed: HTTP ${response.status} ${await response.text()}`);
187
+ }
188
+ const blocks = output.content;
189
+ for await (const event of parseSse(response)) {
190
+ if (event.type === 'message_start') {
191
+ updateUsage(model, output, event.message?.usage);
192
+ }
193
+ else if (event.type === 'content_block_start') {
194
+ const block = event.content_block;
195
+ if (block?.type === 'text') {
196
+ output.content.push({
197
+ type: 'text',
198
+ text: '',
199
+ index: event.index,
200
+ });
201
+ stream.push({
202
+ type: 'text_start',
203
+ contentIndex: output.content.length - 1,
204
+ partial: output,
205
+ });
206
+ }
207
+ else if (block?.type === 'thinking') {
208
+ output.content.push({
209
+ type: 'thinking',
210
+ thinking: '',
211
+ thinkingSignature: '',
212
+ index: event.index,
213
+ });
214
+ stream.push({
215
+ type: 'thinking_start',
216
+ contentIndex: output.content.length - 1,
217
+ partial: output,
218
+ });
219
+ }
220
+ else if (block?.type === 'tool_use') {
221
+ output.content.push({
222
+ type: 'toolCall',
223
+ id: String(block.id),
224
+ name: fromClaudeCodeToolName(String(block.name), context.tools),
225
+ arguments: {},
226
+ partialJson: '',
227
+ index: event.index,
228
+ });
229
+ stream.push({
230
+ type: 'toolcall_start',
231
+ contentIndex: output.content.length - 1,
232
+ partial: output,
233
+ });
234
+ }
235
+ }
236
+ else if (event.type === 'content_block_delta') {
237
+ const contentIndex = blocks.findIndex((block) => block.index === event.index);
238
+ const block = blocks[contentIndex];
239
+ if (!block || !event.delta)
240
+ continue;
241
+ if (event.delta.type === 'text_delta' && block.type === 'text') {
242
+ const delta = String(event.delta.text ?? '');
243
+ block.text += delta;
244
+ stream.push({
245
+ type: 'text_delta',
246
+ contentIndex,
247
+ delta,
248
+ partial: output,
249
+ });
250
+ }
251
+ else if (event.delta.type === 'thinking_delta' &&
252
+ block.type === 'thinking') {
253
+ const delta = String(event.delta.thinking ?? '');
254
+ block.thinking += delta;
255
+ stream.push({
256
+ type: 'thinking_delta',
257
+ contentIndex,
258
+ delta,
259
+ partial: output,
260
+ });
261
+ }
262
+ else if (event.delta.type === 'signature_delta' &&
263
+ block.type === 'thinking') {
264
+ block.thinkingSignature = `${block.thinkingSignature ?? ''}${String(event.delta.signature ?? '')}`;
265
+ }
266
+ else if (event.delta.type === 'input_json_delta' &&
267
+ block.type === 'toolCall') {
268
+ const delta = String(event.delta.partial_json ?? '');
269
+ block.partialJson = `${block.partialJson ?? ''}${delta}`;
270
+ try {
271
+ block.arguments = JSON.parse(block.partialJson);
272
+ }
273
+ catch { }
274
+ stream.push({
275
+ type: 'toolcall_delta',
276
+ contentIndex,
277
+ delta,
278
+ partial: output,
279
+ });
280
+ }
281
+ }
282
+ else if (event.type === 'content_block_stop') {
283
+ const contentIndex = blocks.findIndex((block) => block.index === event.index);
284
+ const block = blocks[contentIndex];
285
+ if (!block)
286
+ continue;
287
+ delete block.index;
288
+ if (block.type === 'text') {
289
+ stream.push({
290
+ type: 'text_end',
291
+ contentIndex,
292
+ content: block.text,
293
+ partial: output,
294
+ });
295
+ }
296
+ else if (block.type === 'thinking') {
297
+ stream.push({
298
+ type: 'thinking_end',
299
+ contentIndex,
300
+ content: block.thinking,
301
+ partial: output,
302
+ });
303
+ }
304
+ else if (block.type === 'toolCall') {
305
+ try {
306
+ block.arguments = JSON.parse(block.partialJson ?? '{}');
307
+ }
308
+ catch { }
309
+ delete block.partialJson;
310
+ stream.push({
311
+ type: 'toolcall_end',
312
+ contentIndex,
313
+ toolCall: block,
314
+ partial: output,
315
+ });
316
+ }
317
+ }
318
+ else if (event.type === 'message_delta') {
319
+ output.stopReason = mapStopReason(String(event.delta?.stop_reason ?? ''));
320
+ updateUsage(model, output, event.usage);
321
+ }
322
+ else if (event.type === 'error') {
323
+ throw new Error(JSON.stringify(event));
324
+ }
325
+ }
326
+ if (options?.signal?.aborted)
327
+ throw new Error('Request was aborted');
328
+ for (const block of output.content)
329
+ delete block.index;
330
+ stream.push({
331
+ type: 'done',
332
+ reason: output.stopReason,
333
+ message: output,
334
+ });
335
+ stream.end();
336
+ }
337
+ catch (error) {
338
+ for (const block of output.content)
339
+ delete block.index;
340
+ output.stopReason = options?.signal?.aborted ? 'aborted' : 'error';
341
+ output.errorMessage =
342
+ error instanceof Error ? error.message : String(error);
343
+ stream.push({ type: 'error', reason: output.stopReason, error: output });
344
+ stream.end();
345
+ }
346
+ })();
347
+ return stream;
348
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@cortexkit/pi-anthropic-auth",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/cortexkit/anthropic-auth.git",
8
+ "directory": "packages/pi"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "keywords": [
24
+ "pi-package"
25
+ ],
26
+ "pi": {
27
+ "extensions": [
28
+ "./dist/index.js"
29
+ ]
30
+ },
31
+ "scripts": {
32
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
33
+ "typecheck": "tsc",
34
+ "types": "tsc",
35
+ "prepublishOnly": "bun run build"
36
+ },
37
+ "dependencies": {
38
+ "@cortexkit/anthropic-auth-core": "1.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "@mariozechner/pi-ai": "*",
42
+ "@mariozechner/pi-coding-agent": "*"
43
+ }
44
+ }