pi-anthropic-oauth 0.2.3 → 0.3.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 CHANGED
@@ -47,9 +47,17 @@ pi update npm:pi-anthropic-oauth
47
47
  > [!NOTE]
48
48
  > Anthropic auth changes are closely monitored for quick compatibility updates.
49
49
 
50
+ ## Claude Code version override
51
+
52
+ Anthropic gates new models on the Claude Code client version (error `claude_code_version_too_old`). The version is sent in both the OAuth user agent and the billing header of the system prompt. Override it without reinstalling:
53
+
54
+ ```bash
55
+ PI_ANTHROPIC_OAUTH_CLAUDE_CODE_VERSION=2.1.280
56
+ ```
57
+
50
58
  ## System prompt rewriting
51
59
 
52
- When using Claude Pro/Max OAuth, the extension prepends Claude Code identity text and rewrites standalone `Pi` / `pi` references in Pi's system prompt to `Claude Code`. The rewrite mode defaults to `aggressive`:
60
+ When using Claude Pro/Max OAuth, the extension prepends the Claude Code billing-header and identity system blocks and rewrites standalone `Pi` / `pi` references in Pi's system prompt to `Claude Code`. The rewrite mode defaults to `aggressive`:
53
61
 
54
62
  ```bash
55
63
  PI_ANTHROPIC_OAUTH_REWRITE_MODE=aggressive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-anthropic-oauth",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
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": ">=0.80.8",
42
- "@earendil-works/pi-coding-agent": ">=0.80.8"
41
+ "@earendil-works/pi-ai": ">=0.86.0",
42
+ "@earendil-works/pi-coding-agent": ">=0.86.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@earendil-works/pi-ai": "^0.80.8",
46
- "@earendil-works/pi-coding-agent": "^0.80.8",
45
+ "@earendil-works/pi-ai": "^0.86.1",
46
+ "@earendil-works/pi-coding-agent": "^0.86.1",
47
47
  "typescript": "^5.9.3"
48
48
  }
49
49
  }
package/src/auth.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  OAuthCredentials,
4
4
  OAuthLoginCallbacks,
5
5
  } from "@earendil-works/pi-ai";
6
+ import { makeClaudeCodeUserAgent } from "./version.js";
6
7
 
7
8
  const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
8
9
  const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
@@ -16,7 +17,7 @@ const SCOPES = [
16
17
  "user:mcp_servers",
17
18
  "user:file_upload",
18
19
  ].join(" ");
19
- const USER_AGENT = "claude-code/2.1.97";
20
+ const USER_AGENT = makeClaudeCodeUserAgent();
20
21
  const CALLBACK_PORT = 53692;
21
22
  const CALLBACK_HOST = "127.0.0.1";
22
23
  const LOCAL_CALLBACK_TIMEOUT = 5 * 60 * 1000;
package/src/prompt.ts CHANGED
@@ -1,5 +1,21 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getClaudeCodeVersion } from "./version.js";
3
+
1
4
  const CLAUDE_CODE_IDENTITY =
2
- "You are Claude Code, Anthropic's official CLI for Claude.";
5
+ "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
6
+
7
+ /**
8
+ * Anthropic's OAuth classifier rejects premium-model requests that do not
9
+ * open with a Claude Code billing-header system block. The `cch` and
10
+ * `cc_prompt_id` keys must be present, but their values are not validated,
11
+ * and the block must not carry `cache_control`.
12
+ */
13
+ export function buildClaudeCodeBillingBlock(): MessageContentBlock {
14
+ return {
15
+ type: "text",
16
+ text: `x-anthropic-billing-header: cc_version=${getClaudeCodeVersion()}; cc_entrypoint=cli; cch=00000; cc_prompt_id=${randomUUID()}`,
17
+ };
18
+ }
3
19
  const PI_REMOVAL_ANCHORS = [
4
20
  "pi-coding-agent",
5
21
  "@earendil-works/pi-coding-agent",
@@ -32,7 +48,15 @@ type MessageContentBlock = {
32
48
  };
33
49
 
34
50
  export function sanitizeSurrogates(text: string): string {
35
- return text.replace(/[\uD800-\uDFFF]/g, "\uFFFD");
51
+ // Replace only *unpaired* surrogates: a high surrogate not followed by a low
52
+ // surrogate, or a low surrogate not preceded by a high surrogate. Valid
53
+ // surrogate pairs (non-BMP characters such as emoji) must pass through
54
+ // untouched. Equivalent to String.prototype.toWellFormed(), which requires
55
+ // Node 20+ while this package supports Node >=18.
56
+ return text.replace(
57
+ /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
58
+ "\uFFFD",
59
+ );
36
60
  }
37
61
 
38
62
  export function buildAnthropicSystemPrompt(
@@ -42,11 +66,8 @@ export function buildAnthropicSystemPrompt(
42
66
  const blocks: MessageContentBlock[] = [];
43
67
 
44
68
  if (isOAuth) {
45
- blocks.push({
46
- type: "text",
47
- text: CLAUDE_CODE_IDENTITY,
48
- cache_control: { type: "ephemeral" },
49
- });
69
+ blocks.push(buildClaudeCodeBillingBlock());
70
+ blocks.push({ type: "text", text: CLAUDE_CODE_IDENTITY });
50
71
  }
51
72
 
52
73
  const sanitized = systemPrompt ? sanitizeSystemText(systemPrompt) : "";
package/src/stream.ts CHANGED
@@ -5,11 +5,15 @@ import {
5
5
  type AssistantMessage,
6
6
  type AssistantMessageEventStream,
7
7
  calculateCost,
8
- type Context,
8
+ collapseSystemMessages,
9
9
  createAssistantMessageEventStream,
10
+ getCurrentSystemPrompt,
11
+ getCurrentTools,
12
+ type JsonObject,
10
13
  type Model,
11
14
  type SimpleStreamOptions,
12
15
  type StopReason,
16
+ type TranscriptContext,
13
17
  } from "@earendil-works/pi-ai";
14
18
  import { isClaudeOAuthAccessToken, USER_AGENT } from "./auth.js";
15
19
  import {
@@ -94,11 +98,19 @@ function makeDefaultHeaders(
94
98
 
95
99
  export function streamAnthropicOAuth(
96
100
  model: Model<Api>,
97
- context: Context,
101
+ context: TranscriptContext,
98
102
  options?: SimpleStreamOptions,
99
103
  ): AssistantMessageEventStream {
100
104
  const stream = createAssistantMessageEventStream();
101
105
 
106
+ // Anthropic has no mid-conversation system messages: fold system-message
107
+ // patches into the leading system message, then read the current prompt and
108
+ // tool declarations from the transcript (pi >= 0.86 no longer provides
109
+ // context.systemPrompt / context.tools).
110
+ const transcript = collapseSystemMessages(context);
111
+ const tools = getCurrentTools(transcript.messages);
112
+ const systemPrompt = getCurrentSystemPrompt(transcript.messages);
113
+
102
114
  void (async () => {
103
115
  const output: AssistantMessage = {
104
116
  role: "assistant",
@@ -144,15 +156,15 @@ export function streamAnthropicOAuth(
144
156
 
145
157
  const params: MessageCreateParamsStreaming = {
146
158
  model: model.id,
147
- messages: convertPiMessagesToAnthropic(context.messages, isOAuth, model),
159
+ messages: convertPiMessagesToAnthropic(transcript.messages, isOAuth, model),
148
160
  max_tokens: maxTokens,
149
161
  stream: true,
150
162
  };
151
163
 
152
- const system = buildAnthropicSystemPrompt(context.systemPrompt, isOAuth);
164
+ const system = buildAnthropicSystemPrompt(systemPrompt, isOAuth);
153
165
  if (system) params.system = system as never;
154
- if (context.tools?.length)
155
- params.tools = convertPiToolsToAnthropic(context.tools, isOAuth);
166
+ if (tools.length)
167
+ params.tools = convertPiToolsToAnthropic(tools, isOAuth);
156
168
 
157
169
  if (options?.reasoning && model.reasoning && maxTokens > 1) {
158
170
  const defaultBudgets: Record<string, number> = {
@@ -168,11 +180,40 @@ export function streamAnthropicOAuth(
168
180
  ];
169
181
  const requestedBudget =
170
182
  customBudget ?? defaultBudgets[options.reasoning] ?? 10240;
183
+ const display = "summarized";
184
+ const forceAdaptive = (
185
+ model.compat as { forceAdaptiveThinking?: boolean } | undefined
186
+ )?.forceAdaptiveThinking;
187
+ const id = model.id.toLowerCase().replace(/\./g, "-");
188
+ const adaptive =
189
+ forceAdaptive === true ||
190
+ (forceAdaptive !== false &&
191
+ (/claude-(?:opus|sonnet|haiku|fable|mythos)-5(?:-|$)/.test(id) ||
192
+ /claude-(?:opus|sonnet|haiku|fable|mythos)-4-(?:[6-9]|\d{2,})(?:-|$)/.test(
193
+ id,
194
+ )));
171
195
 
172
- params.thinking = {
173
- type: "enabled",
174
- budget_tokens: Math.min(requestedBudget, maxTokens - 1),
175
- };
196
+ if (adaptive) {
197
+ const mapped = model.thinkingLevelMap?.[options.reasoning];
198
+ const effort =
199
+ typeof mapped === "string"
200
+ ? mapped
201
+ : options.reasoning === "minimal" || options.reasoning === "low"
202
+ ? "low"
203
+ : options.reasoning === "medium"
204
+ ? "medium"
205
+ : options.reasoning === "high"
206
+ ? "high"
207
+ : "high";
208
+ params.thinking = { type: "adaptive", display } as never;
209
+ Object.assign(params, { output_config: { effort } });
210
+ } else {
211
+ params.thinking = {
212
+ type: "enabled",
213
+ budget_tokens: Math.min(requestedBudget, maxTokens - 1),
214
+ display,
215
+ } as never;
216
+ }
176
217
  }
177
218
 
178
219
  // Raw stream instead of the MessageStream helper: MessageStream
@@ -268,7 +309,7 @@ export function streamAnthropicOAuth(
268
309
  name: isOAuth
269
310
  ? fromClaudeCodeToolName(
270
311
  event.content_block.name,
271
- context.tools,
312
+ tools,
272
313
  )
273
314
  : event.content_block.name,
274
315
  arguments: {},
@@ -322,10 +363,7 @@ export function streamAnthropicOAuth(
322
363
  ) {
323
364
  block.partialJson += event.delta.partial_json;
324
365
  try {
325
- block.arguments = JSON.parse(block.partialJson) as Record<
326
- string,
327
- unknown
328
- >;
366
+ block.arguments = JSON.parse(block.partialJson) as JsonObject;
329
367
  } catch {}
330
368
  stream.push({
331
369
  type: "toolcall_delta",
@@ -361,10 +399,7 @@ export function streamAnthropicOAuth(
361
399
  });
362
400
  } else if (block.type === "toolCall") {
363
401
  try {
364
- block.arguments = JSON.parse(block.partialJson) as Record<
365
- string,
366
- unknown
367
- >;
402
+ block.arguments = JSON.parse(block.partialJson) as JsonObject;
368
403
  } catch {}
369
404
  delete (block as { partialJson?: string }).partialJson;
370
405
  stream.push({
package/src/version.ts ADDED
@@ -0,0 +1,24 @@
1
+ export const CLAUDE_CODE_VERSION_ENV = "PI_ANTHROPIC_OAUTH_CLAUDE_CODE_VERSION";
2
+ const DEFAULT_CLAUDE_CODE_VERSION = "2.1.280";
3
+
4
+ /**
5
+ * Claude Code version advertised to the API (user agent and billing header).
6
+ *
7
+ * Anthropic gates new models on the client version; override with
8
+ * PI_ANTHROPIC_OAUTH_CLAUDE_CODE_VERSION when a newer one is required.
9
+ */
10
+ export function getClaudeCodeVersion(
11
+ env: NodeJS.ProcessEnv = process.env,
12
+ ): string {
13
+ return env[CLAUDE_CODE_VERSION_ENV]?.trim() || DEFAULT_CLAUDE_CODE_VERSION;
14
+ }
15
+
16
+ /**
17
+ * User agent matching the current Claude Code CLI client. The classifier
18
+ * rejects the older `claude-code/<ver>` form for premium models.
19
+ */
20
+ export function makeClaudeCodeUserAgent(
21
+ env: NodeJS.ProcessEnv = process.env,
22
+ ): string {
23
+ return `claude-cli/${getClaudeCodeVersion(env)} (external, cli)`;
24
+ }