@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 +19 -0
- package/README.md +4 -1
- package/package.json +11 -11
- package/src/index.ts +28 -5
- package/src/oauth-transport.ts +97 -0
- package/src/request-shaping.ts +10 -30
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
|
|
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.
|
|
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.
|
|
47
|
-
"@earendil-works/pi-coding-agent": ">=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.
|
|
51
|
-
"@biomejs/biome": "^2.4.
|
|
52
|
-
"@earendil-works/pi-ai": "0.
|
|
53
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
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.
|
|
56
|
+
"eslint": "^10.4.1",
|
|
57
57
|
"globals": "^17.6.0",
|
|
58
|
-
"rumdl": "^0.
|
|
58
|
+
"rumdl": "^0.2.14",
|
|
59
59
|
"typescript": "^6.0.3",
|
|
60
|
-
"typescript-eslint": "^8.
|
|
61
|
-
"vitest": "^4.1.
|
|
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 {
|
|
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
|
-
|
|
11
|
-
|
|
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
|
+
}
|
package/src/request-shaping.ts
CHANGED
|
@@ -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)
|