pi-anthropic-oauth 0.1.17 → 0.2.1
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 +6 -1
- package/package.json +5 -5
- package/src/convert.ts +28 -0
- package/src/index.ts +0 -53
- package/src/stream.ts +27 -13
package/README.md
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
Use Claude Pro/Max in Pi with browser OAuth.
|
|
6
6
|
|
|
7
|
+
> [!IMPORTANT]
|
|
8
|
+
> This release requires Pi 0.80.8 or later in the 0.80.x release line.
|
|
9
|
+
|
|
7
10
|
## Features
|
|
8
11
|
|
|
9
12
|
- Claude Pro/Max login from `/login`
|
|
@@ -34,6 +37,8 @@ Claude Pro/Max
|
|
|
34
37
|
|
|
35
38
|
Complete the browser login. After login, select an Anthropic model in Pi.
|
|
36
39
|
|
|
40
|
+
When the extension is active, `/login` includes `Claude Pro/Max` and Anthropic models show `(sub)` after OAuth login.
|
|
41
|
+
|
|
37
42
|
Update with:
|
|
38
43
|
|
|
39
44
|
```bash
|
|
@@ -96,7 +101,7 @@ To add another Anthropic model, create `~/.pi/agent/models.json`:
|
|
|
96
101
|
```
|
|
97
102
|
|
|
98
103
|
> [!NOTE]
|
|
99
|
-
> Opus 4.8 and Fable 5 are
|
|
104
|
+
> Opus 4.8 and Fable 5 are included in Pi's built-in Anthropic model catalog.
|
|
100
105
|
>
|
|
101
106
|
> Pi requires `baseUrl`, `apiKey`, and `api` when defining custom models in `models.json`. With this extension, requests normally authenticate through Claude Pro/Max OAuth after `/login`, so `apiKey` is only a placeholder to satisfy Pi's config requirements and does not need to be a valid Anthropic API key.
|
|
102
107
|
>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anthropic-oauth",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Claude Pro/Max OAuth extension for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"@anthropic-ai/sdk": "^0.52.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-ai": "
|
|
42
|
-
"@earendil-works/pi-coding-agent": "
|
|
41
|
+
"@earendil-works/pi-ai": "^0.80.8",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "^0.80.8"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@earendil-works/pi-ai": "^0.
|
|
46
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
45
|
+
"@earendil-works/pi-ai": "^0.80.8",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "^0.80.8",
|
|
47
47
|
"typescript": "^5.9.3"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/convert.ts
CHANGED
|
@@ -11,8 +11,10 @@ type ToolResultContentBlock =
|
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
13
|
import type {
|
|
14
|
+
Api,
|
|
14
15
|
ImageContent,
|
|
15
16
|
Message,
|
|
17
|
+
Model,
|
|
16
18
|
TextContent,
|
|
17
19
|
ThinkingContent,
|
|
18
20
|
Tool,
|
|
@@ -52,6 +54,7 @@ export function fromClaudeCodeToolName(name: string, tools?: Tool[]): string {
|
|
|
52
54
|
export function convertPiMessagesToAnthropic(
|
|
53
55
|
messages: Message[],
|
|
54
56
|
isOAuth: boolean,
|
|
57
|
+
model: Pick<Model<Api>, "provider" | "api" | "id">,
|
|
55
58
|
): MessageParam[] {
|
|
56
59
|
const params: MessageParam[] = [];
|
|
57
60
|
const toolIdMap = new Map<string, string>();
|
|
@@ -131,9 +134,34 @@ export function convertPiMessagesToAnthropic(
|
|
|
131
134
|
|
|
132
135
|
const blocks: ContentBlockParam[] = [];
|
|
133
136
|
const emittedToolUseIds: string[] = [];
|
|
137
|
+
const isSameModel =
|
|
138
|
+
message.provider === model.provider &&
|
|
139
|
+
message.api === model.api &&
|
|
140
|
+
message.model === model.id;
|
|
134
141
|
for (const block of message.content) {
|
|
135
142
|
if (block.type === "text" && block.text.trim()) {
|
|
136
143
|
blocks.push({ type: "text", text: sanitizeSurrogates(block.text) });
|
|
144
|
+
} else if (block.type === "thinking") {
|
|
145
|
+
const thinkingSignature = block.thinkingSignature?.trim();
|
|
146
|
+
if (block.redacted) {
|
|
147
|
+
if (isSameModel && thinkingSignature) {
|
|
148
|
+
blocks.push({
|
|
149
|
+
type: "redacted_thinking",
|
|
150
|
+
data: block.thinkingSignature!,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
} else if (isSameModel && thinkingSignature) {
|
|
154
|
+
blocks.push({
|
|
155
|
+
type: "thinking",
|
|
156
|
+
thinking: sanitizeSurrogates(block.thinking),
|
|
157
|
+
signature: block.thinkingSignature!,
|
|
158
|
+
});
|
|
159
|
+
} else if (block.thinking.trim()) {
|
|
160
|
+
blocks.push({
|
|
161
|
+
type: "text",
|
|
162
|
+
text: sanitizeSurrogates(block.thinking),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
137
165
|
} else if (block.type === "toolCall") {
|
|
138
166
|
const anthropicId = getAnthropicToolId(block.id);
|
|
139
167
|
blocks.push({
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,6 @@ import { existsSync, symlinkSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import {
|
|
5
|
-
AuthStorage,
|
|
6
|
-
ModelRegistry,
|
|
7
5
|
type ExtensionAPI,
|
|
8
6
|
type ProviderConfig,
|
|
9
7
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -11,33 +9,6 @@ import type { OAuthCredentials } from "@earendil-works/pi-ai";
|
|
|
11
9
|
import { loginAnthropic, refreshAnthropicToken } from "./auth.js";
|
|
12
10
|
import { streamAnthropicOAuth } from "./stream.js";
|
|
13
11
|
|
|
14
|
-
const DEFAULT_MODELS: NonNullable<ProviderConfig["models"]> = [
|
|
15
|
-
{
|
|
16
|
-
id: "claude-opus-4-8",
|
|
17
|
-
name: "Claude Opus 4.8",
|
|
18
|
-
api: "anthropic-messages",
|
|
19
|
-
reasoning: true,
|
|
20
|
-
thinkingLevelMap: { xhigh: "xhigh" },
|
|
21
|
-
input: ["text", "image"],
|
|
22
|
-
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
23
|
-
contextWindow: 1000000,
|
|
24
|
-
maxTokens: 128000,
|
|
25
|
-
compat: undefined,
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: "claude-fable-5",
|
|
29
|
-
name: "Claude Fable 5",
|
|
30
|
-
api: "anthropic-messages",
|
|
31
|
-
reasoning: true,
|
|
32
|
-
thinkingLevelMap: { xhigh: "xhigh" },
|
|
33
|
-
input: ["text", "image"],
|
|
34
|
-
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
|
35
|
-
contextWindow: 1000000,
|
|
36
|
-
maxTokens: 128000,
|
|
37
|
-
compat: undefined,
|
|
38
|
-
},
|
|
39
|
-
];
|
|
40
|
-
|
|
41
12
|
function ensureClaudeCodeSymlink() {
|
|
42
13
|
const target = join(homedir(), ".pi");
|
|
43
14
|
const link = join(homedir(), ".Claude Code");
|
|
@@ -48,36 +19,12 @@ function ensureClaudeCodeSymlink() {
|
|
|
48
19
|
}
|
|
49
20
|
}
|
|
50
21
|
|
|
51
|
-
function getAnthropicModels(): NonNullable<ProviderConfig["models"]> {
|
|
52
|
-
const modelRegistry = ModelRegistry.create(AuthStorage.inMemory());
|
|
53
|
-
const models: NonNullable<ProviderConfig["models"]> = modelRegistry
|
|
54
|
-
.getAll()
|
|
55
|
-
.filter((model) => model.provider === "anthropic")
|
|
56
|
-
// Spread the full registry model so every field (thinkingLevelMap,
|
|
57
|
-
// baseUrl, headers, compat, and anything added in future) propagates
|
|
58
|
-
// automatically instead of being silently dropped by a hand-picked list.
|
|
59
|
-
.map((model) => ({
|
|
60
|
-
...model,
|
|
61
|
-
api: model.api ?? "anthropic-messages",
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
for (const defaultModel of DEFAULT_MODELS) {
|
|
65
|
-
if (!models.some((model) => model.id === defaultModel.id)) {
|
|
66
|
-
models.push(defaultModel);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return models;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
22
|
export default function (pi: ExtensionAPI) {
|
|
74
23
|
ensureClaudeCodeSymlink();
|
|
75
|
-
const models = getAnthropicModels();
|
|
76
24
|
|
|
77
25
|
pi.registerProvider("anthropic", {
|
|
78
26
|
baseUrl: "https://api.anthropic.com",
|
|
79
27
|
api: "anthropic-messages",
|
|
80
|
-
models,
|
|
81
28
|
oauth: {
|
|
82
29
|
name: "Claude Pro/Max",
|
|
83
30
|
usesCallbackServer: true,
|
package/src/stream.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
type Model,
|
|
11
11
|
type SimpleStreamOptions,
|
|
12
12
|
type StopReason,
|
|
13
|
-
streamSimpleAnthropic,
|
|
14
13
|
} from "@earendil-works/pi-ai";
|
|
15
14
|
import { isClaudeOAuthAccessToken, USER_AGENT } from "./auth.js";
|
|
16
15
|
import {
|
|
@@ -82,7 +81,11 @@ function makeDefaultHeaders(
|
|
|
82
81
|
) {
|
|
83
82
|
continue;
|
|
84
83
|
}
|
|
85
|
-
|
|
84
|
+
const existingKey = Object.keys(headers).find(
|
|
85
|
+
(header) => header.toLowerCase() === normalizedKey,
|
|
86
|
+
);
|
|
87
|
+
if (existingKey) delete headers[existingKey];
|
|
88
|
+
if (value !== null) headers[key] = value;
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -94,16 +97,6 @@ export function streamAnthropicOAuth(
|
|
|
94
97
|
context: Context,
|
|
95
98
|
options?: SimpleStreamOptions,
|
|
96
99
|
): AssistantMessageEventStream {
|
|
97
|
-
// pi-ai keys streamSimple by `api`, so this runs for every anthropic-messages
|
|
98
|
-
// model (copilot, openrouter, …). Delegate non-anthropic providers.
|
|
99
|
-
if (model.provider !== "anthropic") {
|
|
100
|
-
return streamSimpleAnthropic(
|
|
101
|
-
model as Model<"anthropic-messages">,
|
|
102
|
-
context,
|
|
103
|
-
options,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
100
|
const stream = createAssistantMessageEventStream();
|
|
108
101
|
|
|
109
102
|
void (async () => {
|
|
@@ -151,7 +144,7 @@ export function streamAnthropicOAuth(
|
|
|
151
144
|
|
|
152
145
|
const params: MessageCreateParamsStreaming = {
|
|
153
146
|
model: model.id,
|
|
154
|
-
messages: convertPiMessagesToAnthropic(context.messages, isOAuth),
|
|
147
|
+
messages: convertPiMessagesToAnthropic(context.messages, isOAuth, model),
|
|
155
148
|
max_tokens: maxTokens,
|
|
156
149
|
stream: true,
|
|
157
150
|
};
|
|
@@ -255,6 +248,19 @@ export function streamAnthropicOAuth(
|
|
|
255
248
|
contentIndex: output.content.length - 1,
|
|
256
249
|
partial: output,
|
|
257
250
|
});
|
|
251
|
+
} else if (event.content_block.type === "redacted_thinking") {
|
|
252
|
+
output.content.push({
|
|
253
|
+
type: "thinking",
|
|
254
|
+
thinking: "[Reasoning redacted]",
|
|
255
|
+
thinkingSignature: event.content_block.data,
|
|
256
|
+
redacted: true,
|
|
257
|
+
index: event.index,
|
|
258
|
+
} as IndexedBlock);
|
|
259
|
+
stream.push({
|
|
260
|
+
type: "thinking_start",
|
|
261
|
+
contentIndex: output.content.length - 1,
|
|
262
|
+
partial: output,
|
|
263
|
+
});
|
|
258
264
|
} else if (event.content_block.type === "tool_use") {
|
|
259
265
|
output.content.push({
|
|
260
266
|
type: "toolCall",
|
|
@@ -385,6 +391,14 @@ export function streamAnthropicOAuth(
|
|
|
385
391
|
output.usage.cacheWrite =
|
|
386
392
|
(event.usage as { cache_creation_input_tokens?: number })
|
|
387
393
|
.cache_creation_input_tokens || 0;
|
|
394
|
+
const thinkingTokens = (
|
|
395
|
+
event.usage as {
|
|
396
|
+
output_tokens_details?: { thinking_tokens?: number };
|
|
397
|
+
}
|
|
398
|
+
).output_tokens_details?.thinking_tokens;
|
|
399
|
+
if (thinkingTokens != null) {
|
|
400
|
+
output.usage.reasoning = thinkingTokens;
|
|
401
|
+
}
|
|
388
402
|
output.usage.totalTokens =
|
|
389
403
|
output.usage.input +
|
|
390
404
|
output.usage.output +
|