@boostecom/provider 0.0.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 +90 -0
- package/dist/index.cjs +2522 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +848 -0
- package/dist/index.d.ts +848 -0
- package/dist/index.js +2484 -0
- package/dist/index.js.map +1 -0
- package/docs/content/README.md +337 -0
- package/docs/content/agent-teams.mdx +324 -0
- package/docs/content/api.mdx +757 -0
- package/docs/content/best-practices.mdx +624 -0
- package/docs/content/examples.mdx +675 -0
- package/docs/content/guide.mdx +516 -0
- package/docs/content/index.mdx +99 -0
- package/docs/content/installation.mdx +246 -0
- package/docs/content/skills.mdx +548 -0
- package/docs/content/troubleshooting.mdx +588 -0
- package/docs/examples/README.md +499 -0
- package/docs/examples/abort-signal.ts +125 -0
- package/docs/examples/agent-teams.ts +122 -0
- package/docs/examples/basic-usage.ts +73 -0
- package/docs/examples/check-cli.ts +51 -0
- package/docs/examples/conversation-history.ts +69 -0
- package/docs/examples/custom-config.ts +90 -0
- package/docs/examples/generate-object-constraints.ts +209 -0
- package/docs/examples/generate-object.ts +211 -0
- package/docs/examples/hooks-callbacks.ts +63 -0
- package/docs/examples/images.ts +76 -0
- package/docs/examples/integration-test.ts +241 -0
- package/docs/examples/limitations.ts +150 -0
- package/docs/examples/logging-custom-logger.ts +99 -0
- package/docs/examples/logging-default.ts +55 -0
- package/docs/examples/logging-disabled.ts +74 -0
- package/docs/examples/logging-verbose.ts +64 -0
- package/docs/examples/long-running-tasks.ts +179 -0
- package/docs/examples/message-injection.ts +210 -0
- package/docs/examples/mid-stream-injection.ts +126 -0
- package/docs/examples/run-all-examples.sh +48 -0
- package/docs/examples/sdk-tools-callbacks.ts +49 -0
- package/docs/examples/skills-discovery.ts +144 -0
- package/docs/examples/skills-management.ts +140 -0
- package/docs/examples/stream-object.ts +80 -0
- package/docs/examples/streaming.ts +52 -0
- package/docs/examples/structured-output-repro.ts +227 -0
- package/docs/examples/tool-management.ts +215 -0
- package/docs/examples/tool-streaming.ts +132 -0
- package/docs/examples/zod4-compatibility-test.ts +290 -0
- package/docs/src/claude-code-language-model.test.ts +3883 -0
- package/docs/src/claude-code-language-model.ts +2586 -0
- package/docs/src/claude-code-provider.test.ts +97 -0
- package/docs/src/claude-code-provider.ts +179 -0
- package/docs/src/convert-to-claude-code-messages.images.test.ts +104 -0
- package/docs/src/convert-to-claude-code-messages.test.ts +193 -0
- package/docs/src/convert-to-claude-code-messages.ts +419 -0
- package/docs/src/errors.test.ts +213 -0
- package/docs/src/errors.ts +216 -0
- package/docs/src/index.test.ts +49 -0
- package/docs/src/index.ts +98 -0
- package/docs/src/logger.integration.test.ts +164 -0
- package/docs/src/logger.test.ts +184 -0
- package/docs/src/logger.ts +65 -0
- package/docs/src/map-claude-code-finish-reason.test.ts +120 -0
- package/docs/src/map-claude-code-finish-reason.ts +60 -0
- package/docs/src/mcp-helpers.test.ts +71 -0
- package/docs/src/mcp-helpers.ts +123 -0
- package/docs/src/message-injection.test.ts +460 -0
- package/docs/src/types.ts +447 -0
- package/docs/src/validation.test.ts +558 -0
- package/docs/src/validation.ts +360 -0
- package/package.json +124 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import type { ModelMessage } from 'ai';
|
|
2
|
+
import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
3
|
+
|
|
4
|
+
type SDKUserContentPart = SDKUserMessage['message']['content'][number];
|
|
5
|
+
|
|
6
|
+
interface StreamingSegment {
|
|
7
|
+
formatted: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const IMAGE_URL_WARNING = 'Image URLs are not supported by this provider; supply base64/data URLs.';
|
|
11
|
+
const IMAGE_CONVERSION_WARNING = 'Unable to convert image content; supply base64/data URLs.';
|
|
12
|
+
|
|
13
|
+
function normalizeBase64(base64: string): string {
|
|
14
|
+
return base64.replace(/\s+/g, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isImageMimeType(mimeType?: string): boolean {
|
|
18
|
+
return typeof mimeType === 'string' && mimeType.trim().toLowerCase().startsWith('image/');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createImageContent(mediaType: string, data: string): SDKUserContentPart | undefined {
|
|
22
|
+
const trimmedType = mediaType.trim();
|
|
23
|
+
const trimmedData = normalizeBase64(data.trim());
|
|
24
|
+
|
|
25
|
+
if (!trimmedType || !trimmedData) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
type: 'image',
|
|
31
|
+
source: {
|
|
32
|
+
type: 'base64',
|
|
33
|
+
media_type: trimmedType,
|
|
34
|
+
data: trimmedData,
|
|
35
|
+
},
|
|
36
|
+
} as SDKUserContentPart;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractMimeType(candidate: unknown): string | undefined {
|
|
40
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
41
|
+
return candidate.trim();
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseObjectImage(
|
|
47
|
+
imageObj: Record<string, unknown>,
|
|
48
|
+
fallbackMimeType?: string
|
|
49
|
+
): SDKUserContentPart | undefined {
|
|
50
|
+
const data = typeof imageObj.data === 'string' ? imageObj.data : undefined;
|
|
51
|
+
const mimeType = extractMimeType(
|
|
52
|
+
imageObj.mimeType ?? imageObj.mediaType ?? imageObj.media_type ?? fallbackMimeType
|
|
53
|
+
);
|
|
54
|
+
if (!data || !mimeType) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return createImageContent(mimeType, data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseStringImage(
|
|
61
|
+
value: string,
|
|
62
|
+
fallbackMimeType?: string
|
|
63
|
+
): { content?: SDKUserContentPart; warning?: string } {
|
|
64
|
+
const trimmed = value.trim();
|
|
65
|
+
|
|
66
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
67
|
+
return { warning: IMAGE_URL_WARNING };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const dataUrlMatch = trimmed.match(/^data:([^;]+);base64,(.+)$/i);
|
|
71
|
+
if (dataUrlMatch) {
|
|
72
|
+
const [, mediaType, data] = dataUrlMatch;
|
|
73
|
+
const content = createImageContent(mediaType, data);
|
|
74
|
+
return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const base64Match = trimmed.match(/^base64:([^,]+),(.+)$/i);
|
|
78
|
+
if (base64Match) {
|
|
79
|
+
const [, explicitMimeType, data] = base64Match;
|
|
80
|
+
const content = createImageContent(explicitMimeType, data);
|
|
81
|
+
return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (fallbackMimeType) {
|
|
85
|
+
const content = createImageContent(fallbackMimeType, trimmed);
|
|
86
|
+
if (content) {
|
|
87
|
+
return { content };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { warning: IMAGE_CONVERSION_WARNING };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseImagePart(part: unknown): { content?: SDKUserContentPart; warning?: string } {
|
|
95
|
+
if (!part || typeof part !== 'object') {
|
|
96
|
+
return { warning: IMAGE_CONVERSION_WARNING };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const imageValue = (part as { image?: unknown }).image;
|
|
100
|
+
const mimeType = extractMimeType((part as { mimeType?: unknown }).mimeType);
|
|
101
|
+
|
|
102
|
+
if (typeof imageValue === 'string') {
|
|
103
|
+
return parseStringImage(imageValue, mimeType);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (imageValue && typeof imageValue === 'object') {
|
|
107
|
+
const content = parseObjectImage(imageValue as Record<string, unknown>, mimeType);
|
|
108
|
+
return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { warning: IMAGE_CONVERSION_WARNING };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function convertBinaryToBase64(data: Uint8Array | ArrayBuffer): string | undefined {
|
|
115
|
+
if (typeof Buffer !== 'undefined') {
|
|
116
|
+
const buffer =
|
|
117
|
+
data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(new Uint8Array(data));
|
|
118
|
+
return buffer.toString('base64');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof btoa === 'function') {
|
|
122
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
123
|
+
let binary = '';
|
|
124
|
+
const chunkSize = 0x8000;
|
|
125
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
126
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
127
|
+
binary += String.fromCharCode(...chunk);
|
|
128
|
+
}
|
|
129
|
+
return btoa(binary);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type FileLikePart = {
|
|
136
|
+
mediaType?: unknown;
|
|
137
|
+
mimeType?: unknown;
|
|
138
|
+
data?: unknown;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
function parseFilePart(part: FileLikePart): { content?: SDKUserContentPart; warning?: string } {
|
|
142
|
+
const mimeType = extractMimeType(part.mediaType ?? part.mimeType);
|
|
143
|
+
if (!mimeType || !isImageMimeType(mimeType)) {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const data = part.data;
|
|
148
|
+
if (typeof data === 'string') {
|
|
149
|
+
const content = createImageContent(mimeType, data);
|
|
150
|
+
return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
data instanceof Uint8Array ||
|
|
155
|
+
(typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer)
|
|
156
|
+
) {
|
|
157
|
+
const base64 = convertBinaryToBase64(data);
|
|
158
|
+
if (!base64) {
|
|
159
|
+
return { warning: IMAGE_CONVERSION_WARNING };
|
|
160
|
+
}
|
|
161
|
+
const content = createImageContent(mimeType, base64);
|
|
162
|
+
return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { warning: IMAGE_CONVERSION_WARNING };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Converts AI SDK prompt format to Claude Code SDK message format.
|
|
170
|
+
* Handles system prompts, user messages, assistant responses, and tool interactions.
|
|
171
|
+
*
|
|
172
|
+
* @param prompt - The AI SDK prompt containing messages
|
|
173
|
+
* @returns An object containing the formatted message prompt and optional system prompt
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```typescript
|
|
177
|
+
* const { messagesPrompt } = convertToClaudeCodeMessages(
|
|
178
|
+
* [{ role: 'user', content: 'Hello!' }]
|
|
179
|
+
* );
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* @remarks
|
|
183
|
+
* - Image parts are collected for streaming input; unsupported variants produce warnings
|
|
184
|
+
* - Tool calls are simplified to "[Tool calls made]" notation
|
|
185
|
+
* - JSON schema enforcement is handled natively by the SDK's outputFormat option (v0.1.45+)
|
|
186
|
+
*/
|
|
187
|
+
export function convertToClaudeCodeMessages(prompt: readonly ModelMessage[]): {
|
|
188
|
+
messagesPrompt: string;
|
|
189
|
+
systemPrompt?: string;
|
|
190
|
+
warnings?: string[];
|
|
191
|
+
streamingContentParts: SDKUserMessage['message']['content'];
|
|
192
|
+
hasImageParts: boolean;
|
|
193
|
+
} {
|
|
194
|
+
const messages: string[] = [];
|
|
195
|
+
const warnings: string[] = [];
|
|
196
|
+
let systemPrompt: string | undefined;
|
|
197
|
+
const streamingSegments: StreamingSegment[] = [];
|
|
198
|
+
const imageMap = new Map<number, SDKUserContentPart[]>();
|
|
199
|
+
let hasImageParts = false;
|
|
200
|
+
|
|
201
|
+
const addSegment = (formatted: string): number => {
|
|
202
|
+
streamingSegments.push({ formatted });
|
|
203
|
+
return streamingSegments.length - 1;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const addImageForSegment = (segmentIndex: number, content: SDKUserContentPart): void => {
|
|
207
|
+
hasImageParts = true;
|
|
208
|
+
if (!imageMap.has(segmentIndex)) {
|
|
209
|
+
imageMap.set(segmentIndex, []);
|
|
210
|
+
}
|
|
211
|
+
imageMap.get(segmentIndex)?.push(content);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
for (const message of prompt) {
|
|
215
|
+
switch (message.role) {
|
|
216
|
+
case 'system':
|
|
217
|
+
systemPrompt = message.content;
|
|
218
|
+
if (typeof message.content === 'string' && message.content.trim().length > 0) {
|
|
219
|
+
addSegment(message.content);
|
|
220
|
+
} else {
|
|
221
|
+
addSegment('');
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case 'user':
|
|
226
|
+
if (typeof message.content === 'string') {
|
|
227
|
+
messages.push(message.content);
|
|
228
|
+
addSegment(`Human: ${message.content}`);
|
|
229
|
+
} else {
|
|
230
|
+
// Handle multi-part content
|
|
231
|
+
const textParts = message.content
|
|
232
|
+
.filter((part) => part.type === 'text')
|
|
233
|
+
.map((part) => part.text)
|
|
234
|
+
.join('\n');
|
|
235
|
+
|
|
236
|
+
const segmentIndex = addSegment(textParts ? `Human: ${textParts}` : '');
|
|
237
|
+
|
|
238
|
+
if (textParts) {
|
|
239
|
+
messages.push(textParts);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const part of message.content) {
|
|
243
|
+
if (part.type === 'image') {
|
|
244
|
+
const { content, warning } = parseImagePart(part);
|
|
245
|
+
if (content) {
|
|
246
|
+
addImageForSegment(segmentIndex, content);
|
|
247
|
+
} else if (warning) {
|
|
248
|
+
warnings.push(warning);
|
|
249
|
+
}
|
|
250
|
+
} else if (part.type === 'file') {
|
|
251
|
+
const { content, warning } = parseFilePart(part);
|
|
252
|
+
if (content) {
|
|
253
|
+
addImageForSegment(segmentIndex, content);
|
|
254
|
+
} else if (warning) {
|
|
255
|
+
warnings.push(warning);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case 'assistant': {
|
|
263
|
+
let assistantContent = '';
|
|
264
|
+
if (typeof message.content === 'string') {
|
|
265
|
+
assistantContent = message.content;
|
|
266
|
+
} else {
|
|
267
|
+
const textParts = message.content
|
|
268
|
+
.filter((part) => part.type === 'text')
|
|
269
|
+
.map((part) => part.text)
|
|
270
|
+
.join('\n');
|
|
271
|
+
|
|
272
|
+
if (textParts) {
|
|
273
|
+
assistantContent = textParts;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Handle tool calls if present
|
|
277
|
+
const toolCalls = message.content.filter((part) => part.type === 'tool-call');
|
|
278
|
+
if (toolCalls.length > 0) {
|
|
279
|
+
// For now, we'll just note that tool calls were made
|
|
280
|
+
assistantContent += `\n[Tool calls made]`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const formattedAssistant = `Assistant: ${assistantContent}`;
|
|
284
|
+
messages.push(formattedAssistant);
|
|
285
|
+
addSegment(formattedAssistant);
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
case 'tool':
|
|
290
|
+
// Tool results could be included in the conversation
|
|
291
|
+
// Filter out ToolApprovalResponse parts, only process ToolResultPart
|
|
292
|
+
for (const tool of message.content) {
|
|
293
|
+
if (tool.type === 'tool-approval-response') {
|
|
294
|
+
continue; // Skip approval responses
|
|
295
|
+
}
|
|
296
|
+
// Handle different ToolResultOutput types
|
|
297
|
+
let resultText: string;
|
|
298
|
+
const output = tool.output;
|
|
299
|
+
if (output.type === 'text' || output.type === 'error-text') {
|
|
300
|
+
resultText = output.value;
|
|
301
|
+
} else if (output.type === 'json' || output.type === 'error-json') {
|
|
302
|
+
resultText = JSON.stringify(output.value);
|
|
303
|
+
} else if (output.type === 'execution-denied') {
|
|
304
|
+
resultText = `[Execution denied${output.reason ? `: ${output.reason}` : ''}]`;
|
|
305
|
+
} else if (output.type === 'content') {
|
|
306
|
+
// Handle content array - extract text parts
|
|
307
|
+
resultText = output.value
|
|
308
|
+
.filter((part): part is { type: 'text'; text: string } => part.type === 'text')
|
|
309
|
+
.map((part) => part.text)
|
|
310
|
+
.join('\n');
|
|
311
|
+
} else {
|
|
312
|
+
resultText = '[Unknown output type]';
|
|
313
|
+
}
|
|
314
|
+
const formattedToolResult = `Tool Result (${tool.toolName}): ${resultText}`;
|
|
315
|
+
messages.push(formattedToolResult);
|
|
316
|
+
addSegment(formattedToolResult);
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// For the SDK, we need to provide a single prompt string
|
|
323
|
+
// Format the conversation history properly
|
|
324
|
+
|
|
325
|
+
// Combine system prompt with messages
|
|
326
|
+
let finalPrompt = '';
|
|
327
|
+
|
|
328
|
+
// Add system prompt at the beginning if present
|
|
329
|
+
if (systemPrompt) {
|
|
330
|
+
finalPrompt = systemPrompt;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (messages.length > 0) {
|
|
334
|
+
// Format messages
|
|
335
|
+
const formattedMessages = [];
|
|
336
|
+
for (let i = 0; i < messages.length; i++) {
|
|
337
|
+
const msg = messages[i];
|
|
338
|
+
// Check if this is a user or assistant message based on content
|
|
339
|
+
if (msg.startsWith('Assistant:') || msg.startsWith('Tool Result')) {
|
|
340
|
+
formattedMessages.push(msg);
|
|
341
|
+
} else {
|
|
342
|
+
// User messages
|
|
343
|
+
formattedMessages.push(`Human: ${msg}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Combine system prompt with messages
|
|
348
|
+
if (finalPrompt) {
|
|
349
|
+
const joinedMessages = formattedMessages.join('\n\n');
|
|
350
|
+
finalPrompt = joinedMessages ? `${finalPrompt}\n\n${joinedMessages}` : finalPrompt;
|
|
351
|
+
} else {
|
|
352
|
+
finalPrompt = formattedMessages.join('\n\n');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Build streaming parts including text and images
|
|
357
|
+
const streamingParts: SDKUserContentPart[] = [];
|
|
358
|
+
const imagePartsInOrder: SDKUserContentPart[] = [];
|
|
359
|
+
|
|
360
|
+
const appendImagesForIndex = (index: number) => {
|
|
361
|
+
const images = imageMap.get(index);
|
|
362
|
+
if (!images) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
images.forEach((image) => {
|
|
366
|
+
streamingParts.push(image);
|
|
367
|
+
imagePartsInOrder.push(image);
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (streamingSegments.length > 0) {
|
|
372
|
+
let accumulatedText = '';
|
|
373
|
+
let emittedText = false;
|
|
374
|
+
|
|
375
|
+
const flushText = () => {
|
|
376
|
+
if (!accumulatedText) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
streamingParts.push({ type: 'text', text: accumulatedText });
|
|
380
|
+
accumulatedText = '';
|
|
381
|
+
emittedText = true;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
streamingSegments.forEach((segment, index) => {
|
|
385
|
+
const segmentText = segment.formatted;
|
|
386
|
+
if (segmentText) {
|
|
387
|
+
if (!accumulatedText) {
|
|
388
|
+
accumulatedText = emittedText ? `\n\n${segmentText}` : segmentText;
|
|
389
|
+
} else {
|
|
390
|
+
accumulatedText += `\n\n${segmentText}`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (imageMap.has(index)) {
|
|
395
|
+
flushText();
|
|
396
|
+
appendImagesForIndex(index);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
flushText();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Note: JSON schema enforcement is now handled natively by the SDK's outputFormat option (v0.1.45+)
|
|
404
|
+
// No prompt injection needed - structured outputs are guaranteed by the SDK
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
messagesPrompt: finalPrompt,
|
|
408
|
+
systemPrompt,
|
|
409
|
+
...(warnings.length > 0 && { warnings }),
|
|
410
|
+
streamingContentParts:
|
|
411
|
+
streamingParts.length > 0
|
|
412
|
+
? (streamingParts as SDKUserMessage['message']['content'])
|
|
413
|
+
: ([
|
|
414
|
+
{ type: 'text', text: finalPrompt },
|
|
415
|
+
...imagePartsInOrder,
|
|
416
|
+
] as SDKUserMessage['message']['content']),
|
|
417
|
+
hasImageParts,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createAPICallError,
|
|
4
|
+
createAuthenticationError,
|
|
5
|
+
createTimeoutError,
|
|
6
|
+
isAuthenticationError,
|
|
7
|
+
isTimeoutError,
|
|
8
|
+
getErrorMetadata,
|
|
9
|
+
} from './errors.js';
|
|
10
|
+
import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
|
|
11
|
+
|
|
12
|
+
describe('Error Creation Functions', () => {
|
|
13
|
+
describe('createAPICallError', () => {
|
|
14
|
+
it('should create APICallError with message and details', () => {
|
|
15
|
+
const error = createAPICallError({
|
|
16
|
+
message: 'Test error',
|
|
17
|
+
exitCode: 1,
|
|
18
|
+
stderr: 'Command failed',
|
|
19
|
+
promptExcerpt: 'test prompt',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(error).toBeInstanceOf(APICallError);
|
|
23
|
+
expect(error.message).toBe('Test error');
|
|
24
|
+
expect(error.isRetryable).toBe(false);
|
|
25
|
+
expect(error.data).toEqual({
|
|
26
|
+
exitCode: 1,
|
|
27
|
+
stderr: 'Command failed',
|
|
28
|
+
promptExcerpt: 'test prompt',
|
|
29
|
+
code: undefined,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle optional parameters', () => {
|
|
34
|
+
const error = createAPICallError({
|
|
35
|
+
message: 'Minimal error',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(error).toBeInstanceOf(APICallError);
|
|
39
|
+
expect(error.message).toBe('Minimal error');
|
|
40
|
+
expect(error.requestBodyValues).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should set retryable flag', () => {
|
|
44
|
+
const error = createAPICallError({
|
|
45
|
+
message: 'Retryable error',
|
|
46
|
+
isRetryable: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(error.isRetryable).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('createAuthenticationError', () => {
|
|
54
|
+
it('should create LoadAPIKeyError for authentication', () => {
|
|
55
|
+
const error = createAuthenticationError({
|
|
56
|
+
message: 'Auth failed',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(error).toBeInstanceOf(LoadAPIKeyError);
|
|
60
|
+
expect(error.message).toBe('Auth failed');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should use default message when empty', () => {
|
|
64
|
+
const error = createAuthenticationError({
|
|
65
|
+
message: '',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(error.message).toBe(
|
|
69
|
+
'Authentication failed. Please ensure Claude Code SDK is properly authenticated.'
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('createTimeoutError', () => {
|
|
75
|
+
it('should create retryable APICallError for timeout', () => {
|
|
76
|
+
const error = createTimeoutError({
|
|
77
|
+
message: 'Request timed out after 2 minutes',
|
|
78
|
+
timeoutMs: 120000,
|
|
79
|
+
promptExcerpt: 'test prompt',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(error).toBeInstanceOf(APICallError);
|
|
83
|
+
expect(error.message).toBe('Request timed out after 2 minutes');
|
|
84
|
+
expect(error.isRetryable).toBe(true);
|
|
85
|
+
expect(error.data).toMatchObject({
|
|
86
|
+
code: 'TIMEOUT',
|
|
87
|
+
timeoutMs: 120000,
|
|
88
|
+
promptExcerpt: 'test prompt',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should work without prompt excerpt', () => {
|
|
93
|
+
const error = createTimeoutError({
|
|
94
|
+
message: 'Timeout',
|
|
95
|
+
timeoutMs: 60000,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(error.requestBodyValues).toBeUndefined();
|
|
99
|
+
expect((error.data as any).timeoutMs).toBe(60000);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('Error Detection Functions', () => {
|
|
105
|
+
describe('isAuthenticationError', () => {
|
|
106
|
+
it('should detect LoadAPIKeyError', () => {
|
|
107
|
+
const error = new LoadAPIKeyError({ message: 'Auth failed' });
|
|
108
|
+
expect(isAuthenticationError(error)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should detect APICallError with exit code 401', () => {
|
|
112
|
+
const error = new APICallError({
|
|
113
|
+
message: 'Unauthorized',
|
|
114
|
+
url: 'test-url',
|
|
115
|
+
requestBodyValues: {},
|
|
116
|
+
isRetryable: false,
|
|
117
|
+
data: { exitCode: 401 },
|
|
118
|
+
});
|
|
119
|
+
expect(isAuthenticationError(error)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should return false for other errors', () => {
|
|
123
|
+
expect(isAuthenticationError(new Error('Generic error'))).toBe(false);
|
|
124
|
+
expect(
|
|
125
|
+
isAuthenticationError(
|
|
126
|
+
new APICallError({
|
|
127
|
+
message: 'Not auth',
|
|
128
|
+
url: 'test-url',
|
|
129
|
+
requestBodyValues: {},
|
|
130
|
+
isRetryable: false,
|
|
131
|
+
data: { exitCode: 1 },
|
|
132
|
+
})
|
|
133
|
+
)
|
|
134
|
+
).toBe(false);
|
|
135
|
+
expect(isAuthenticationError(null)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('isTimeoutError', () => {
|
|
140
|
+
it('should detect APICallError with TIMEOUT code', () => {
|
|
141
|
+
const error = new APICallError({
|
|
142
|
+
message: 'Timeout',
|
|
143
|
+
url: 'test-url',
|
|
144
|
+
requestBodyValues: {},
|
|
145
|
+
isRetryable: true,
|
|
146
|
+
data: { code: 'TIMEOUT' },
|
|
147
|
+
});
|
|
148
|
+
expect(isTimeoutError(error)).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return false for non-timeout errors', () => {
|
|
152
|
+
expect(isTimeoutError(new Error('Not timeout'))).toBe(false);
|
|
153
|
+
expect(
|
|
154
|
+
isTimeoutError(
|
|
155
|
+
new APICallError({
|
|
156
|
+
message: 'Other error',
|
|
157
|
+
url: 'test-url',
|
|
158
|
+
requestBodyValues: {},
|
|
159
|
+
isRetryable: false,
|
|
160
|
+
data: { code: 'OTHER' },
|
|
161
|
+
})
|
|
162
|
+
)
|
|
163
|
+
).toBe(false);
|
|
164
|
+
expect(isTimeoutError(null)).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('getErrorMetadata', () => {
|
|
170
|
+
it('should extract metadata from APICallError', () => {
|
|
171
|
+
const error = new APICallError({
|
|
172
|
+
message: 'API call failed',
|
|
173
|
+
url: 'test-url',
|
|
174
|
+
requestBodyValues: {},
|
|
175
|
+
isRetryable: false,
|
|
176
|
+
data: {
|
|
177
|
+
exitCode: 1,
|
|
178
|
+
stderr: 'error output',
|
|
179
|
+
code: 'ENOENT',
|
|
180
|
+
custom: 'data',
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const metadata = getErrorMetadata(error);
|
|
185
|
+
|
|
186
|
+
expect(metadata).toEqual({
|
|
187
|
+
exitCode: 1,
|
|
188
|
+
stderr: 'error output',
|
|
189
|
+
code: 'ENOENT',
|
|
190
|
+
custom: 'data',
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should return undefined for non-APICallError', () => {
|
|
195
|
+
const regularError = new Error('Regular error');
|
|
196
|
+
expect(getErrorMetadata(regularError)).toBeUndefined();
|
|
197
|
+
|
|
198
|
+
const customError = { message: 'Custom error' };
|
|
199
|
+
expect(getErrorMetadata(customError)).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle APICallError without data', () => {
|
|
203
|
+
const error = new APICallError({
|
|
204
|
+
message: 'API call failed',
|
|
205
|
+
url: 'test-url',
|
|
206
|
+
requestBodyValues: {},
|
|
207
|
+
isRetryable: false,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const metadata = getErrorMetadata(error);
|
|
211
|
+
expect(metadata).toBeUndefined();
|
|
212
|
+
});
|
|
213
|
+
});
|