ai-sdk-provider-claude-code 1.1.3 → 1.1.4

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 CHANGED
@@ -121,10 +121,19 @@ Key changes:
121
121
  - 🔧 Tool management (MCP servers, permissions)
122
122
  - 🧩 Callbacks (hooks, canUseTool)
123
123
 
124
+ ## Image Inputs (Streaming Only)
125
+
126
+ - Enable streaming input (`streamingInput: 'always'` or provide `canUseTool`) before sending images.
127
+ - Supported payloads: data URLs (`data:image/png;base64,...`), strings prefixed with `base64:<mediaType>,<data>`, or objects `{ data: '<base64>', mimeType: 'image/png' }`.
128
+ - Remote HTTP(S) image URLs are ignored with the warning "Image URLs are not supported by this provider; supply base64/data URLs." (`supportsImageUrls` remains `false`).
129
+ - When streaming input is disabled, image parts trigger the streaming prerequisite warning and are omitted from the request.
130
+ - Use realistic image payloads—very small placeholders may result in the model asking for a different image.
131
+ - `examples/images.ts` accepts a local image path and converts it to a data URL on the fly: `npx tsx examples/images.ts /absolute/path/to/image.png`.
132
+
124
133
  ## Limitations
125
134
 
126
135
  - Requires Node.js ≥ 18
127
- - No image support
136
+ - Image inputs require streaming mode with base64/data URLs (remote fetch is not supported)
128
137
  - Some AI SDK parameters unsupported (temperature, maxTokens, etc.)
129
138
  - `canUseTool` requires streaming input at the SDK level (AsyncIterable prompt). This provider supports it via `streamingInput`: use `'auto'` (default when `canUseTool` is set) or `'always'`. See GUIDE for details.
130
139
 
package/dist/index.cjs CHANGED
@@ -43,26 +43,176 @@ var import_provider2 = require("@ai-sdk/provider");
43
43
  var import_provider_utils = require("@ai-sdk/provider-utils");
44
44
 
45
45
  // src/convert-to-claude-code-messages.ts
