@gotgenes/pi-anthropic-auth 0.4.6 → 0.5.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/CHANGELOG.md +166 -0
- package/README.md +2 -0
- package/package.json +29 -21
- package/src/anthropic-oauth.ts +38 -0
- package/{dist/constants.js → src/constants.ts} +42 -22
- package/src/debug.ts +38 -0
- package/src/index.ts +13 -0
- package/src/request-shaping.ts +294 -0
- package/src/system-prompt-shaping.ts +205 -0
- package/dist/anthropic-oauth.d.ts +0 -8
- package/dist/anthropic-oauth.js +0 -24
- package/dist/constants.d.ts +0 -78
- package/dist/debug.d.ts +0 -3
- package/dist/debug.js +0 -32
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -10
- package/dist/request-shaping.d.ts +0 -1
- package/dist/request-shaping.js +0 -189
- package/dist/system-prompt-shaping.d.ts +0 -53
- package/dist/system-prompt-shaping.js +0 -146
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
BILLING_HEADER_POSITIONS,
|
|
4
|
+
BILLING_HEADER_SALT,
|
|
5
|
+
CLAUDE_CODE_ENTRYPOINT,
|
|
6
|
+
CLAUDE_CODE_IDENTITY_PREFIX,
|
|
7
|
+
CLAUDE_CODE_VERSION,
|
|
8
|
+
MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX,
|
|
9
|
+
} from "./constants";
|
|
10
|
+
import { debugLog, isToolUseOnlyDebugEnabled } from "./debug";
|
|
11
|
+
import { shapeSystemBlocks } from "./system-prompt-shaping";
|
|
12
|
+
|
|
13
|
+
type TextBlock = {
|
|
14
|
+
type: "text";
|
|
15
|
+
text: string;
|
|
16
|
+
cache_control?: unknown;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type MessageBlock = {
|
|
21
|
+
type?: string;
|
|
22
|
+
text?: string;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type MessageParam = {
|
|
27
|
+
role?: string;
|
|
28
|
+
content?: string | MessageBlock[];
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type AnthropicPayload = {
|
|
33
|
+
model?: unknown;
|
|
34
|
+
messages?: unknown;
|
|
35
|
+
system?: unknown;
|
|
36
|
+
stream?: unknown;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isAnthropicMessagesPayload(
|
|
45
|
+
payload: unknown,
|
|
46
|
+
): payload is AnthropicPayload {
|
|
47
|
+
return (
|
|
48
|
+
isRecord(payload) &&
|
|
49
|
+
typeof payload.model === "string" &&
|
|
50
|
+
Array.isArray(payload.messages) &&
|
|
51
|
+
typeof payload.stream === "boolean"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
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
|
+
function getFirstUserText(messages: MessageParam[]): string {
|
|
80
|
+
const firstUserMessage = messages.find((message) => message.role === "user");
|
|
81
|
+
if (!firstUserMessage) return "";
|
|
82
|
+
|
|
83
|
+
if (typeof firstUserMessage.content === "string") {
|
|
84
|
+
return firstUserMessage.content;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!Array.isArray(firstUserMessage.content)) {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const firstTextBlock = firstUserMessage.content.find(
|
|
92
|
+
(block) => block.type === "text" && typeof block.text === "string",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return typeof firstTextBlock?.text === "string" ? firstTextBlock.text : "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildBillingHeaderValue(messages: MessageParam[]): string | undefined {
|
|
99
|
+
const messageText = getFirstUserText(messages);
|
|
100
|
+
if (!messageText) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cch = createHash("sha256")
|
|
105
|
+
.update(messageText)
|
|
106
|
+
.digest("hex")
|
|
107
|
+
.slice(0, 5);
|
|
108
|
+
const sampledCharacters = BILLING_HEADER_POSITIONS.map(
|
|
109
|
+
(index) => messageText[index] || "0",
|
|
110
|
+
).join("");
|
|
111
|
+
const suffix = createHash("sha256")
|
|
112
|
+
.update(`${BILLING_HEADER_SALT}${sampledCharacters}${CLAUDE_CODE_VERSION}`)
|
|
113
|
+
.digest("hex")
|
|
114
|
+
.slice(0, 3);
|
|
115
|
+
|
|
116
|
+
return [
|
|
117
|
+
"x-anthropic-billing-header:",
|
|
118
|
+
`cc_version=${CLAUDE_CODE_VERSION}.${suffix};`,
|
|
119
|
+
`cc_entrypoint=${CLAUDE_CODE_ENTRYPOINT};`,
|
|
120
|
+
`cch=${cch};`,
|
|
121
|
+
].join(" ");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeSystemBlock(block: unknown): TextBlock {
|
|
125
|
+
if (typeof block === "string") {
|
|
126
|
+
return { type: "text", text: block };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isRecord(block) && typeof block.text === "string") {
|
|
130
|
+
return {
|
|
131
|
+
...block,
|
|
132
|
+
type: "text",
|
|
133
|
+
text: block.text,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { type: "text", text: "" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function prependBillingHeader(
|
|
141
|
+
system: unknown,
|
|
142
|
+
messages: MessageParam[],
|
|
143
|
+
): unknown {
|
|
144
|
+
const billingHeader = buildBillingHeaderValue(messages);
|
|
145
|
+
if (!billingHeader) {
|
|
146
|
+
return system;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const systemBlocks = Array.isArray(system)
|
|
150
|
+
? system.map(normalizeSystemBlock)
|
|
151
|
+
: system == null
|
|
152
|
+
? []
|
|
153
|
+
: [normalizeSystemBlock(system)];
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
systemBlocks.some((block) =>
|
|
157
|
+
block.text.includes("x-anthropic-billing-header:"),
|
|
158
|
+
)
|
|
159
|
+
) {
|
|
160
|
+
return systemBlocks;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const billingBlock: TextBlock = { type: "text", text: billingHeader };
|
|
164
|
+
|
|
165
|
+
return [billingBlock, ...systemBlocks];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Splits assistant messages that interleave text and tool_use blocks.
|
|
170
|
+
*
|
|
171
|
+
* The Anthropic API rejects assistant turns where non-tool_use blocks follow
|
|
172
|
+
* a tool_use block. Pi's serializer can produce this ordering, so we split
|
|
173
|
+
* the message into two consecutive assistant turns: one with text blocks and
|
|
174
|
+
* one with tool_use blocks. The reordering is safe because the text and
|
|
175
|
+
* tool_use blocks are semantically independent within a single turn.
|
|
176
|
+
*/
|
|
177
|
+
function splitAssistantToolUseTrailingContent(
|
|
178
|
+
messages: MessageParam[],
|
|
179
|
+
): MessageParam[] {
|
|
180
|
+
return messages.flatMap((message) => {
|
|
181
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) {
|
|
182
|
+
return [message];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const firstToolUseIndex = message.content.findIndex(
|
|
186
|
+
(block) => block.type === "tool_use",
|
|
187
|
+
);
|
|
188
|
+
if (firstToolUseIndex === -1) {
|
|
189
|
+
return [message];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const trailingBlocks = message.content.slice(firstToolUseIndex);
|
|
193
|
+
if (!trailingBlocks.some((block) => block.type !== "tool_use")) {
|
|
194
|
+
return [message];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const nonToolUseBlocks = message.content.filter(
|
|
198
|
+
(block) => block.type !== "tool_use",
|
|
199
|
+
);
|
|
200
|
+
const toolUseBlocks = message.content.filter(
|
|
201
|
+
(block) => block.type === "tool_use",
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return [
|
|
205
|
+
{ ...message, content: nonToolUseBlocks },
|
|
206
|
+
{ ...message, content: toolUseBlocks },
|
|
207
|
+
];
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getToolDefinitionNames(payload: AnthropicPayload): string[] {
|
|
212
|
+
const tools = payload.tools;
|
|
213
|
+
if (!Array.isArray(tools)) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return tools
|
|
218
|
+
.map((tool) =>
|
|
219
|
+
isRecord(tool) && typeof tool.name === "string" ? tool.name : undefined,
|
|
220
|
+
)
|
|
221
|
+
.filter((name): name is string => typeof name === "string");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getToolUseNames(messages: MessageParam[]): string[] {
|
|
225
|
+
return messages.flatMap((message) => {
|
|
226
|
+
if (!Array.isArray(message.content)) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return message.content
|
|
231
|
+
.map((block) =>
|
|
232
|
+
block.type === "tool_use" && typeof block.name === "string"
|
|
233
|
+
? block.name
|
|
234
|
+
: undefined,
|
|
235
|
+
)
|
|
236
|
+
.filter((name): name is string => typeof name === "string");
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function countAssistantMessages(messages: MessageParam[]): number {
|
|
241
|
+
return messages.filter((message) => message.role === "assistant").length;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function shouldLogRequestDebug(messages: MessageParam[]): boolean {
|
|
245
|
+
if (!isToolUseOnlyDebugEnabled()) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return getToolUseNames(messages).length > 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function shapeAnthropicOAuthPayload(payload: unknown): unknown {
|
|
253
|
+
if (!isAnthropicMessagesPayload(payload)) {
|
|
254
|
+
return payload;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const messages = payload.messages as MessageParam[];
|
|
258
|
+
if (!isOAuthAnthropicPayload(payload)) {
|
|
259
|
+
return payload;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const normalizedMessages = splitAssistantToolUseTrailingContent(messages);
|
|
263
|
+
|
|
264
|
+
const shapedSystem = Array.isArray(payload.system)
|
|
265
|
+
? shapeSystemBlocks(payload.system as TextBlock[])
|
|
266
|
+
: payload.system;
|
|
267
|
+
const finalSystem = prependBillingHeader(shapedSystem, normalizedMessages);
|
|
268
|
+
|
|
269
|
+
const toolUseNamesBefore = getToolUseNames(messages);
|
|
270
|
+
const toolUseNamesAfter = getToolUseNames(normalizedMessages);
|
|
271
|
+
|
|
272
|
+
if (shouldLogRequestDebug(messages)) {
|
|
273
|
+
debugLog("before-provider-request", {
|
|
274
|
+
model: payload.model,
|
|
275
|
+
systemBlockCountBefore: Array.isArray(payload.system)
|
|
276
|
+
? payload.system.length
|
|
277
|
+
: 0,
|
|
278
|
+
systemBlockCountAfter: Array.isArray(finalSystem)
|
|
279
|
+
? finalSystem.length
|
|
280
|
+
: 0,
|
|
281
|
+
assistantMessagesBefore: countAssistantMessages(messages),
|
|
282
|
+
assistantMessagesAfter: countAssistantMessages(normalizedMessages),
|
|
283
|
+
toolDefinitions: getToolDefinitionNames(payload),
|
|
284
|
+
toolUseNamesBefore,
|
|
285
|
+
toolUseNamesAfter,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
...payload,
|
|
291
|
+
messages: normalizedMessages,
|
|
292
|
+
system: finalSystem,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MINIMAL_ANTHROPIC_OAUTH_PROMPT,
|
|
3
|
+
PARAGRAPH_REMOVAL_ANCHORS,
|
|
4
|
+
PI_DEFAULT_PROMPT_PREFIX,
|
|
5
|
+
PI_DEFAULT_PROMPT_TERMINATOR,
|
|
6
|
+
TEXT_REPLACEMENTS,
|
|
7
|
+
} from "./constants";
|
|
8
|
+
import { debugLog, isToolUseOnlyDebugEnabled } from "./debug";
|
|
9
|
+
|
|
10
|
+
let warnedTerminatorMissing = false;
|
|
11
|
+
|
|
12
|
+
function warnTerminatorMissingOnce(): void {
|
|
13
|
+
if (warnedTerminatorMissing) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
warnedTerminatorMissing = true;
|
|
17
|
+
console.warn(
|
|
18
|
+
"[pi-anthropic-auth] Pi default preamble terminator not found; falling back to '# Project Context' anchor. " +
|
|
19
|
+
"Upstream Pi may have reworded its preamble — update PI_DEFAULT_PROMPT_TERMINATOR.",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reset the one-time terminator-missing warning latch. Exposed for tests.
|
|
25
|
+
*/
|
|
26
|
+
export function _resetShapingWarnings(): void {
|
|
27
|
+
warnedTerminatorMissing = false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SanitizedSystemTextReport = {
|
|
31
|
+
text: string;
|
|
32
|
+
removedParagraphs: Array<{
|
|
33
|
+
anchor: string;
|
|
34
|
+
preview: string;
|
|
35
|
+
}>;
|
|
36
|
+
replacementMatches: string[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function previewParagraph(paragraph: string): string {
|
|
40
|
+
return paragraph.replace(/\s+/g, " ").trim().slice(0, 140);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shouldLogPromptDebug(report: SanitizedSystemTextReport): boolean {
|
|
44
|
+
if (!isToolUseOnlyDebugEnabled()) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
report.removedParagraphs.length === 0 &&
|
|
50
|
+
report.replacementMatches.length > 0
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sanitize system prompt text by removing paragraphs containing known
|
|
56
|
+
* Pi-specific anchor strings and applying inline text replacements for
|
|
57
|
+
* known Anthropic classifier trigger phrases.
|
|
58
|
+
*
|
|
59
|
+
* A paragraph is any text between blank lines (`\n\n`).
|
|
60
|
+
*
|
|
61
|
+
* This approach is resilient to upstream rewording — as long as the anchor
|
|
62
|
+
* string still appears somewhere in the paragraph, removal works regardless
|
|
63
|
+
* of how the surrounding text changes.
|
|
64
|
+
*/
|
|
65
|
+
export function sanitizeSystemTextWithReport(
|
|
66
|
+
text: string,
|
|
67
|
+
): SanitizedSystemTextReport {
|
|
68
|
+
const paragraphs = text.split(/\n\n+/);
|
|
69
|
+
const removedParagraphs: SanitizedSystemTextReport["removedParagraphs"] = [];
|
|
70
|
+
|
|
71
|
+
const filtered = paragraphs.filter((paragraph) => {
|
|
72
|
+
for (const anchor of PARAGRAPH_REMOVAL_ANCHORS) {
|
|
73
|
+
if (!paragraph.includes(anchor)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
removedParagraphs.push({
|
|
78
|
+
anchor,
|
|
79
|
+
preview: previewParagraph(paragraph),
|
|
80
|
+
});
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
let result = filtered.join("\n\n");
|
|
87
|
+
const replacementMatches: string[] = [];
|
|
88
|
+
|
|
89
|
+
for (const rule of TEXT_REPLACEMENTS) {
|
|
90
|
+
if (result.includes(rule.match)) {
|
|
91
|
+
replacementMatches.push(rule.match);
|
|
92
|
+
}
|
|
93
|
+
result = result.replaceAll(rule.match, rule.replacement);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
text: result.trim(),
|
|
98
|
+
removedParagraphs,
|
|
99
|
+
replacementMatches,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function sanitizeSystemText(text: string): string {
|
|
104
|
+
return sanitizeSystemTextWithReport(text).text;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function findProjectContextStart(systemPrompt: string): number {
|
|
108
|
+
const marker = "\n\n# Project Context\n\n";
|
|
109
|
+
return systemPrompt.indexOf(marker);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Shape a system prompt string for Anthropic OAuth compatibility.
|
|
114
|
+
*
|
|
115
|
+
* For the normal upstream Pi prompt shape, sanitize only the known preamble
|
|
116
|
+
* span and replace its identity paragraph with the minimal neutral prompt.
|
|
117
|
+
* This preserves downstream configuration/extension points embedded in the
|
|
118
|
+
* preamble (tool snippets and guideline bullets) while still stripping the
|
|
119
|
+
* Pi-specific identity, filler, and documentation paragraphs.
|
|
120
|
+
*
|
|
121
|
+
* If Pi's known preamble terminator drifts upstream, we fall back to slicing
|
|
122
|
+
* from `# Project Context`. If that section is also absent, we return the
|
|
123
|
+
* minimal prompt only.
|
|
124
|
+
*/
|
|
125
|
+
export function shapeAnthropicOAuthSystemPrompt(systemPrompt: string): string {
|
|
126
|
+
const prefixIdx = systemPrompt.indexOf(PI_DEFAULT_PROMPT_PREFIX);
|
|
127
|
+
if (prefixIdx === -1) {
|
|
128
|
+
return systemPrompt;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const terminatorIdx = systemPrompt.indexOf(
|
|
132
|
+
PI_DEFAULT_PROMPT_TERMINATOR,
|
|
133
|
+
prefixIdx,
|
|
134
|
+
);
|
|
135
|
+
if (terminatorIdx !== -1) {
|
|
136
|
+
const terminatorEnd = terminatorIdx + PI_DEFAULT_PROMPT_TERMINATOR.length;
|
|
137
|
+
const preamble = systemPrompt.slice(prefixIdx, terminatorEnd);
|
|
138
|
+
const report = sanitizeSystemTextWithReport(preamble);
|
|
139
|
+
const shapedPreamble = report.text
|
|
140
|
+
? `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}\n\n${report.text}`
|
|
141
|
+
: MINIMAL_ANTHROPIC_OAUTH_PROMPT;
|
|
142
|
+
|
|
143
|
+
if (shouldLogPromptDebug(report)) {
|
|
144
|
+
debugLog("system-prompt-shaping", {
|
|
145
|
+
mode: "terminator",
|
|
146
|
+
originalLength: systemPrompt.length,
|
|
147
|
+
preambleLength: preamble.length,
|
|
148
|
+
sanitizedPreambleLength: report.text.length,
|
|
149
|
+
removedParagraphCount: report.removedParagraphs.length,
|
|
150
|
+
removedAnchors: report.removedParagraphs.map((entry) => entry.anchor),
|
|
151
|
+
removedParagraphPreviews: report.removedParagraphs.map(
|
|
152
|
+
(entry) => entry.preview,
|
|
153
|
+
),
|
|
154
|
+
replacementMatches: report.replacementMatches,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
systemPrompt.slice(0, prefixIdx) +
|
|
160
|
+
shapedPreamble +
|
|
161
|
+
systemPrompt.slice(terminatorEnd)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
warnTerminatorMissingOnce();
|
|
166
|
+
if (!isToolUseOnlyDebugEnabled()) {
|
|
167
|
+
debugLog("system-prompt-shaping", {
|
|
168
|
+
mode: "project-context-fallback",
|
|
169
|
+
originalLength: systemPrompt.length,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const projectContextStart = findProjectContextStart(systemPrompt);
|
|
174
|
+
if (projectContextStart === -1) {
|
|
175
|
+
return MINIMAL_ANTHROPIC_OAUTH_PROMPT;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}${systemPrompt.slice(projectContextStart)}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type TextBlock = {
|
|
182
|
+
type: "text";
|
|
183
|
+
text: string;
|
|
184
|
+
[key: string]: unknown;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Apply system prompt shaping to an array of Anthropic system text blocks.
|
|
189
|
+
*
|
|
190
|
+
* Finds the first block containing Pi's default prompt preamble and replaces
|
|
191
|
+
* its text in-place (returning a new array). Blocks without the preamble are
|
|
192
|
+
* passed through unchanged.
|
|
193
|
+
*/
|
|
194
|
+
export function shapeSystemBlocks(blocks: TextBlock[]): TextBlock[] {
|
|
195
|
+
return blocks.map((block) => {
|
|
196
|
+
if (
|
|
197
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive runtime guard
|
|
198
|
+
block.type !== "text" ||
|
|
199
|
+
!block.text.includes(PI_DEFAULT_PROMPT_PREFIX)
|
|
200
|
+
) {
|
|
201
|
+
return block;
|
|
202
|
+
}
|
|
203
|
+
return { ...block, text: shapeAnthropicOAuthSystemPrompt(block.text) };
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
2
|
-
export declare function mergeRefreshedCredentials(credentials: OAuthCredentials, refreshed: Partial<OAuthCredentials>): OAuthCredentials;
|
|
3
|
-
export declare const anthropicOAuthOverride: {
|
|
4
|
-
readonly name: "Anthropic (Claude Pro/Max)";
|
|
5
|
-
readonly login: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials>;
|
|
6
|
-
readonly refreshToken: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
|
|
7
|
-
readonly getApiKey: (credentials: OAuthCredentials) => string;
|
|
8
|
-
};
|
package/dist/anthropic-oauth.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { loginAnthropic, refreshAnthropicToken, } from "@earendil-works/pi-ai/oauth";
|
|
2
|
-
export function mergeRefreshedCredentials(credentials, refreshed) {
|
|
3
|
-
return {
|
|
4
|
-
...credentials,
|
|
5
|
-
...refreshed,
|
|
6
|
-
refresh: typeof refreshed.refresh === "string" &&
|
|
7
|
-
refreshed.refresh.trim().length > 0
|
|
8
|
-
? refreshed.refresh
|
|
9
|
-
: credentials.refresh,
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
export const anthropicOAuthOverride = {
|
|
13
|
-
name: "Anthropic (Claude Pro/Max)",
|
|
14
|
-
login(callbacks) {
|
|
15
|
-
return loginAnthropic(callbacks);
|
|
16
|
-
},
|
|
17
|
-
async refreshToken(credentials) {
|
|
18
|
-
const refreshed = await refreshAnthropicToken(credentials.refresh);
|
|
19
|
-
return mergeRefreshedCredentials(credentials, refreshed);
|
|
20
|
-
},
|
|
21
|
-
getApiKey(credentials) {
|
|
22
|
-
return credentials.access;
|
|
23
|
-
},
|
|
24
|
-
};
|
package/dist/constants.d.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prefix of Pi's built-in default system prompt preamble.
|
|
3
|
-
*
|
|
4
|
-
* Used to detect whether a system block contains Pi's original verbose
|
|
5
|
-
* preamble so it can be replaced with the minimal neutral prompt.
|
|
6
|
-
*/
|
|
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)";
|
|
16
|
-
/**
|
|
17
|
-
* Prefix of the minimal neutral Anthropic OAuth system prompt.
|
|
18
|
-
*
|
|
19
|
-
* Used as a detection marker in request shaping to identify system blocks
|
|
20
|
-
* that have already been shaped. Must match the first line of
|
|
21
|
-
* MINIMAL_ANTHROPIC_OAUTH_PROMPT.
|
|
22
|
-
*/
|
|
23
|
-
export declare const MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX = "You are an expert coding assistant.";
|
|
24
|
-
/**
|
|
25
|
-
* Minimal neutral system prompt used for Anthropic OAuth requests.
|
|
26
|
-
*
|
|
27
|
-
* Replaces Pi's verbose default preamble to avoid prompt fingerprinting
|
|
28
|
-
* while preserving any project context that follows.
|
|
29
|
-
*/
|
|
30
|
-
export declare const MINIMAL_ANTHROPIC_OAUTH_PROMPT: string;
|
|
31
|
-
/**
|
|
32
|
-
* Prefix of Claude Code's identity injection block.
|
|
33
|
-
*
|
|
34
|
-
* Used to detect OAuth Anthropic payloads built by Pi's built-in Anthropic
|
|
35
|
-
* provider, which injects a "You are Claude Code, Anthropic's official CLI"
|
|
36
|
-
* system block for OAuth sessions.
|
|
37
|
-
*/
|
|
38
|
-
export declare const CLAUDE_CODE_IDENTITY_PREFIX = "You are Claude Code, Anthropic's official CLI";
|
|
39
|
-
/**
|
|
40
|
-
* Claude Code version string embedded in the billing header.
|
|
41
|
-
*
|
|
42
|
-
* **Must be kept in sync with the current Claude Code release.**
|
|
43
|
-
* Update this value when a new Claude Code version ships. If it drifts
|
|
44
|
-
* too far from what Anthropic expects, OAuth requests may be rejected or
|
|
45
|
-
* counted incorrectly.
|
|
46
|
-
*/
|
|
47
|
-
export declare const CLAUDE_CODE_VERSION = "2.1.119";
|
|
48
|
-
/** Salt used in the billing header suffix hash. */
|
|
49
|
-
export declare const BILLING_HEADER_SALT = "59cf53e54c78";
|
|
50
|
-
/** Character positions sampled from the first user message for the billing hash. */
|
|
51
|
-
export declare const BILLING_HEADER_POSITIONS: readonly [4, 7, 20];
|
|
52
|
-
/** Entrypoint identifier included in the billing header. */
|
|
53
|
-
export declare const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
54
|
-
/**
|
|
55
|
-
* Strings whose presence in a paragraph marks it as Pi-specific and droppable.
|
|
56
|
-
*
|
|
57
|
-
* Each entry is checked with `paragraph.includes(anchor)`.
|
|
58
|
-
*/
|
|
59
|
-
export declare const PARAGRAPH_REMOVAL_ANCHORS: readonly string[];
|
|
60
|
-
/**
|
|
61
|
-
* Inline text replacements applied after paragraph removal.
|
|
62
|
-
*
|
|
63
|
-
* These handle known Anthropic classifier trigger phrases that may appear
|
|
64
|
-
* in paragraphs we want to keep. Each rule is applied with `replaceAll`.
|
|
65
|
-
*
|
|
66
|
-
* The "Here is some useful information..." phrase was isolated by
|
|
67
|
-
* `opencode-anthropic-auth` via sliding-window bisection of a 10KB failing
|
|
68
|
-
* prompt. When it reaches Anthropic combined with typical agent context,
|
|
69
|
-
* /v1/messages responds with a 400 disguised as "You're out of extra usage."
|
|
70
|
-
* Replacing the word "useful" is enough to unblock the request.
|
|
71
|
-
*
|
|
72
|
-
* We don't currently emit this phrase, but it's included as a documented
|
|
73
|
-
* future risk per Issue #10.
|
|
74
|
-
*/
|
|
75
|
-
export declare const TEXT_REPLACEMENTS: readonly {
|
|
76
|
-
match: string;
|
|
77
|
-
replacement: string;
|
|
78
|
-
}[];
|
package/dist/debug.d.ts
DELETED
package/dist/debug.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { anthropicOAuthOverride } from "./anthropic-oauth.js";
|
|
2
|
-
import { shapeAnthropicOAuthPayload } from "./request-shaping.js";
|
|
3
|
-
export default function (pi) {
|
|
4
|
-
pi.registerProvider("anthropic", {
|
|
5
|
-
oauth: anthropicOAuthOverride,
|
|
6
|
-
});
|
|
7
|
-
pi.on("before_provider_request", (event) => {
|
|
8
|
-
return shapeAnthropicOAuthPayload(event.payload);
|
|
9
|
-
});
|
|
10
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function shapeAnthropicOAuthPayload(payload: unknown): unknown;
|