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 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 registered automatically. Fable 5 works on Pro/Max through July 7, 2026; after that Anthropic says continued use requires extra usage credits unless extended.
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.17",
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.74.0",
46
- "@earendil-works/pi-coding-agent": "^0.74.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
- headers[key] = value;
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 +