@gotgenes/pi-anthropic-auth 0.4.0 → 0.4.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 +28 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +8 -0
- package/dist/debug.d.ts +3 -0
- package/dist/debug.js +32 -0
- package/dist/request-shaping.js +51 -1
- package/dist/system-prompt-shaping.d.ts +21 -8
- package/dist/system-prompt-shaping.js +97 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,34 @@ pnpm run build # compile
|
|
|
57
57
|
pi -e /absolute/path/to/pi-anthropic-auth/dist/index.js
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
### Debug Logging
|
|
61
|
+
|
|
62
|
+
Set `PI_ANTHROPIC_AUTH_DEBUG` to enable structured debug logs from the OAuth shaping layer.
|
|
63
|
+
|
|
64
|
+
Modes:
|
|
65
|
+
|
|
66
|
+
- `PI_ANTHROPIC_AUTH_DEBUG=all` — log all Anthropic OAuth shaping events
|
|
67
|
+
- `PI_ANTHROPIC_AUTH_DEBUG=tool-use` — log only requests that include `tool_use`
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
PI_ANTHROPIC_AUTH_DEBUG=tool-use \
|
|
73
|
+
pi \
|
|
74
|
+
--model anthropic/claude-haiku-4-5 \
|
|
75
|
+
--no-session \
|
|
76
|
+
--tools read,grep,find,ls \
|
|
77
|
+
-e /absolute/path/to/pi-anthropic-auth/src/index.ts \
|
|
78
|
+
-p "How many lines are in @AGENTS.md ?"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Similar Projects
|
|
82
|
+
|
|
83
|
+
- [opencode-anthropic-auth](https://github.com/ex-machina-co/opencode-anthropic-auth/) — Anthropic OAuth compatibility work for [OpenCode](https://opencode.ai/).
|
|
84
|
+
- [pi-anthropic-oauth](https://github.com/leohenon/pi-anthropic-oauth) — a Pi extension that takes a fuller provider-override approach.
|
|
85
|
+
|
|
86
|
+
For notes on how this project compares to similar work, see [docs/comparison-to-similar-projects.md](docs/comparison-to-similar-projects.md).
|
|
87
|
+
|
|
60
88
|
## Acknowledgments
|
|
61
89
|
|
|
62
90
|
This project was inspired by [opencode-anthropic-auth](https://github.com/ex-machina-co/opencode-anthropic-auth/), which solved the same Anthropic OAuth compatibility problem for [OpenCode](https://opencode.ai/).
|
package/dist/constants.d.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* preamble so it can be replaced with the minimal neutral prompt.
|
|
6
6
|
*/
|
|
7
7
|
export declare const PI_DEFAULT_PROMPT_PREFIX = "You are an expert coding assistant operating inside pi, a coding agent harness.";
|
|
8
|
+
/**
|
|
9
|
+
* Final line of Pi's built-in default system prompt preamble.
|
|
10
|
+
*
|
|
11
|
+
* Used to replace the entire Pi-generated preamble body with the minimal
|
|
12
|
+
* neutral Anthropic OAuth prompt while preserving anything appended after the
|
|
13
|
+
* preamble (project context, skills, and date/cwd footer).
|
|
14
|
+
*/
|
|
15
|
+
export declare const PI_DEFAULT_PROMPT_TERMINATOR = "- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)";
|
|
8
16
|
/**
|
|
9
17
|
* Prefix of the minimal neutral Anthropic OAuth system prompt.
|
|
10
18
|
*
|
package/dist/constants.js
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* preamble so it can be replaced with the minimal neutral prompt.
|
|
6
6
|
*/
|
|
7
7
|
export const PI_DEFAULT_PROMPT_PREFIX = "You are an expert coding assistant operating inside pi, a coding agent harness.";
|
|
8
|
+
/**
|
|
9
|
+
* Final line of Pi's built-in default system prompt preamble.
|
|
10
|
+
*
|
|
11
|
+
* Used to replace the entire Pi-generated preamble body with the minimal
|
|
12
|
+
* neutral Anthropic OAuth prompt while preserving anything appended after the
|
|
13
|
+
* preamble (project context, skills, and date/cwd footer).
|
|
14
|
+
*/
|
|
15
|
+
export const PI_DEFAULT_PROMPT_TERMINATOR = "- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)";
|
|
8
16
|
/**
|
|
9
17
|
* Prefix of the minimal neutral Anthropic OAuth system prompt.
|
|
10
18
|
*
|
package/dist/debug.d.ts
ADDED
package/dist/debug.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function getDebugMode(value) {
|
|
2
|
+
if (!value)
|
|
3
|
+
return "off";
|
|
4
|
+
switch (value.trim().toLowerCase()) {
|
|
5
|
+
case "1":
|
|
6
|
+
case "true":
|
|
7
|
+
case "yes":
|
|
8
|
+
case "on":
|
|
9
|
+
case "debug":
|
|
10
|
+
case "all":
|
|
11
|
+
return "all";
|
|
12
|
+
case "tool":
|
|
13
|
+
case "tools":
|
|
14
|
+
case "tool-use":
|
|
15
|
+
case "tool_use":
|
|
16
|
+
return "tool-use";
|
|
17
|
+
default:
|
|
18
|
+
return "off";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function isDebugEnabled() {
|
|
22
|
+
return getDebugMode(process.env.PI_ANTHROPIC_AUTH_DEBUG) !== "off";
|
|
23
|
+
}
|
|
24
|
+
export function isToolUseOnlyDebugEnabled() {
|
|
25
|
+
return getDebugMode(process.env.PI_ANTHROPIC_AUTH_DEBUG) === "tool-use";
|
|
26
|
+
}
|
|
27
|
+
export function debugLog(scope, data) {
|
|
28
|
+
if (!isDebugEnabled()) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.error(`[pi-anthropic-auth][debug] ${scope} ${JSON.stringify(data)}`);
|
|
32
|
+
}
|
package/dist/request-shaping.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { BILLING_HEADER_POSITIONS, BILLING_HEADER_SALT, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY_PREFIX, CLAUDE_CODE_VERSION, MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX, } from "./constants.js";
|
|
3
|
+
import { debugLog, isToolUseOnlyDebugEnabled } from "./debug.js";
|
|
3
4
|
import { shapeSystemBlocks } from "./system-prompt-shaping.js";
|
|
4
5
|
function isRecord(value) {
|
|
5
6
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -119,6 +120,36 @@ function splitAssistantToolUseTrailingContent(messages) {
|
|
|
119
120
|
];
|
|
120
121
|
});
|
|
121
122
|
}
|
|
123
|
+
function getToolDefinitionNames(payload) {
|
|
124
|
+
const tools = payload.tools;
|
|
125
|
+
if (!Array.isArray(tools)) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
return tools
|
|
129
|
+
.map((tool) => isRecord(tool) && typeof tool.name === "string" ? tool.name : undefined)
|
|
130
|
+
.filter((name) => typeof name === "string");
|
|
131
|
+
}
|
|
132
|
+
function getToolUseNames(messages) {
|
|
133
|
+
return messages.flatMap((message) => {
|
|
134
|
+
if (!Array.isArray(message.content)) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
return message.content
|
|
138
|
+
.map((block) => block?.type === "tool_use" && typeof block.name === "string"
|
|
139
|
+
? block.name
|
|
140
|
+
: undefined)
|
|
141
|
+
.filter((name) => typeof name === "string");
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function countAssistantMessages(messages) {
|
|
145
|
+
return messages.filter((message) => message.role === "assistant").length;
|
|
146
|
+
}
|
|
147
|
+
function shouldLogRequestDebug(messages) {
|
|
148
|
+
if (!isToolUseOnlyDebugEnabled()) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return getToolUseNames(messages).length > 0;
|
|
152
|
+
}
|
|
122
153
|
export function shapeAnthropicOAuthPayload(payload) {
|
|
123
154
|
if (!isAnthropicMessagesPayload(payload)) {
|
|
124
155
|
return payload;
|
|
@@ -131,9 +162,28 @@ export function shapeAnthropicOAuthPayload(payload) {
|
|
|
131
162
|
const shapedSystem = Array.isArray(payload.system)
|
|
132
163
|
? shapeSystemBlocks(payload.system)
|
|
133
164
|
: payload.system;
|
|
165
|
+
const finalSystem = prependBillingHeader(shapedSystem, normalizedMessages);
|
|
166
|
+
const toolUseNamesBefore = getToolUseNames(messages);
|
|
167
|
+
const toolUseNamesAfter = getToolUseNames(normalizedMessages);
|
|
168
|
+
if (shouldLogRequestDebug(messages)) {
|
|
169
|
+
debugLog("before-provider-request", {
|
|
170
|
+
model: payload.model,
|
|
171
|
+
systemBlockCountBefore: Array.isArray(payload.system)
|
|
172
|
+
? payload.system.length
|
|
173
|
+
: 0,
|
|
174
|
+
systemBlockCountAfter: Array.isArray(finalSystem)
|
|
175
|
+
? finalSystem.length
|
|
176
|
+
: 0,
|
|
177
|
+
assistantMessagesBefore: countAssistantMessages(messages),
|
|
178
|
+
assistantMessagesAfter: countAssistantMessages(normalizedMessages),
|
|
179
|
+
toolDefinitions: getToolDefinitionNames(payload),
|
|
180
|
+
toolUseNamesBefore,
|
|
181
|
+
toolUseNamesAfter,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
134
184
|
return {
|
|
135
185
|
...payload,
|
|
136
186
|
messages: normalizedMessages,
|
|
137
|
-
system:
|
|
187
|
+
system: finalSystem,
|
|
138
188
|
};
|
|
139
189
|
}
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reset the one-time terminator-missing warning latch. Exposed for tests.
|
|
3
|
+
*/
|
|
4
|
+
export declare function _resetShapingWarnings(): void;
|
|
5
|
+
export type SanitizedSystemTextReport = {
|
|
6
|
+
text: string;
|
|
7
|
+
removedParagraphs: Array<{
|
|
8
|
+
anchor: string;
|
|
9
|
+
preview: string;
|
|
10
|
+
}>;
|
|
11
|
+
replacementMatches: string[];
|
|
12
|
+
};
|
|
1
13
|
/**
|
|
2
14
|
* Sanitize system prompt text by removing paragraphs containing known
|
|
3
15
|
* Pi-specific anchor strings and applying inline text replacements for
|
|
@@ -9,19 +21,20 @@
|
|
|
9
21
|
* string still appears somewhere in the paragraph, removal works regardless
|
|
10
22
|
* of how the surrounding text changes.
|
|
11
23
|
*/
|
|
24
|
+
export declare function sanitizeSystemTextWithReport(text: string): SanitizedSystemTextReport;
|
|
12
25
|
export declare function sanitizeSystemText(text: string): string;
|
|
13
26
|
/**
|
|
14
27
|
* Shape a system prompt string for Anthropic OAuth compatibility.
|
|
15
28
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* extension
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Prepends a minimal neutral prompt to replace the removed Pi identity.
|
|
29
|
+
* For the normal upstream Pi prompt shape, sanitize only the known preamble
|
|
30
|
+
* span and replace its identity paragraph with the minimal neutral prompt.
|
|
31
|
+
* This preserves downstream configuration/extension points embedded in the
|
|
32
|
+
* preamble (tool snippets and guideline bullets) while still stripping the
|
|
33
|
+
* Pi-specific identity, filler, and documentation paragraphs.
|
|
22
34
|
*
|
|
23
|
-
* If
|
|
24
|
-
*
|
|
35
|
+
* If Pi's known preamble terminator drifts upstream, we fall back to slicing
|
|
36
|
+
* from `# Project Context`. If that section is also absent, we return the
|
|
37
|
+
* minimal prompt only.
|
|
25
38
|
*/
|
|
26
39
|
export declare function shapeAnthropicOAuthSystemPrompt(systemPrompt: string): string;
|
|
27
40
|
type TextBlock = {
|
|
@@ -1,4 +1,30 @@
|
|
|
1
|
-
import { MINIMAL_ANTHROPIC_OAUTH_PROMPT, PARAGRAPH_REMOVAL_ANCHORS, PI_DEFAULT_PROMPT_PREFIX, TEXT_REPLACEMENTS, } from "./constants.js";
|
|
1
|
+
import { MINIMAL_ANTHROPIC_OAUTH_PROMPT, PARAGRAPH_REMOVAL_ANCHORS, PI_DEFAULT_PROMPT_PREFIX, PI_DEFAULT_PROMPT_TERMINATOR, TEXT_REPLACEMENTS, } from "./constants.js";
|
|
2
|
+
import { debugLog, isToolUseOnlyDebugEnabled } from "./debug.js";
|
|
3
|
+
let warnedTerminatorMissing = false;
|
|
4
|
+
function warnTerminatorMissingOnce() {
|
|
5
|
+
if (warnedTerminatorMissing) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
warnedTerminatorMissing = true;
|
|
9
|
+
console.warn("[pi-anthropic-auth] Pi default preamble terminator not found; falling back to '# Project Context' anchor. " +
|
|
10
|
+
"Upstream Pi may have reworded its preamble — update PI_DEFAULT_PROMPT_TERMINATOR.");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Reset the one-time terminator-missing warning latch. Exposed for tests.
|
|
14
|
+
*/
|
|
15
|
+
export function _resetShapingWarnings() {
|
|
16
|
+
warnedTerminatorMissing = false;
|
|
17
|
+
}
|
|
18
|
+
function previewParagraph(paragraph) {
|
|
19
|
+
return paragraph.replace(/\s+/g, " ").trim().slice(0, 140);
|
|
20
|
+
}
|
|
21
|
+
function shouldLogPromptDebug(report) {
|
|
22
|
+
if (!isToolUseOnlyDebugEnabled()) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return (report.removedParagraphs.length === 0 &&
|
|
26
|
+
report.replacementMatches.length > 0);
|
|
27
|
+
}
|
|
2
28
|
/**
|
|
3
29
|
* Sanitize system prompt text by removing paragraphs containing known
|
|
4
30
|
* Pi-specific anchor strings and applying inline text replacements for
|
|
@@ -10,43 +36,97 @@ import { MINIMAL_ANTHROPIC_OAUTH_PROMPT, PARAGRAPH_REMOVAL_ANCHORS, PI_DEFAULT_P
|
|
|
10
36
|
* string still appears somewhere in the paragraph, removal works regardless
|
|
11
37
|
* of how the surrounding text changes.
|
|
12
38
|
*/
|
|
13
|
-
export function
|
|
39
|
+
export function sanitizeSystemTextWithReport(text) {
|
|
14
40
|
const paragraphs = text.split(/\n\n+/);
|
|
41
|
+
const removedParagraphs = [];
|
|
15
42
|
const filtered = paragraphs.filter((paragraph) => {
|
|
16
43
|
for (const anchor of PARAGRAPH_REMOVAL_ANCHORS) {
|
|
17
|
-
if (paragraph.includes(anchor))
|
|
18
|
-
|
|
44
|
+
if (!paragraph.includes(anchor)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
removedParagraphs.push({
|
|
48
|
+
anchor,
|
|
49
|
+
preview: previewParagraph(paragraph),
|
|
50
|
+
});
|
|
51
|
+
return false;
|
|
19
52
|
}
|
|
20
53
|
return true;
|
|
21
54
|
});
|
|
22
55
|
let result = filtered.join("\n\n");
|
|
56
|
+
const replacementMatches = [];
|
|
23
57
|
for (const rule of TEXT_REPLACEMENTS) {
|
|
58
|
+
if (result.includes(rule.match)) {
|
|
59
|
+
replacementMatches.push(rule.match);
|
|
60
|
+
}
|
|
24
61
|
result = result.replaceAll(rule.match, rule.replacement);
|
|
25
62
|
}
|
|
26
|
-
return
|
|
63
|
+
return {
|
|
64
|
+
text: result.trim(),
|
|
65
|
+
removedParagraphs,
|
|
66
|
+
replacementMatches,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function sanitizeSystemText(text) {
|
|
70
|
+
return sanitizeSystemTextWithReport(text).text;
|
|
71
|
+
}
|
|
72
|
+
function findProjectContextStart(systemPrompt) {
|
|
73
|
+
const marker = "\n\n# Project Context\n\n";
|
|
74
|
+
return systemPrompt.indexOf(marker);
|
|
27
75
|
}
|
|
28
76
|
/**
|
|
29
77
|
* Shape a system prompt string for Anthropic OAuth compatibility.
|
|
30
78
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* extension
|
|
34
|
-
*
|
|
79
|
+
* For the normal upstream Pi prompt shape, sanitize only the known preamble
|
|
80
|
+
* span and replace its identity paragraph with the minimal neutral prompt.
|
|
81
|
+
* This preserves downstream configuration/extension points embedded in the
|
|
82
|
+
* preamble (tool snippets and guideline bullets) while still stripping the
|
|
83
|
+
* Pi-specific identity, filler, and documentation paragraphs.
|
|
35
84
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* unchanged — this gates shaping so non-Pi prompts pass through.
|
|
85
|
+
* If Pi's known preamble terminator drifts upstream, we fall back to slicing
|
|
86
|
+
* from `# Project Context`. If that section is also absent, we return the
|
|
87
|
+
* minimal prompt only.
|
|
40
88
|
*/
|
|
41
89
|
export function shapeAnthropicOAuthSystemPrompt(systemPrompt) {
|
|
42
|
-
|
|
90
|
+
const prefixIdx = systemPrompt.indexOf(PI_DEFAULT_PROMPT_PREFIX);
|
|
91
|
+
if (prefixIdx === -1) {
|
|
43
92
|
return systemPrompt;
|
|
44
93
|
}
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
94
|
+
const terminatorIdx = systemPrompt.indexOf(PI_DEFAULT_PROMPT_TERMINATOR, prefixIdx);
|
|
95
|
+
if (terminatorIdx !== -1) {
|
|
96
|
+
const terminatorEnd = terminatorIdx + PI_DEFAULT_PROMPT_TERMINATOR.length;
|
|
97
|
+
const preamble = systemPrompt.slice(prefixIdx, terminatorEnd);
|
|
98
|
+
const report = sanitizeSystemTextWithReport(preamble);
|
|
99
|
+
const shapedPreamble = report.text
|
|
100
|
+
? `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}\n\n${report.text}`
|
|
101
|
+
: MINIMAL_ANTHROPIC_OAUTH_PROMPT;
|
|
102
|
+
if (shouldLogPromptDebug(report)) {
|
|
103
|
+
debugLog("system-prompt-shaping", {
|
|
104
|
+
mode: "terminator",
|
|
105
|
+
originalLength: systemPrompt.length,
|
|
106
|
+
preambleLength: preamble.length,
|
|
107
|
+
sanitizedPreambleLength: report.text.length,
|
|
108
|
+
removedParagraphCount: report.removedParagraphs.length,
|
|
109
|
+
removedAnchors: report.removedParagraphs.map((entry) => entry.anchor),
|
|
110
|
+
removedParagraphPreviews: report.removedParagraphs.map((entry) => entry.preview),
|
|
111
|
+
replacementMatches: report.replacementMatches,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return (systemPrompt.slice(0, prefixIdx) +
|
|
115
|
+
shapedPreamble +
|
|
116
|
+
systemPrompt.slice(terminatorEnd));
|
|
117
|
+
}
|
|
118
|
+
warnTerminatorMissingOnce();
|
|
119
|
+
if (!isToolUseOnlyDebugEnabled()) {
|
|
120
|
+
debugLog("system-prompt-shaping", {
|
|
121
|
+
mode: "project-context-fallback",
|
|
122
|
+
originalLength: systemPrompt.length,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const projectContextStart = findProjectContextStart(systemPrompt);
|
|
126
|
+
if (projectContextStart === -1) {
|
|
47
127
|
return MINIMAL_ANTHROPIC_OAUTH_PROMPT;
|
|
48
128
|
}
|
|
49
|
-
return `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}
|
|
129
|
+
return `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}${systemPrompt.slice(projectContextStart)}`;
|
|
50
130
|
}
|
|
51
131
|
/**
|
|
52
132
|
* Apply system prompt shaping to an array of Anthropic system text blocks.
|