@cortexkit/pi-anthropic-auth 1.5.0 → 1.6.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.
Files changed (2) hide show
  1. package/dist/convert.js +46 -21
  2. package/package.json +3 -2
package/dist/convert.js CHANGED
@@ -12,15 +12,27 @@ const CLAUDE_CODE_TOOLS = new Map([
12
12
  'WebSearch',
13
13
  ].map((name) => [name.toLowerCase(), name]));
14
14
  function sanitize(text) {
15
- return text.replace(/[\uD800-\uDFFF]/g, '\uFFFD');
15
+ return text.replace(/[\uD800-\uDFFF]/gu, '\uFFFD');
16
16
  }
17
+ /**
18
+ * Detect lone (unpaired) UTF-16 surrogates. With the `u` flag the character
19
+ * class only matches surrogates that are NOT part of a valid pair, since valid
20
+ * pairs are folded into a single astral code point. Anthropic rejects payloads
21
+ * containing lone surrogates (invalid UTF-8).
22
+ */
23
+ function hasLoneSurrogate(text) {
24
+ return /[\uD800-\uDFFF]/u.test(text);
25
+ }
26
+ /**
27
+ * Sanitize a tool-call ID to match Anthropic's `^[a-zA-Z0-9_-]+$` pattern.
28
+ * Cross-provider IDs (e.g. OpenAI Codex `call_xxx|fc_xxx`) contain characters
29
+ * Anthropic rejects. Deterministic — same input always yields the same output.
30
+ */
17
31
  function sanitizeToolId(id) {
18
32
  if (!id)
19
33
  return 'tool_call_unknown';
20
- const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, '_');
21
- if (!sanitized)
22
- return 'tool_call_unknown';
23
- return sanitized.slice(0, 256);
34
+ const cleaned = id.replace(/[^a-zA-Z0-9_-]/g, '_');
35
+ return cleaned.length > 256 ? cleaned.slice(0, 256) : cleaned;
24
36
  }
25
37
  function toClaudeCodeToolName(name) {
26
38
  return CLAUDE_CODE_TOOLS.get(name.toLowerCase()) ?? name;
@@ -86,14 +98,23 @@ function convertMessages(messages) {
86
98
  }
87
99
  else if (block.type === 'thinking' && block.thinking.trim()) {
88
100
  const thinking = block;
89
- if (thinking.thinkingSignature) {
101
+ if (thinking.thinkingSignature &&
102
+ !hasLoneSurrogate(thinking.thinking)) {
103
+ // Signed thinking blocks must be sent back verbatim — the signature
104
+ // is computed over the original text. Sanitizing would alter it and
105
+ // Anthropic rejects the block as "modified". Anthropic-origin
106
+ // thinking is valid UTF-8, so this is the normal path.
90
107
  blocks.push({
91
108
  type: 'thinking',
92
- thinking: sanitize(thinking.thinking),
109
+ thinking: thinking.thinking,
93
110
  signature: thinking.thinkingSignature,
94
111
  });
95
112
  }
96
113
  else {
114
+ // Either unsigned, or signed-but-contains a lone surrogate. In the
115
+ // latter case we cannot keep the signature: sanitizing breaks it and
116
+ // sending the raw lone surrogate is an invalid-UTF8 400. Drop the
117
+ // signature and downgrade to sanitized text.
97
118
  blocks.push({ type: 'text', text: sanitize(thinking.thinking) });
98
119
  }
99
120
  }
@@ -113,34 +134,33 @@ function convertMessages(messages) {
113
134
  if (message.role === 'toolResult') {
114
135
  const toolResult = message;
115
136
  const toolResults = [];
116
- const firstContent = convertTextAndImages(toolResult.content);
117
- const firstContentArr = Array.isArray(firstContent)
118
- ? firstContent
119
- : [{ type: 'text', text: firstContent }];
120
- if (toolResult.isError && firstContentArr.length === 0) {
121
- firstContentArr.push({ type: 'text', text: 'Error' });
137
+ let content = convertTextAndImages(toolResult.content);
138
+ // Anthropic rejects tool_result with is_error=true but empty content
139
+ if (toolResult.isError &&
140
+ (!content || (Array.isArray(content) && content.length === 0))) {
141
+ content = [{ type: 'text', text: 'Error' }];
122
142
  }
123
143
  toolResults.push({
124
144
  type: 'tool_result',
125
145
  tool_use_id: sanitizeToolId(toolResult.toolCallId),
126
- content: firstContentArr,
146
+ content: content,
127
147
  is_error: toolResult.isError,
128
148
  });
129
149
  let nextIndex = index + 1;
130
150
  while (nextIndex < messages.length &&
131
151
  messages[nextIndex]?.role === 'toolResult') {
132
152
  const next = messages[nextIndex];
133
- const nextContent = convertTextAndImages(next.content);
134
- const nextContentArr = Array.isArray(nextContent)
135
- ? nextContent
136
- : [{ type: 'text', text: nextContent }];
137
- if (next.isError && nextContentArr.length === 0) {
138
- nextContentArr.push({ type: 'text', text: 'Error' });
153
+ let nextContent = convertTextAndImages(next.content);
154
+ // Anthropic rejects tool_result with is_error=true but empty content
155
+ if (next.isError &&
156
+ (!nextContent ||
157
+ (Array.isArray(nextContent) && nextContent.length === 0))) {
158
+ nextContent = [{ type: 'text', text: 'Error' }];
139
159
  }
140
160
  toolResults.push({
141
161
  type: 'tool_result',
142
162
  tool_use_id: sanitizeToolId(next.toolCallId),
143
- content: nextContentArr,
163
+ content: nextContent,
144
164
  is_error: next.isError,
145
165
  });
146
166
  nextIndex += 1;
@@ -215,6 +235,11 @@ function applyCacheMode(body, enabled, mode) {
215
235
  }
216
236
  export async function buildAnthropicRequest(modelId, context, options, cache, fastModeEnabled = false, identity) {
217
237
  const messages = convertMessages(context.messages);
238
+ // Strip trailing assistant messages — Anthropic rejects prefill on some models
239
+ while (messages.length &&
240
+ messages[messages.length - 1]?.role === 'assistant') {
241
+ messages.pop();
242
+ }
218
243
  const system = [
219
244
  {
220
245
  type: 'text',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/pi-anthropic-auth",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,10 +32,11 @@
32
32
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
33
33
  "typecheck": "tsc",
34
34
  "types": "tsc",
35
+ "test": "bun test src/tests",
35
36
  "prepublishOnly": "bun run build"
36
37
  },
37
38
  "dependencies": {
38
- "@cortexkit/anthropic-auth-core": "1.5.0"
39
+ "@cortexkit/anthropic-auth-core": "1.6.1"
39
40
  },
40
41
  "peerDependencies": {
41
42
  "@earendil-works/pi-ai": "*",