ai-sdk-ollama 3.6.0 → 3.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 70def97: Support media/image content in tool result messages. When a tool returns `output.type === 'content'` with `image-data`, `image-url`, or `file-data` (image) parts, the provider now sends them in the tool message's `images` array to Ollama. Fixes #527.
8
+
9
+ ## 3.7.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 229a396: Use jsonrepair for tool-call argument parsing
14
+ - **Tool-call JSON repair**: `parseToolArguments` now uses [jsonrepair](https://github.com/josdejong/jsonrepair) when `JSON.parse` fails, so malformed tool-argument strings (trailing commas, unquoted keys, single quotes) are repaired instead of returning `{}`. Same logic is used when converting messages in `convertToOllamaChatMessages`.
15
+ - **Quoted/double-encoded JSON**: When the model returns a quoted JSON string (e.g. `"{\"query\":\"weather\"}"`), the inner JSON is now parsed so tool arguments are not lost.
16
+ - **Exports**: `parseToolArguments` and `resolveToolCallingOptions` are now exported from the main and browser entry points for advanced use.
17
+ - **Example**: New `tool-json-repair-example.ts` in examples/node demonstrates tool-argument repair; `json-repair-example.ts` and README updated to reference it.
18
+
3
19
  ## 3.6.0
4
20
 
5
21
  ### Minor Changes
@@ -28,6 +28,8 @@ __export(index_browser_exports, {
28
28
  createOllama: () => createOllama,
29
29
  generateText: () => generateText,
30
30
  ollama: () => ollama,
31
+ parseToolArguments: () => parseToolArguments,
32
+ resolveToolCallingOptions: () => resolveToolCallingOptions,
31
33
  streamText: () => streamText
32
34
  });
33
35
  module.exports = __toCommonJS(index_browser_exports);
@@ -36,187 +38,8 @@ module.exports = __toCommonJS(index_browser_exports);
36
38
  var import_provider = require("@ai-sdk/provider");
37
39
  var import_browser = require("ollama/browser");
38
40
 
39
- // src/utils/convert-to-ollama-messages.ts
40
- function convertToOllamaChatMessages(prompt) {
41
- const messages = [];
42
- for (const message of prompt) {
43
- switch (message.role) {
44
- case "system": {
45
- messages.push({
46
- role: "system",
47
- content: message.content
48
- });
49
- break;
50
- }
51
- case "user": {
52
- if (typeof message.content === "string") {
53
- messages.push({
54
- role: "user",
55
- content: message.content
56
- });
57
- } else {
58
- const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
59
- const imageParts = message.content.filter(
60
- (part) => part.type === "file"
61
- ).filter((part) => {
62
- return part.mediaType?.startsWith("image/") || false;
63
- }).map((part) => {
64
- const imageData = part.data;
65
- if (imageData instanceof URL) {
66
- if (imageData.protocol === "data:") {
67
- const base64Match = imageData.href.match(
68
- /data:[^;]+;base64,(.+)/
69
- );
70
- if (base64Match) {
71
- return base64Match[1];
72
- }
73
- return imageData.href;
74
- }
75
- return imageData.href;
76
- } else if (typeof imageData === "string") {
77
- if (imageData.startsWith("data:")) {
78
- const base64Match = imageData.match(/data:[^;]+;base64,(.+)/);
79
- if (base64Match) {
80
- return base64Match[1];
81
- }
82
- }
83
- return imageData;
84
- } else if (imageData instanceof Uint8Array) {
85
- return Buffer.from(imageData).toString("base64");
86
- } else {
87
- console.warn(
88
- `Unsupported image data type: ${typeof imageData}`
89
- );
90
- return null;
91
- }
92
- }).filter((img) => img !== null);
93
- messages.push({
94
- role: "user",
95
- content: textParts || "",
96
- // Ensure content is never undefined
97
- images: imageParts.length > 0 ? imageParts : void 0
98
- });
99
- }
100
- break;
101
- }
102
- case "assistant": {
103
- let content;
104
- const toolCalls = [];
105
- if (typeof message.content === "string") {
106
- content = message.content;
107
- } else {
108
- const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
109
- const reasoningParts = message.content.filter((part) => part.type === "reasoning").map((part) => part.text).join("\n");
110
- content = [textParts, reasoningParts].filter(Boolean).join("\n");
111
- for (const part of message.content) {
112
- if (part.type === "tool-call") {
113
- let args;
114
- try {
115
- args = typeof part.input === "string" ? JSON.parse(part.input) : part.input;
116
- } catch (error48) {
117
- console.warn("Failed to parse tool call input:", error48);
118
- args = {};
119
- }
120
- toolCalls.push({
121
- id: part.toolCallId,
122
- type: "function",
123
- function: {
124
- name: part.toolName,
125
- arguments: args
126
- }
127
- });
128
- }
129
- }
130
- }
131
- messages.push({
132
- role: "assistant",
133
- content: content || "",
134
- // Ensure content is never undefined
135
- tool_calls: toolCalls.length > 0 ? toolCalls : void 0
136
- });
137
- break;
138
- }
139
- case "tool": {
140
- if (typeof message.content === "string") {
141
- messages.push({
142
- role: "tool",
143
- content: message.content
144
- });
145
- } else {
146
- for (const part of message.content) {
147
- if (part.type === "tool-result") {
148
- const contentValue = part.output.type === "text" || part.output.type === "error-text" ? part.output.value : part.output.type === "json" || part.output.type === "error-json" ? JSON.stringify(part.output.value) : JSON.stringify(part.output);
149
- messages.push({
150
- role: "tool",
151
- content: contentValue,
152
- tool_name: part.toolName
153
- });
154
- }
155
- }
156
- }
157
- break;
158
- }
159
- default: {
160
- const role = message.role;
161
- throw new Error(
162
- `Unsupported message role: ${role}. Supported roles are: system, user, assistant, tool`
163
- );
164
- }
165
- }
166
- }
167
- return messages;
168
- }
169
-
170
- // src/utils/map-ollama-finish-reason.ts
171
- function mapOllamaFinishReason(reason) {
172
- if (!reason) {
173
- return {
174
- unified: "stop",
175
- raw: void 0
176
- };
177
- }
178
- switch (reason) {
179
- case "stop": {
180
- return {
181
- unified: "stop",
182
- raw: reason
183
- };
184
- }
185
- case "length": {
186
- return {
187
- unified: "length",
188
- raw: reason
189
- };
190
- }
191
- default: {
192
- return {
193
- unified: "other",
194
- raw: reason
195
- };
196
- }
197
- }
198
- }
199
-
200
- // src/utils/ollama-error.ts
201
- var OllamaError = class _OllamaError extends Error {
202
- constructor({
203
- message,
204
- cause,
205
- data
206
- }) {
207
- super(message);
208
- __publicField(this, "cause");
209
- __publicField(this, "data");
210
- this.name = "OllamaError";
211
- this.cause = cause;
212
- this.data = data;
213
- }
214
- static isOllamaError(error48) {
215
- return error48 instanceof _OllamaError;
216
- }
217
- };
218
-
219
41
  // src/utils/tool-calling-reliability.ts
42
+ var import_jsonrepair = require("jsonrepair");
220
43
  var DEFAULT_TOOL_CALLING_OPTIONS = {
221
44
  maxRetries: 2,
222
45
  forceCompletion: true,
@@ -242,10 +65,29 @@ function parseToolArguments(input) {
242
65
  if (typeof input === "string") {
243
66
  try {
244
67
  const parsed = JSON.parse(input);
68
+ if (typeof parsed === "string") {
69
+ try {
70
+ const inner = JSON.parse(parsed);
71
+ return ensureRecord(inner);
72
+ } catch {
73
+ try {
74
+ const repaired = (0, import_jsonrepair.jsonrepair)(parsed);
75
+ return ensureRecord(JSON.parse(repaired));
76
+ } catch {
77
+ return {};
78
+ }
79
+ }
80
+ }
245
81
  return ensureRecord(parsed);
246
- } catch (error48) {
247
- console.warn("Failed to parse tool arguments as JSON:", error48);
248
- return {};
82
+ } catch {
83
+ try {
84
+ const repaired = (0, import_jsonrepair.jsonrepair)(input);
85
+ const parsed = JSON.parse(repaired);
86
+ return ensureRecord(parsed);
87
+ } catch (error48) {
88
+ console.warn("Failed to parse tool arguments as JSON:", error48);
89
+ return {};
90
+ }
249
91
  }
250
92
  }
251
93
  return ensureRecord(input);
@@ -404,13 +246,10 @@ function normalizeToolParameters(input, mappings = DEFAULT_PARAMETER_MAPPINGS) {
404
246
  }
405
247
  return normalized;
406
248
  }
407
- async function validateToolResult(toolName, input, result, executeFunction, options = {}) {
249
+ async function validateToolResult(_toolName, input, result, executeFunction, options = {}) {
408
250
  const { normalizeParameters = true, parameterMappings } = options;
409
251
  const normalizedInput = normalizeParameters ? normalizeToolParameters(input, parameterMappings) : input;
410
252
  if (!result || typeof result === "object" && Object.keys(result).length === 0) {
411
- console.warn(
412
- `\u26A0\uFE0F Tool ${toolName} returned empty result, attempting recovery...`
413
- );
414
253
  try {
415
254
  const recoveredResult = await executeFunction(normalizedInput);
416
255
  if (recoveredResult && (typeof recoveredResult !== "object" || Object.keys(recoveredResult).length > 0)) {
@@ -535,9 +374,209 @@ ${finalInstruction}`;
535
374
  return textContent?.text || "";
536
375
  }
537
376
 
377
+ // src/utils/convert-to-ollama-messages.ts
378
+ function normalizeImageDataForOllama(imageData) {
379
+ if (imageData instanceof URL) {
380
+ if (imageData.protocol === "data:") {
381
+ const base64Match = imageData.href.match(/data:[^;]+;base64,(.+)/);
382
+ const extracted = base64Match?.[1];
383
+ if (typeof extracted === "string") return extracted;
384
+ return imageData.href;
385
+ }
386
+ return imageData.href;
387
+ }
388
+ if (typeof imageData === "string") {
389
+ if (imageData.startsWith("data:")) {
390
+ const base64Match = imageData.match(/data:[^;]+;base64,(.+)/);
391
+ const extracted = base64Match?.[1];
392
+ if (typeof extracted === "string") return extracted;
393
+ }
394
+ return imageData;
395
+ }
396
+ if (imageData instanceof Uint8Array) {
397
+ return Buffer.from(imageData).toString("base64");
398
+ }
399
+ return null;
400
+ }
401
+ function convertToOllamaChatMessages(prompt) {
402
+ const messages = [];
403
+ for (const message of prompt) {
404
+ switch (message.role) {
405
+ case "system": {
406
+ messages.push({
407
+ role: "system",
408
+ content: message.content
409
+ });
410
+ break;
411
+ }
412
+ case "user": {
413
+ if (typeof message.content === "string") {
414
+ messages.push({
415
+ role: "user",
416
+ content: message.content
417
+ });
418
+ } else {
419
+ const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
420
+ const imageParts = message.content.filter(
421
+ (part) => part.type === "file"
422
+ ).filter((part) => part.mediaType?.startsWith("image/") ?? false).map((part) => normalizeImageDataForOllama(part.data)).filter((img) => img !== null);
423
+ messages.push({
424
+ role: "user",
425
+ content: textParts || "",
426
+ // Ensure content is never undefined
427
+ images: imageParts.length > 0 ? imageParts : void 0
428
+ });
429
+ }
430
+ break;
431
+ }
432
+ case "assistant": {
433
+ let content;
434
+ const toolCalls = [];
435
+ if (typeof message.content === "string") {
436
+ content = message.content;
437
+ } else {
438
+ const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
439
+ const reasoningParts = message.content.filter((part) => part.type === "reasoning").map((part) => part.text).join("\n");
440
+ content = [textParts, reasoningParts].filter(Boolean).join("\n");
441
+ for (const part of message.content) {
442
+ if (part.type === "tool-call") {
443
+ const args = parseToolArguments(part.input);
444
+ toolCalls.push({
445
+ id: part.toolCallId,
446
+ type: "function",
447
+ function: {
448
+ name: part.toolName,
449
+ arguments: args
450
+ }
451
+ });
452
+ }
453
+ }
454
+ }
455
+ messages.push({
456
+ role: "assistant",
457
+ content: content || "",
458
+ // Ensure content is never undefined
459
+ tool_calls: toolCalls.length > 0 ? toolCalls : void 0
460
+ });
461
+ break;
462
+ }
463
+ case "tool": {
464
+ if (typeof message.content === "string") {
465
+ messages.push({
466
+ role: "tool",
467
+ content: message.content
468
+ });
469
+ } else {
470
+ for (const part of message.content) {
471
+ if (part.type !== "tool-result") continue;
472
+ if (part.output.type === "content") {
473
+ const textParts = [];
474
+ const imageParts = [];
475
+ for (const item of part.output.value) {
476
+ switch (item.type) {
477
+ case "text": {
478
+ textParts.push(item.text);
479
+ break;
480
+ }
481
+ case "image-data": {
482
+ const normalized = normalizeImageDataForOllama(item.data);
483
+ if (normalized) imageParts.push(normalized);
484
+ break;
485
+ }
486
+ case "image-url": {
487
+ imageParts.push(item.url);
488
+ break;
489
+ }
490
+ case "file-data": {
491
+ if (item.mediaType?.startsWith("image/")) {
492
+ const normalized = normalizeImageDataForOllama(item.data);
493
+ if (normalized) imageParts.push(normalized);
494
+ }
495
+ break;
496
+ }
497
+ }
498
+ }
499
+ messages.push({
500
+ role: "tool",
501
+ content: textParts.join("\n") || "",
502
+ tool_name: part.toolName,
503
+ images: imageParts.length > 0 ? imageParts : void 0
504
+ });
505
+ continue;
506
+ }
507
+ const contentValue = part.output.type === "text" || part.output.type === "error-text" ? part.output.value : part.output.type === "json" || part.output.type === "error-json" ? JSON.stringify(part.output.value) : part.output.type === "execution-denied" ? "" : JSON.stringify(part.output);
508
+ messages.push({
509
+ role: "tool",
510
+ content: contentValue,
511
+ tool_name: part.toolName
512
+ });
513
+ }
514
+ }
515
+ break;
516
+ }
517
+ default: {
518
+ const role = message.role;
519
+ throw new Error(
520
+ `Unsupported message role: ${role}. Supported roles are: system, user, assistant, tool`
521
+ );
522
+ }
523
+ }
524
+ }
525
+ return messages;
526
+ }
527
+
528
+ // src/utils/map-ollama-finish-reason.ts
529
+ function mapOllamaFinishReason(reason) {
530
+ if (!reason) {
531
+ return {
532
+ unified: "stop",
533
+ raw: void 0
534
+ };
535
+ }
536
+ switch (reason) {
537
+ case "stop": {
538
+ return {
539
+ unified: "stop",
540
+ raw: reason
541
+ };
542
+ }
543
+ case "length": {
544
+ return {
545
+ unified: "length",
546
+ raw: reason
547
+ };
548
+ }
549
+ default: {
550
+ return {
551
+ unified: "other",
552
+ raw: reason
553
+ };
554
+ }
555
+ }
556
+ }
557
+
558
+ // src/utils/ollama-error.ts
559
+ var OllamaError = class _OllamaError extends Error {
560
+ constructor({
561
+ message,
562
+ cause,
563
+ data
564
+ }) {
565
+ super(message);
566
+ __publicField(this, "cause");
567
+ __publicField(this, "data");
568
+ this.name = "OllamaError";
569
+ this.cause = cause;
570
+ this.data = data;
571
+ }
572
+ static isOllamaError(error48) {
573
+ return error48 instanceof _OllamaError;
574
+ }
575
+ };
576
+
538
577
  // src/utils/object-generation-reliability.ts
539
578
  var import_provider_utils = require("@ai-sdk/provider-utils");
540
- var import_jsonrepair = require("jsonrepair");
579
+ var import_jsonrepair2 = require("jsonrepair");
541
580
  function isZodSchema(schema) {
542
581
  return typeof schema === "object" && schema !== null && "parse" in schema && typeof schema.parse === "function";
543
582
  }
@@ -1150,7 +1189,7 @@ async function enhancedRepairText(options) {
1150
1189
  async function cascadeRepairText(options) {
1151
1190
  const { text } = options;
1152
1191
  try {
1153
- const repairedText = (0, import_jsonrepair.jsonrepair)(text);
1192
+ const repairedText = (0, import_jsonrepair2.jsonrepair)(text);
1154
1193
  JSON.parse(repairedText);
1155
1194
  return repairedText;
1156
1195
  } catch {