@gotgenes/pi-anthropic-auth 0.5.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.1](https://github.com/gotgenes/pi-anthropic-auth/compare/v0.5.0...v0.5.1) (2026-06-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * shape Anthropic OAuth requests at the transport layer ([a8c2015](https://github.com/gotgenes/pi-anthropic-auth/commit/a8c2015c293c6b60d1110f73620dd3825057c6e6))
9
+
10
+
11
+ ### Documentation
12
+
13
+ * document the OAuth transport-wrapper architecture ([6ce28a4](https://github.com/gotgenes/pi-anthropic-auth/commit/6ce28a484189a3f1500bc0eb3e18c87fcb1e5c6f))
14
+ * note prek rumdl-fmt pre-commit hook in markdown-conventions skill ([5838075](https://github.com/gotgenes/pi-anthropic-auth/commit/58380753961e3fa1bb0b10c1cd72ca936b926e48))
15
+ * port reusable skills from pi-packages ([1e606a9](https://github.com/gotgenes/pi-anthropic-auth/commit/1e606a97d2dfd509ad4b8c5b29f9b2eea2771afc))
16
+
17
+
18
+ ### Miscellaneous Chores
19
+
20
+ * bump dependencies and establish Pi 0.79.1 base ([62696c9](https://github.com/gotgenes/pi-anthropic-auth/commit/62696c97934a3bdbd67091fb30ee2af7d8959f8b))
21
+
3
22
  ## [0.5.0](https://github.com/gotgenes/pi-anthropic-auth/compare/v0.4.6...v0.5.0) (2026-05-24)
4
23
 
5
24
 
package/README.md CHANGED
@@ -16,7 +16,10 @@ This extension fills in the gaps for users who want to use their **Claude Pro or
16
16
 
17
17
  It keeps everything you'd expect — the built-in `anthropic` provider, the full model list, API-key behavior, and the native `/login anthropic` flow — and layers on the compatibility fixes needed to make OAuth subscriptions work reliably.
18
18
 
19
- Requests to non-Anthropic providers and plain API-key Anthropic requests pass through completely untouched — the extension only activates when it detects an Anthropic OAuth payload.
19
+ Requests to non-Anthropic providers and plain API-key Anthropic requests pass through completely untouched — the extension only activates when it detects an Anthropic OAuth access token (`sk-ant-oat`).
20
+
21
+ Shaping runs in a thin transport wrapper around Pi's own Anthropic transport, so it applies to every OAuth call path — the interactive loop, compaction, and any background-agent work — not just the main turn.
22
+ See [docs/architecture.md](docs/architecture.md) for how this works.
20
23
 
21
24
  ## Install
22
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-anthropic-auth",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Pi extension package for Anthropic OAuth compatibility",
5
5
  "author": {
6
6
  "name": "Chris Lasher"
@@ -43,22 +43,22 @@
43
43
  ]
44
44
  },
45
45
  "peerDependencies": {
46
- "@earendil-works/pi-ai": ">=0.75.0",
47
- "@earendil-works/pi-coding-agent": ">=0.75.0"
46
+ "@earendil-works/pi-ai": ">=0.79.1",
47
+ "@earendil-works/pi-coding-agent": ">=0.79.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@anthropic-ai/sdk": "^0.95.0",
51
- "@biomejs/biome": "^2.4.14",
52
- "@earendil-works/pi-ai": "0.75.5",
53
- "@earendil-works/pi-coding-agent": "0.75.5",
50
+ "@anthropic-ai/sdk": "^0.104.1",
51
+ "@biomejs/biome": "^2.4.16",
52
+ "@earendil-works/pi-ai": "0.79.1",
53
+ "@earendil-works/pi-coding-agent": "0.79.1",
54
54
  "@eslint/js": "^10.0.1",
55
55
  "@types/node": "^22.15.3",
56
- "eslint": "^10.4.0",
56
+ "eslint": "^10.4.1",
57
57
  "globals": "^17.6.0",
58
- "rumdl": "^0.1.93",
58
+ "rumdl": "^0.2.14",
59
59
  "typescript": "^6.0.3",
60
- "typescript-eslint": "^8.59.4",
61
- "vitest": "^4.1.5"
60
+ "typescript-eslint": "^8.61.0",
61
+ "vitest": "^4.1.8"
62
62
  },
63
63
  "scripts": {
64
64
  "check": "tsc --noEmit",
package/src/index.ts CHANGED
@@ -1,13 +1,36 @@
1
+ import { getApiProvider } from "@earendil-works/pi-ai";
1
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
3
  import { anthropicOAuthOverride } from "./anthropic-oauth";
3
- import { shapeAnthropicOAuthPayload } from "./request-shaping";
4
+ import { createAnthropicOAuthStreamSimple } from "./oauth-transport";
4
5
 
5
6
  export default function (pi: ExtensionAPI) {
7
+ // Capture Pi's built-in anthropic-messages transport BEFORE we override it,
8
+ // so our wrapper can delegate to it (delegating to the registered wrapper
9
+ // instead would recurse). pi-ai registers its built-ins on import, so this
10
+ // is resolved by the time extensions load.
11
+ const builtinTransport = getApiProvider("anthropic-messages");
12
+
13
+ // Re-register the built-in `anthropic` provider with:
14
+ // 1. our OAuth login + refresh override (`oauth`), and
15
+ // 2. a thin transport wrapper (`streamSimple`) that applies Claude Code
16
+ // OAuth request shaping on every Anthropic call path.
17
+ //
18
+ // Omitting `models` preserves Pi's built-in Anthropic model list. The
19
+ // transport wrapper replaces our previous `before_provider_request` handler:
20
+ // that hook only fires for the interactive agent loop, so auxiliary OAuth
21
+ // requests (built-in compaction, third-party background agents) bypassed it
22
+ // and failed with Anthropic "extra usage" 400s. Registering `streamSimple`
23
+ // routes through Pi's singleton API registry, so the same shaping now covers
24
+ // the main loop, `completeSimple` compaction, and `agentLoop` background work.
6
25
  pi.registerProvider("anthropic", {
7
26
  oauth: anthropicOAuthOverride,
8
- });
9
-
10
- pi.on("before_provider_request", (event) => {
11
- return shapeAnthropicOAuthPayload(event.payload);
27
+ ...(builtinTransport
28
+ ? {
29
+ api: "anthropic-messages",
30
+ streamSimple: createAnthropicOAuthStreamSimple(
31
+ builtinTransport.streamSimple,
32
+ ),
33
+ }
34
+ : {}),
12
35
  });
13
36
  }
@@ -0,0 +1,97 @@
1
+ import type {
2
+ Api,
3
+ AssistantMessageEventStream,
4
+ Context,
5
+ Model,
6
+ SimpleStreamOptions,
7
+ } from "@earendil-works/pi-ai";
8
+ import { shapeAnthropicOAuthPayload } from "./request-shaping";
9
+
10
+ /**
11
+ * Anthropic OAuth access tokens are issued with an `sk-ant-oat` prefix.
12
+ *
13
+ * This is the same signal Pi's built-in Anthropic provider uses internally to
14
+ * decide whether to emit Claude Code identity headers, so gating on it keeps
15
+ * our shaping aligned with Pi's own OAuth detection.
16
+ */
17
+ const ANTHROPIC_OAUTH_TOKEN_MARKER = "sk-ant-oat";
18
+
19
+ /**
20
+ * The transport-level `streamSimple` handler shape Pi's API registry uses.
21
+ *
22
+ * It matches `ApiStreamSimpleFunction` from `@earendil-works/pi-ai` and is
23
+ * intentionally wider than a single concrete model type because Pi registers
24
+ * it per `Api`, not per model.
25
+ */
26
+ export type AnthropicStreamSimple = (
27
+ model: Model<Api>,
28
+ context: Context,
29
+ options?: SimpleStreamOptions,
30
+ ) => AssistantMessageEventStream;
31
+
32
+ /**
33
+ * Returns true when the resolved API key is an Anthropic OAuth access token.
34
+ *
35
+ * API-key requests (and OAuth tokens for other providers) return false, so the
36
+ * caller leaves their payloads untouched.
37
+ */
38
+ export function isAnthropicOAuthToken(
39
+ apiKey: string | undefined,
40
+ ): apiKey is string {
41
+ return (
42
+ typeof apiKey === "string" && apiKey.includes(ANTHROPIC_OAUTH_TOKEN_MARKER)
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Wraps Pi's built-in Anthropic `streamSimple` transport so OAuth request
48
+ * shaping runs on **every** Anthropic call path, not only the main agent loop.
49
+ *
50
+ * Pi only threads its `before_provider_request` hook into the interactive agent
51
+ * loop. Auxiliary calls — built-in compaction/summarization (`completeSimple`)
52
+ * and third-party background agents (e.g. pi-observational-memory's observer /
53
+ * reflector / dropper running via `agentLoop`) — issue requests through the
54
+ * same singleton API-registry transport but without that hook. Those OAuth
55
+ * requests then reach Anthropic with no Claude Code billing header and are
56
+ * classified as third-party app usage, producing the misleading "extra usage"
57
+ * HTTP 400.
58
+ *
59
+ * By injecting our shaping as an `onPayload` on the underlying transport, every
60
+ * Anthropic OAuth request is shaped regardless of which Pi code path issued it.
61
+ * The wrapper composes (does not replace) any caller-provided `onPayload`, so
62
+ * other extensions' `before_provider_request` handlers continue to run on the
63
+ * main loop, and our shaping is applied last — closest to the wire.
64
+ *
65
+ * Gating is strictly OAuth-only: when the request is not an Anthropic OAuth
66
+ * token, the payload passes through untouched, preserving Pi's normal
67
+ * API-key and non-Anthropic transport behavior.
68
+ *
69
+ * @param delegate The built-in Anthropic `streamSimple` transport, captured via
70
+ * `getApiProvider("anthropic-messages")` **before** this wrapper is
71
+ * registered. It must be the underlying transport, never the registered
72
+ * wrapper, otherwise calls would recurse.
73
+ */
74
+ export function createAnthropicOAuthStreamSimple(
75
+ delegate: AnthropicStreamSimple,
76
+ ): AnthropicStreamSimple {
77
+ return (model, context, options) => {
78
+ const callerOnPayload = options?.onPayload;
79
+
80
+ const onPayload: SimpleStreamOptions["onPayload"] = async (
81
+ payload,
82
+ payloadModel,
83
+ ) => {
84
+ const upstream = callerOnPayload
85
+ ? ((await callerOnPayload(payload, payloadModel)) ?? payload)
86
+ : payload;
87
+
88
+ if (!isAnthropicOAuthToken(options?.apiKey)) {
89
+ return upstream;
90
+ }
91
+
92
+ return shapeAnthropicOAuthPayload(upstream);
93
+ };
94
+
95
+ return delegate(model, context, { ...options, onPayload });
96
+ };
97
+ }
@@ -3,9 +3,7 @@ import {
3
3
  BILLING_HEADER_POSITIONS,
4
4
  BILLING_HEADER_SALT,
5
5
  CLAUDE_CODE_ENTRYPOINT,
6
- CLAUDE_CODE_IDENTITY_PREFIX,
7
6
  CLAUDE_CODE_VERSION,
8
- MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX,
9
7
  } from "./constants";
10
8
  import { debugLog, isToolUseOnlyDebugEnabled } from "./debug";
11
9
  import { shapeSystemBlocks } from "./system-prompt-shaping";
@@ -52,30 +50,6 @@ function isAnthropicMessagesPayload(
52
50
  );
53
51
  }
54
52
 
55
- function isOAuthAnthropicPayload(payload: AnthropicPayload): boolean {
56
- if (!Array.isArray(payload.system)) {
57
- return false;
58
- }
59
-
60
- return payload.system.some(hasOAuthAnthropicSystemMarker);
61
- }
62
-
63
- function hasOAuthAnthropicSystemMarker(block: unknown): boolean {
64
- if (
65
- !isRecord(block) ||
66
- block.type !== "text" ||
67
- typeof block.text !== "string"
68
- ) {
69
- return false;
70
- }
71
-
72
- return (
73
- block.text.includes(CLAUDE_CODE_IDENTITY_PREFIX) ||
74
- block.text.includes("x-anthropic-billing-header:") ||
75
- block.text.startsWith(MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX)
76
- );
77
- }
78
-
79
53
  function getFirstUserText(messages: MessageParam[]): string {
80
54
  const firstUserMessage = messages.find((message) => message.role === "user");
81
55
  if (!firstUserMessage) return "";
@@ -249,16 +223,22 @@ function shouldLogRequestDebug(messages: MessageParam[]): boolean {
249
223
  return getToolUseNames(messages).length > 0;
250
224
  }
251
225
 
226
+ /**
227
+ * Applies Anthropic Claude Code OAuth shaping to a built provider payload.
228
+ *
229
+ * This function assumes the caller has already confirmed the request is an
230
+ * Anthropic OAuth request. OAuth gating lives at the transport seam
231
+ * (`createAnthropicOAuthStreamSimple`), which only invokes this shaping when
232
+ * the request carries an `sk-ant-oat` access token. We therefore do not
233
+ * sniff system-prompt markers here; non-Anthropic-messages payloads are still
234
+ * returned untouched as a structural guard.
235
+ */
252
236
  export function shapeAnthropicOAuthPayload(payload: unknown): unknown {
253
237
  if (!isAnthropicMessagesPayload(payload)) {
254
238
  return payload;
255
239
  }
256
240
 
257
241
  const messages = payload.messages as MessageParam[];
258
- if (!isOAuthAnthropicPayload(payload)) {
259
- return payload;
260
- }
261
-
262
242
  const normalizedMessages = splitAssistantToolUseTrailingContent(messages);
263
243
 
264
244
  const shapedSystem = Array.isArray(payload.system)