@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.
@@ -1,189 +0,0 @@
1
- import { createHash } from "node:crypto";
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";
4
- import { shapeSystemBlocks } from "./system-prompt-shaping.js";
5
- function isRecord(value) {
6
- return value !== null && typeof value === "object" && !Array.isArray(value);
7
- }
8
- function isAnthropicMessagesPayload(payload) {
9
- return (isRecord(payload) &&
10
- typeof payload.model === "string" &&
11
- Array.isArray(payload.messages) &&
12
- typeof payload.stream === "boolean");
13
- }
14
- function isOAuthAnthropicPayload(payload) {
15
- if (!Array.isArray(payload.system)) {
16
- return false;
17
- }
18
- return payload.system.some(hasOAuthAnthropicSystemMarker);
19
- }
20
- function hasOAuthAnthropicSystemMarker(block) {
21
- if (!isRecord(block) ||
22
- block.type !== "text" ||
23
- typeof block.text !== "string") {
24
- return false;
25
- }
26
- return (block.text.includes(CLAUDE_CODE_IDENTITY_PREFIX) ||
27
- block.text.includes("x-anthropic-billing-header:") ||
28
- block.text.startsWith(MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX));
29
- }
30
- function getFirstUserText(messages) {
31
- const firstUserMessage = messages.find((message) => message.role === "user");
32
- if (!firstUserMessage)
33
- return "";
34
- if (typeof firstUserMessage.content === "string") {
35
- return firstUserMessage.content;
36
- }
37
- if (!Array.isArray(firstUserMessage.content)) {
38
- return "";
39
- }
40
- const firstTextBlock = firstUserMessage.content.find((block) => block?.type === "text" && typeof block.text === "string");
41
- return typeof firstTextBlock?.text === "string" ? firstTextBlock.text : "";
42
- }
43
- function buildBillingHeaderValue(messages) {
44
- const messageText = getFirstUserText(messages);
45
- if (!messageText) {
46
- return undefined;
47
- }
48
- const cch = createHash("sha256")
49
- .update(messageText)
50
- .digest("hex")
51
- .slice(0, 5);
52
- const sampledCharacters = BILLING_HEADER_POSITIONS.map((index) => messageText[index] || "0").join("");
53
- const suffix = createHash("sha256")
54
- .update(`${BILLING_HEADER_SALT}${sampledCharacters}${CLAUDE_CODE_VERSION}`)
55
- .digest("hex")
56
- .slice(0, 3);
57
- return [
58
- "x-anthropic-billing-header:",
59
- `cc_version=${CLAUDE_CODE_VERSION}.${suffix};`,
60
- `cc_entrypoint=${CLAUDE_CODE_ENTRYPOINT};`,
61
- `cch=${cch};`,
62
- ].join(" ");
63
- }
64
- function normalizeSystemBlock(block) {
65
- if (typeof block === "string") {
66
- return { type: "text", text: block };
67
- }
68
- if (isRecord(block) && typeof block.text === "string") {
69
- return {
70
- ...block,
71
- type: "text",
72
- text: block.text,
73
- };
74
- }
75
- return { type: "text", text: String(block ?? "") };
76
- }
77
- function prependBillingHeader(system, messages) {
78
- const billingHeader = buildBillingHeaderValue(messages);
79
- if (!billingHeader) {
80
- return system;
81
- }
82
- const systemBlocks = Array.isArray(system)
83
- ? system.map(normalizeSystemBlock)
84
- : system == null
85
- ? []
86
- : [normalizeSystemBlock(system)];
87
- if (systemBlocks.some((block) => block.text.includes("x-anthropic-billing-header:"))) {
88
- return systemBlocks;
89
- }
90
- const billingBlock = { type: "text", text: billingHeader };
91
- return [billingBlock, ...systemBlocks];
92
- }
93
- /**
94
- * Splits assistant messages that interleave text and tool_use blocks.
95
- *
96
- * The Anthropic API rejects assistant turns where non-tool_use blocks follow
97
- * a tool_use block. Pi's serializer can produce this ordering, so we split
98
- * the message into two consecutive assistant turns: one with text blocks and
99
- * one with tool_use blocks. The reordering is safe because the text and
100
- * tool_use blocks are semantically independent within a single turn.
101
- */
102
- function splitAssistantToolUseTrailingContent(messages) {
103
- return messages.flatMap((message) => {
104
- if (message.role !== "assistant" || !Array.isArray(message.content)) {
105
- return [message];
106
- }
107
- const firstToolUseIndex = message.content.findIndex((block) => block?.type === "tool_use");
108
- if (firstToolUseIndex === -1) {
109
- return [message];
110
- }
111
- const trailingBlocks = message.content.slice(firstToolUseIndex);
112
- if (!trailingBlocks.some((block) => block?.type !== "tool_use")) {
113
- return [message];
114
- }
115
- const nonToolUseBlocks = message.content.filter((block) => block?.type !== "tool_use");
116
- const toolUseBlocks = message.content.filter((block) => block?.type === "tool_use");
117
- return [
118
- { ...message, content: nonToolUseBlocks },
119
- { ...message, content: toolUseBlocks },
120
- ];
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
- }
153
- export function shapeAnthropicOAuthPayload(payload) {
154
- if (!isAnthropicMessagesPayload(payload)) {
155
- return payload;
156
- }
157
- const messages = payload.messages;
158
- if (!isOAuthAnthropicPayload(payload)) {
159
- return payload;
160
- }
161
- const normalizedMessages = splitAssistantToolUseTrailingContent(messages);
162
- const shapedSystem = Array.isArray(payload.system)
163
- ? shapeSystemBlocks(payload.system)
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
- }
184
- return {
185
- ...payload,
186
- messages: normalizedMessages,
187
- system: finalSystem,
188
- };
189
- }
@@ -1,53 +0,0 @@
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
- };
13
- /**
14
- * Sanitize system prompt text by removing paragraphs containing known
15
- * Pi-specific anchor strings and applying inline text replacements for
16
- * known Anthropic classifier trigger phrases.
17
- *
18
- * A paragraph is any text between blank lines (`\n\n`).
19
- *
20
- * This approach is resilient to upstream rewording — as long as the anchor
21
- * string still appears somewhere in the paragraph, removal works regardless
22
- * of how the surrounding text changes.
23
- */
24
- export declare function sanitizeSystemTextWithReport(text: string): SanitizedSystemTextReport;
25
- export declare function sanitizeSystemText(text: string): string;
26
- /**
27
- * Shape a system prompt string for Anthropic OAuth compatibility.
28
- *
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.
34
- *
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.
38
- */
39
- export declare function shapeAnthropicOAuthSystemPrompt(systemPrompt: string): string;
40
- type TextBlock = {
41
- type: "text";
42
- text: string;
43
- [key: string]: unknown;
44
- };
45
- /**
46
- * Apply system prompt shaping to an array of Anthropic system text blocks.
47
- *
48
- * Finds the first block containing Pi's default prompt preamble and replaces
49
- * its text in-place (returning a new array). Blocks without the preamble are
50
- * passed through unchanged.
51
- */
52
- export declare function shapeSystemBlocks(blocks: TextBlock[]): TextBlock[];
53
- export {};
@@ -1,146 +0,0 @@
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
- }
28
- /**
29
- * Sanitize system prompt text by removing paragraphs containing known
30
- * Pi-specific anchor strings and applying inline text replacements for
31
- * known Anthropic classifier trigger phrases.
32
- *
33
- * A paragraph is any text between blank lines (`\n\n`).
34
- *
35
- * This approach is resilient to upstream rewording — as long as the anchor
36
- * string still appears somewhere in the paragraph, removal works regardless
37
- * of how the surrounding text changes.
38
- */
39
- export function sanitizeSystemTextWithReport(text) {
40
- const paragraphs = text.split(/\n\n+/);
41
- const removedParagraphs = [];
42
- const filtered = paragraphs.filter((paragraph) => {
43
- for (const anchor of PARAGRAPH_REMOVAL_ANCHORS) {
44
- if (!paragraph.includes(anchor)) {
45
- continue;
46
- }
47
- removedParagraphs.push({
48
- anchor,
49
- preview: previewParagraph(paragraph),
50
- });
51
- return false;
52
- }
53
- return true;
54
- });
55
- let result = filtered.join("\n\n");
56
- const replacementMatches = [];
57
- for (const rule of TEXT_REPLACEMENTS) {
58
- if (result.includes(rule.match)) {
59
- replacementMatches.push(rule.match);
60
- }
61
- result = result.replaceAll(rule.match, rule.replacement);
62
- }
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);
75
- }
76
- /**
77
- * Shape a system prompt string for Anthropic OAuth compatibility.
78
- *
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.
84
- *
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.
88
- */
89
- export function shapeAnthropicOAuthSystemPrompt(systemPrompt) {
90
- const prefixIdx = systemPrompt.indexOf(PI_DEFAULT_PROMPT_PREFIX);
91
- if (prefixIdx === -1) {
92
- return systemPrompt;
93
- }
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) {
127
- return MINIMAL_ANTHROPIC_OAUTH_PROMPT;
128
- }
129
- return `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}${systemPrompt.slice(projectContextStart)}`;
130
- }
131
- /**
132
- * Apply system prompt shaping to an array of Anthropic system text blocks.
133
- *
134
- * Finds the first block containing Pi's default prompt preamble and replaces
135
- * its text in-place (returning a new array). Blocks without the preamble are
136
- * passed through unchanged.
137
- */
138
- export function shapeSystemBlocks(blocks) {
139
- return blocks.map((block) => {
140
- if (block.type !== "text" ||
141
- !block.text.includes(PI_DEFAULT_PROMPT_PREFIX)) {
142
- return block;
143
- }
144
- return { ...block, text: shapeAnthropicOAuthSystemPrompt(block.text) };
145
- });
146
- }