pi-anthropic-oauth 0.1.9 → 0.1.11
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 +23 -11
- package/package.json +1 -1
- package/src/convert.ts +76 -13
- package/src/stream.ts +11 -0
package/README.md
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-anthropic-oauth
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/pi-anthropic-oauth) [](https://nodejs.org)
|
|
4
4
|
|
|
5
|
-
Claude Pro/Max
|
|
5
|
+
Use Claude Pro/Max in Pi with browser OAuth.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Claude Pro/Max login from `/login anthropic`
|
|
10
|
+
- Automatic token refresh
|
|
11
|
+
- Claude Code-compatible OAuth headers and prompt shaping
|
|
12
|
+
- No API key or extra usage needed.
|
|
13
|
+
- Uses Pi's Anthropic model registry
|
|
14
|
+
- Adds Claude Opus 4.7 by default
|
|
15
|
+
- Auto-creates `~/.Claude Code` → `~/.pi` symlink when missing
|
|
9
16
|
|
|
10
|
-
##
|
|
17
|
+
## Quick start
|
|
11
18
|
|
|
12
19
|
```bash
|
|
13
20
|
pi install npm:pi-anthropic-oauth
|
|
@@ -25,14 +32,13 @@ Choose:
|
|
|
25
32
|
Claude Pro/Max
|
|
26
33
|
```
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
> [!WARNING]
|
|
36
|
+
> Use at your own risk. This may go against Anthropic's terms.
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- rewrites Pi system identity where needed
|
|
33
|
-
- auto-creates `~/.Claude Code` → `~/.pi` symlink
|
|
38
|
+
> [!NOTE]
|
|
39
|
+
> Anthropic auth changes are closely monitored for quick compatibility updates.
|
|
34
40
|
|
|
35
|
-
##
|
|
41
|
+
## Extra models
|
|
36
42
|
|
|
37
43
|
To add another Anthropic model, create `~/.pi/agent/models.json`:
|
|
38
44
|
|
|
@@ -57,6 +63,12 @@ To add another Anthropic model, create `~/.pi/agent/models.json`:
|
|
|
57
63
|
> [!NOTE]
|
|
58
64
|
> 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 anthropic`, so `apiKey` is present to satisfy Pi's config requirements and does not need to be a valid Anthropic API key.
|
|
59
65
|
|
|
66
|
+
## Troubleshooting
|
|
67
|
+
|
|
68
|
+
- Re-run `/login anthropic` if auth looks stale
|
|
69
|
+
- If local callback login does not complete, paste the final callback URL or `code#state` when prompted
|
|
70
|
+
- If something breaks, please open an issue with your Pi version, extension version, and error output
|
|
71
|
+
|
|
60
72
|
## License
|
|
61
73
|
|
|
62
74
|
MIT
|
package/package.json
CHANGED
package/src/convert.ts
CHANGED
|
@@ -73,10 +73,33 @@ export function convertPiMessagesToAnthropic(
|
|
|
73
73
|
return candidate;
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
// Anthropic requires every `tool_use` block to be IMMEDIATELY followed by a
|
|
77
|
+
// user message containing a matching `tool_result` for the same id. Aborted
|
|
78
|
+
// / errored assistant turns or partial histories can leave orphan tool_uses
|
|
79
|
+
// behind, which causes the API to reject the next request with:
|
|
80
|
+
// `tool_use` ids were found without `tool_result` blocks immediately after
|
|
81
|
+
// We track outstanding tool_use ids from the last assistant message and
|
|
82
|
+
// synthesize "No result provided" results for any that aren't satisfied by
|
|
83
|
+
// the time we emit the next user message (or finish the history).
|
|
84
|
+
let pendingToolUseIds: string[] = [];
|
|
85
|
+
|
|
86
|
+
const flushPendingToolResults = () => {
|
|
87
|
+
if (pendingToolUseIds.length === 0) return;
|
|
88
|
+
const syntheticBlocks: ContentBlockParam[] = pendingToolUseIds.map((id) => ({
|
|
89
|
+
type: "tool_result",
|
|
90
|
+
tool_use_id: id,
|
|
91
|
+
content: "No result provided",
|
|
92
|
+
is_error: true,
|
|
93
|
+
}));
|
|
94
|
+
params.push({ role: "user", content: syntheticBlocks });
|
|
95
|
+
pendingToolUseIds = [];
|
|
96
|
+
};
|
|
97
|
+
|
|
76
98
|
for (let i = 0; i < messages.length; i++) {
|
|
77
99
|
const message = messages[i];
|
|
78
100
|
|
|
79
101
|
if (message.role === "user") {
|
|
102
|
+
flushPendingToolResults();
|
|
80
103
|
if (typeof message.content === "string") {
|
|
81
104
|
if (message.content.trim()) params.push({ role: "user", content: sanitizeSurrogates(message.content) });
|
|
82
105
|
} else {
|
|
@@ -94,45 +117,85 @@ export function convertPiMessagesToAnthropic(
|
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
if (message.role === "assistant") {
|
|
120
|
+
// Skip aborted / errored assistant turns entirely. Their content may
|
|
121
|
+
// include partial tool_use blocks that will never get a matching
|
|
122
|
+
// tool_result, which would poison every subsequent request.
|
|
123
|
+
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Defensive: if the previous assistant turn had unresolved tool_uses and
|
|
128
|
+
// the next message is another assistant (shouldn't normally happen),
|
|
129
|
+
// patch them up before emitting the new assistant turn.
|
|
130
|
+
flushPendingToolResults();
|
|
131
|
+
|
|
97
132
|
const blocks: ContentBlockParam[] = [];
|
|
133
|
+
const emittedToolUseIds: string[] = [];
|
|
98
134
|
for (const block of message.content) {
|
|
99
135
|
if (block.type === "text" && block.text.trim()) {
|
|
100
136
|
blocks.push({ type: "text", text: sanitizeSurrogates(block.text) });
|
|
101
137
|
} else if (block.type === "toolCall") {
|
|
138
|
+
const anthropicId = getAnthropicToolId(block.id);
|
|
102
139
|
blocks.push({
|
|
103
140
|
type: "tool_use",
|
|
104
|
-
id:
|
|
141
|
+
id: anthropicId,
|
|
105
142
|
name: isOAuth ? toClaudeCodeToolName(block.name) : block.name,
|
|
106
|
-
input: block.arguments,
|
|
143
|
+
input: block.arguments ?? {},
|
|
107
144
|
});
|
|
145
|
+
emittedToolUseIds.push(anthropicId);
|
|
108
146
|
}
|
|
109
147
|
}
|
|
110
|
-
if (blocks.length > 0)
|
|
148
|
+
if (blocks.length > 0) {
|
|
149
|
+
params.push({ role: "assistant", content: blocks });
|
|
150
|
+
pendingToolUseIds = emittedToolUseIds;
|
|
151
|
+
}
|
|
111
152
|
continue;
|
|
112
153
|
}
|
|
113
154
|
|
|
114
155
|
if (message.role === "toolResult") {
|
|
115
|
-
const toolResults = [
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
156
|
+
const toolResults: ContentBlockParam[] = [];
|
|
157
|
+
const satisfiedIds = new Set<string>();
|
|
158
|
+
|
|
159
|
+
const firstId = getAnthropicToolId(message.toolCallId);
|
|
160
|
+
toolResults.push({
|
|
161
|
+
type: "tool_result",
|
|
162
|
+
tool_use_id: firstId,
|
|
163
|
+
content: convertToolResultContentToAnthropic(message.content),
|
|
164
|
+
is_error: message.isError,
|
|
165
|
+
});
|
|
166
|
+
satisfiedIds.add(firstId);
|
|
123
167
|
|
|
124
168
|
let j = i + 1;
|
|
125
169
|
while (j < messages.length && messages[j]?.role === "toolResult") {
|
|
126
170
|
const nextMessage = messages[j] as ToolResultMessage;
|
|
171
|
+
const nextId = getAnthropicToolId(nextMessage.toolCallId);
|
|
127
172
|
toolResults.push({
|
|
128
|
-
type: "tool_result"
|
|
129
|
-
tool_use_id:
|
|
173
|
+
type: "tool_result",
|
|
174
|
+
tool_use_id: nextId,
|
|
130
175
|
content: convertToolResultContentToAnthropic(nextMessage.content),
|
|
131
176
|
is_error: nextMessage.isError,
|
|
132
177
|
});
|
|
178
|
+
satisfiedIds.add(nextId);
|
|
133
179
|
j++;
|
|
134
180
|
}
|
|
135
181
|
i = j - 1;
|
|
182
|
+
|
|
183
|
+
// Synthesize "No result provided" for any tool_use from the last
|
|
184
|
+
// assistant message that wasn't covered by the toolResult run. They
|
|
185
|
+
// must live in the SAME user message as the real tool_results so that
|
|
186
|
+
// every tool_use is immediately followed by matching results.
|
|
187
|
+
for (const id of pendingToolUseIds) {
|
|
188
|
+
if (!satisfiedIds.has(id)) {
|
|
189
|
+
toolResults.push({
|
|
190
|
+
type: "tool_result",
|
|
191
|
+
tool_use_id: id,
|
|
192
|
+
content: "No result provided",
|
|
193
|
+
is_error: true,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
pendingToolUseIds = [];
|
|
198
|
+
|
|
136
199
|
params.push({ role: "user", content: toolResults });
|
|
137
200
|
}
|
|
138
201
|
}
|
package/src/stream.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type Model,
|
|
11
11
|
type SimpleStreamOptions,
|
|
12
12
|
type StopReason,
|
|
13
|
+
streamSimpleAnthropic,
|
|
13
14
|
} from "@mariozechner/pi-ai";
|
|
14
15
|
import { isClaudeOAuthAccessToken, USER_AGENT } from "./auth.js";
|
|
15
16
|
import {
|
|
@@ -83,6 +84,16 @@ export function streamAnthropicOAuth(
|
|
|
83
84
|
context: Context,
|
|
84
85
|
options?: SimpleStreamOptions,
|
|
85
86
|
): AssistantMessageEventStream {
|
|
87
|
+
// pi-ai keys streamSimple by `api`, so this runs for every anthropic-messages
|
|
88
|
+
// model (copilot, openrouter, …). Delegate non-anthropic providers.
|
|
89
|
+
if (model.provider !== "anthropic") {
|
|
90
|
+
return streamSimpleAnthropic(
|
|
91
|
+
model as Model<"anthropic-messages">,
|
|
92
|
+
context,
|
|
93
|
+
options,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
const stream = createAssistantMessageEventStream();
|
|
87
98
|
|
|
88
99
|
void (async () => {
|