46
+ var IMAGE_URL_WARNING = "Image URLs are not supported by this provider; supply base64/data URLs.";
47
+ var IMAGE_CONVERSION_WARNING = "Unable to convert image content; supply base64/data URLs.";
48
+ function normalizeBase64(base64) {
49
+ return base64.replace(/\s+/g, "");
50
+ }
51
+ function isImageMimeType(mimeType) {
52
+ return typeof mimeType === "string" && mimeType.trim().toLowerCase().startsWith("image/");
53
+ }
54
+ function createImageContent(mediaType, data) {
55
+ const trimmedType = mediaType.trim();
56
+ const trimmedData = normalizeBase64(data.trim());
57
+ if (!trimmedType || !trimmedData) {
58
+ return void 0;
59
+ }
60
+ return {
61
+ type: "image",
62
+ source: {
63
+ type: "base64",
64
+ media_type: trimmedType,
65
+ data: trimmedData
66
+ }
67
+ };
68
+ }
69
+ function extractMimeType(candidate) {
70
+ if (typeof candidate === "string" && candidate.trim()) {
71
+ return candidate.trim();
72
+ }
73
+ return void 0;
74
+ }
75
+ function parseObjectImage(imageObj, fallbackMimeType) {
76
+ const data = typeof imageObj.data === "string" ? imageObj.data : void 0;
77
+ const mimeType = extractMimeType(
78
+ imageObj.mimeType ?? imageObj.mediaType ?? imageObj.media_type ?? fallbackMimeType
79
+ );
80
+ if (!data || !mimeType) {
81
+ return void 0;
82
+ }
83
+ return createImageContent(mimeType, data);
84
+ }
85
+ function parseStringImage(value, fallbackMimeType) {
86
+ const trimmed = value.trim();
87
+ if (/^https?:\/\//i.test(trimmed)) {
88
+ return { warning: IMAGE_URL_WARNING };
89
+ }
90
+ const dataUrlMatch = trimmed.match(/^data:([^;]+);base64,(.+)$/i);
91
+ if (dataUrlMatch) {
92
+ const [, mediaType, data] = dataUrlMatch;
93
+ const content = createImageContent(mediaType, data);
94
+ return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
95
+ }
96
+ const base64Match = trimmed.match(/^base64:([^,]+),(.+)$/i);
97
+ if (base64Match) {
98
+ const [, explicitMimeType, data] = base64Match;
99
+ const content = createImageContent(explicitMimeType, data);
100
+ return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
101
+ }
102
+ if (fallbackMimeType) {
103
+ const content = createImageContent(fallbackMimeType, trimmed);
104
+ if (content) {
105
+ return { content };
106
+ }
107
+ }
108
+ return { warning: IMAGE_CONVERSION_WARNING };
109
+ }
110
+ function parseImagePart(part) {
111
+ if (!part || typeof part !== "object") {
112
+ return { warning: IMAGE_CONVERSION_WARNING };
113
+ }
114
+ const imageValue = part.image;
115
+ const mimeType = extractMimeType(part.mimeType);
116
+ if (typeof imageValue === "string") {
117
+ return parseStringImage(imageValue, mimeType);
118
+ }
119
+ if (imageValue && typeof imageValue === "object") {
120
+ const content = parseObjectImage(imageValue, mimeType);
121
+ return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
122
+ }
123
+ return { warning: IMAGE_CONVERSION_WARNING };
124
+ }
125
+ function convertBinaryToBase64(data) {
126
+ if (typeof Buffer !== "undefined") {
127
+ const buffer = data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(new Uint8Array(data));
128
+ return buffer.toString("base64");
129
+ }
130
+ if (typeof btoa === "function") {
131
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
132
+ let binary = "";
133
+ const chunkSize = 32768;
134
+ for (let i = 0; i < bytes.length; i += chunkSize) {
135
+ const chunk = bytes.subarray(i, i + chunkSize);
136
+ binary += String.fromCharCode(...chunk);
137
+ }
138
+ return btoa(binary);
139
+ }
140
+ return void 0;
141
+ }
142
+ function parseFilePart(part) {
143
+ const mimeType = extractMimeType(part.mediaType ?? part.mimeType);
144
+ if (!mimeType || !isImageMimeType(mimeType)) {
145
+ return {};
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
+ if (data instanceof Uint8Array || typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer) {
153
+ const base64 = convertBinaryToBase64(data);
154
+ if (!base64) {
155
+ return { warning: IMAGE_CONVERSION_WARNING };
156
+ }
157
+ const content = createImageContent(mimeType, base64);
158
+ return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
159
+ }
160
+ return { warning: IMAGE_CONVERSION_WARNING };
161
+ }
46
162
  function convertToClaudeCodeMessages(prompt, mode = { type: "regular" }, jsonSchema) {
47
163
  const messages = [];
48
164
  const warnings = [];
49
165
  let systemPrompt;
166
+ const streamingSegments = [];
167
+ const imageMap = /* @__PURE__ */ new Map();
168
+ let hasImageParts = false;
169
+ const addSegment = (formatted) => {
170
+ streamingSegments.push({ formatted });
171
+ return streamingSegments.length - 1;
172
+ };
173
+ const addImageForSegment = (segmentIndex, content) => {
174
+ hasImageParts = true;
175
+ if (!imageMap.has(segmentIndex)) {
176
+ imageMap.set(segmentIndex, []);
177
+ }
178
+ imageMap.get(segmentIndex)?.push(content);
179
+ };
50
180
  for (const message of prompt) {
51
181
  switch (message.role) {
52
182
  case "system":
53
183
  systemPrompt = message.content;
184
+ if (typeof message.content === "string" && message.content.trim().length > 0) {
185
+ addSegment(message.content);
186
+ } else {
187
+ addSegment("");
188
+ }
54
189
  break;
55
190
  case "user":
56
191
  if (typeof message.content === "string") {
57
192
  messages.push(message.content);
193
+ addSegment(`Human: ${message.content}`);
58
194
  } else {
59
195
  const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
196
+ const segmentIndex = addSegment(textParts ? `Human: ${textParts}` : "");
60
197
  if (textParts) {
61
198
  messages.push(textParts);
62
199
  }
63
- const imageParts = message.content.filter((part) => part.type === "image");
64
- if (imageParts.length > 0) {
65
- warnings.push("Claude Code SDK does not support image inputs. Images will be ignored.");
200
+ for (const part of message.content) {
201
+ if (part.type === "image") {
202
+ const { content, warning } = parseImagePart(part);
203
+ if (content) {
204
+ addImageForSegment(segmentIndex, content);
205
+ } else if (warning) {
206
+ warnings.push(warning);
207
+ }
208
+ } else if (part.type === "file") {
209
+ const { content, warning } = parseFilePart(part);
210
+ if (content) {
211
+ addImageForSegment(segmentIndex, content);
212
+ } else if (warning) {
213
+ warnings.push(warning);
214
+ }
215
+ }
66
216
  }
67
217
  }
68
218
  break;
@@ -81,13 +231,17 @@ function convertToClaudeCodeMessages(prompt, mode = { type: "regular" }, jsonSch
81
231
  [Tool calls made]`;
82
232
  }
83
233
  }
84
- messages.push(`Assistant: ${assistantContent}`);
234
+ const formattedAssistant = `Assistant: ${assistantContent}`;
235
+ messages.push(formattedAssistant);
236
+ addSegment(formattedAssistant);
85
237
  break;
86
238
  }
87
239
  case "tool":
88
240
  for (const tool3 of message.content) {
89
241
  const resultText = tool3.output.type === "text" ? tool3.output.value : JSON.stringify(tool3.output.value);
90
- messages.push(`Tool Result (${tool3.toolName}): ${resultText}`);
242
+ const formattedToolResult = `Tool Result (${tool3.toolName}): ${resultText}`;
243
+ messages.push(formattedToolResult);
244
+ addSegment(formattedToolResult);
91
245
  }
92
246
  break;
93
247
  }
@@ -96,22 +250,67 @@ function convertToClaudeCodeMessages(prompt, mode = { type: "regular" }, jsonSch
96
250
  if (systemPrompt) {
97
251
  finalPrompt = systemPrompt;
98
252
  }
99
- if (messages.length === 0) {
100
- return { messagesPrompt: finalPrompt, systemPrompt };
101
- }
102
- const formattedMessages = [];
103
- for (let i = 0; i < messages.length; i++) {
104
- const msg = messages[i];
105
- if (msg.startsWith("Assistant:") || msg.startsWith("Tool Result")) {
106
- formattedMessages.push(msg);
253
+ if (messages.length > 0) {
254
+ const formattedMessages = [];
255
+ for (let i = 0; i < messages.length; i++) {
256
+ const msg = messages[i];
257
+ if (msg.startsWith("Assistant:") || msg.startsWith("Tool Result")) {
258
+ formattedMessages.push(msg);
259
+ } else {
260
+ formattedMessages.push(`Human: ${msg}`);
261
+ }
262
+ }
263
+ if (finalPrompt) {
264
+ const joinedMessages = formattedMessages.join("\n\n");
265
+ finalPrompt = joinedMessages ? `${finalPrompt}
266
+
267
+ ${joinedMessages}` : finalPrompt;
107
268
  } else {
108
- formattedMessages.push(`Human: ${msg}`);
269
+ finalPrompt = formattedMessages.join("\n\n");
109
270
  }
110
271
  }
111
- if (finalPrompt) {
112
- finalPrompt = finalPrompt + "\n\n" + formattedMessages.join("\n\n");
113
- } else {
114
- finalPrompt = formattedMessages.join("\n\n");
272
+ let streamingParts = [];
273
+ const imagePartsInOrder = [];
274
+ const appendImagesForIndex = (index) => {
275
+ const images = imageMap.get(index);
276
+ if (!images) {
277
+ return;
278
+ }
279
+ images.forEach((image) => {
280
+ streamingParts.push(image);
281
+ imagePartsInOrder.push(image);
282
+ });
283
+ };
284
+ if (streamingSegments.length > 0) {
285
+ let accumulatedText = "";
286
+ let emittedText = false;
287
+ const flushText = () => {
288
+ if (!accumulatedText) {
289
+ return;
290
+ }
291
+ streamingParts.push({ type: "text", text: accumulatedText });
292
+ accumulatedText = "";
293
+ emittedText = true;
294
+ };
295
+ streamingSegments.forEach((segment, index) => {
296
+ const segmentText = segment.formatted;
297
+ if (segmentText) {
298
+ if (!accumulatedText) {
299
+ accumulatedText = emittedText ? `
300
+
301
+ ${segmentText}` : segmentText;
302
+ } else {
303
+ accumulatedText += `
304
+
305
+ ${segmentText}`;
306
+ }
307
+ }
308
+ if (imageMap.has(index)) {
309
+ flushText();
310
+ appendImagesForIndex(index);
311
+ }
312
+ });
313
+ flushText();
115
314
  }
116
315
  if (mode?.type === "object-json" && jsonSchema) {
117
316
  const schemaStr = JSON.stringify(jsonSchema, null, 2);
@@ -127,11 +326,20 @@ Now, based on the following conversation, generate ONLY the JSON object with the
127
326
  ${finalPrompt}
128
327
 
129
328
  Remember: Your ENTIRE response must be ONLY the JSON object, starting with { and ending with }`;
329
+ streamingParts = [
330
+ { type: "text", text: finalPrompt },
331
+ ...imagePartsInOrder
332
+ ];
130
333
  }
131
334
  return {
132
335
  messagesPrompt: finalPrompt,
133
336
  systemPrompt,
134
- ...warnings.length > 0 && { warnings }
337
+ ...warnings.length > 0 && { warnings },
338
+ streamingContentParts: streamingParts.length > 0 ? streamingParts : [
339
+ { type: "text", text: finalPrompt },
340
+ ...imagePartsInOrder
341
+ ],
342
+ hasImageParts
135
343
  };
136
344
  }
137
345
 
@@ -474,12 +682,14 @@ function isAbortError(err) {
474
682
  }
475
683
  return false;
476
684
  }
477
- function toAsyncIterablePrompt(messagesPrompt, outputStreamEnded, sessionId) {
685
+ var STREAMING_FEATURE_WARNING = "Claude Code SDK features (hooks/MCP/images) require streaming input. Set `streamingInput: 'always'` or provide `canUseTool` (auto streams only when canUseTool is set).";
686
+ function toAsyncIterablePrompt(messagesPrompt, outputStreamEnded, sessionId, contentParts) {
687
+ const content = contentParts && contentParts.length > 0 ? contentParts : [{ type: "text", text: messagesPrompt }];
478
688
  const msg = {
479
689
  type: "user",
480
690
  message: {
481
691
  role: "user",
482
- content: [{ type: "text", text: messagesPrompt }]
692
+ content
483
693
  },
484
694
  parent_tool_use_id: null,
485
695
  session_id: sessionId ?? ""
@@ -673,7 +883,12 @@ var ClaudeCodeLanguageModel = class {
673
883
  }
674
884
  async doGenerate(options) {
675
885
  const mode = options.responseFormat?.type === "json" ? { type: "object-json" } : { type: "regular" };
676
- const { messagesPrompt, warnings: messageWarnings } = convertToClaudeCodeMessages(
886
+ const {
887
+ messagesPrompt,
888
+ warnings: messageWarnings,
889
+ streamingContentParts,
890
+ hasImageParts
891
+ } = convertToClaudeCodeMessages(
677
892
  options.prompt,
678
893
  mode,
679
894
  options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0
@@ -702,18 +917,29 @@ var ClaudeCodeLanguageModel = class {
702
917
  });
703
918
  });
704
919
  }
920
+ const modeSetting = this.settings.streamingInput ?? "auto";
921
+ const wantsStreamInput = modeSetting === "always" || modeSetting === "auto" && !!this.settings.canUseTool;
922
+ if (!wantsStreamInput && hasImageParts) {
923
+ warnings.push({
924
+ type: "other",
925
+ message: STREAMING_FEATURE_WARNING
926
+ });
927
+ }
705
928
  let done = () => {
706
929
  };
707
930
  const outputStreamEnded = new Promise((resolve) => {
708
931
  done = () => resolve(void 0);
709
932
  });
710
933
  try {
711
- const modeSetting = this.settings.streamingInput ?? "auto";
712
- const wantsStream = modeSetting === "always" || modeSetting === "auto" && !!this.settings.canUseTool;
713
934
  if (this.settings.canUseTool && this.settings.permissionPromptToolName) {
714
935
  throw new Error("canUseTool requires streamingInput mode ('auto' or 'always') and cannot be used with permissionPromptToolName (SDK constraint). Set streamingInput: 'auto' (or 'always') and remove permissionPromptToolName, or remove canUseTool.");
715
936
  }
716
- const sdkPrompt = wantsStream ? toAsyncIterablePrompt(messagesPrompt, outputStreamEnded, this.settings.resume ?? this.sessionId) : messagesPrompt;
937
+ const sdkPrompt = wantsStreamInput ? toAsyncIterablePrompt(
938
+ messagesPrompt,
939
+ outputStreamEnded,
940
+ this.settings.resume ?? this.sessionId,
941
+ streamingContentParts
942
+ ) : messagesPrompt;
717
943
  const response = (0, import_claude_code.query)({
718
944
  prompt: sdkPrompt,
719
945
  options: queryOptions
@@ -785,7 +1011,12 @@ var ClaudeCodeLanguageModel = class {
785
1011
  }
786
1012
  async doStream(options) {
787
1013
  const mode = options.responseFormat?.type === "json" ? { type: "object-json" } : { type: "regular" };
788
- const { messagesPrompt, warnings: messageWarnings } = convertToClaudeCodeMessages(
1014
+ const {
1015
+ messagesPrompt,
1016
+ warnings: messageWarnings,
1017
+ streamingContentParts,
1018
+ hasImageParts
1019
+ } = convertToClaudeCodeMessages(
789
1020
  options.prompt,
790
1021
  mode,
791
1022
  options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0
@@ -808,6 +1039,14 @@ var ClaudeCodeLanguageModel = class {
808
1039
  });
809
1040
  });
810
1041
  }
1042
+ const modeSetting = this.settings.streamingInput ?? "auto";
1043
+ const wantsStreamInput = modeSetting === "always" || modeSetting === "auto" && !!this.settings.canUseTool;
1044
+ if (!wantsStreamInput && hasImageParts) {
1045
+ warnings.push({
1046
+ type: "other",
1047
+ message: STREAMING_FEATURE_WARNING
1048
+ });
1049
+ }
811
1050
  const stream = new ReadableStream({
812
1051
  start: async (controller) => {
813
1052
  let done = () => {
@@ -817,12 +1056,15 @@ var ClaudeCodeLanguageModel = class {
817
1056
  });
818
1057
  try {
819
1058
  controller.enqueue({ type: "stream-start", warnings });
820
- const modeSetting = this.settings.streamingInput ?? "auto";
821
- const wantsStream = modeSetting === "always" || modeSetting === "auto" && !!this.settings.canUseTool;
822
1059
  if (this.settings.canUseTool && this.settings.permissionPromptToolName) {
823
1060
  throw new Error("canUseTool requires streamingInput mode ('auto' or 'always') and cannot be used with permissionPromptToolName (SDK constraint). Set streamingInput: 'auto' (or 'always') and remove permissionPromptToolName, or remove canUseTool.");
824
1061
  }
825
- const sdkPrompt = wantsStream ? toAsyncIterablePrompt(messagesPrompt, outputStreamEnded, this.settings.resume ?? this.sessionId) : messagesPrompt;
1062
+ const sdkPrompt = wantsStreamInput ? toAsyncIterablePrompt(
1063
+ messagesPrompt,
1064
+ outputStreamEnded,
1065
+ this.settings.resume ?? this.sessionId,
1066
+ streamingContentParts
1067
+ ) : messagesPrompt;
826
1068
  const response = (0, import_claude_code.query)({
827
1069
  prompt: sdkPrompt,
828
1070
  options: queryOptions