ai-sdk-ollama 3.7.0 → 3.8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ed617f1: - **Fix:** Make `OllamaProviderOptions`, `OllamaChatProviderOptions`, and `OllamaEmbeddingProviderOptions` compatible with the AI SDK's `providerOptions` (`Record<string, JSONObject>`). They are now defined as type aliases from Zod schemas (`z.infer`), so passing e.g. `providerOptions: { ollama: { structuredOutputs: true } }` into `streamText`, `generateText`, or `ToolLoopAgent` type-checks correctly (fixes #548).
8
+ - **New:** Export provider option Zod schemas: `ollamaProviderOptionsSchema`, `ollamaChatProviderOptionsSchema`, `ollamaEmbeddingProviderOptionsSchema`, and `ollamaRerankingProviderOptionsSchema` for validation or parsing.
9
+ - **New:** Image generation support via `ollama.imageModel(modelId)` and `OllamaImageModel`, using the AI SDK's `generateImage()` and experimental Ollama image models (e.g. `x/z-image-turbo`, `x/flux2-klein`). Supports `providerOptions.ollama` (e.g. `steps`, `negative_prompt`).
10
+ - **Change:** Reranking model now uses `parseProviderOptions` from `@ai-sdk/provider-utils` for `providerOptions.ollama`; `OllamaRerankingProviderOptions` is now a type inferred from the exported schema.
11
+
12
+ ## 3.7.1
13
+
14
+ ### Patch Changes
15
+
16
+ - 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.
17
+
3
18
  ## 3.7.0
4
19
 
5
20
  ### Minor Changes
@@ -35,7 +35,6 @@ __export(index_browser_exports, {
35
35
  module.exports = __toCommonJS(index_browser_exports);
36
36
 
37
37
  // src/provider.browser.ts
38
- var import_provider = require("@ai-sdk/provider");
39
38
  var import_browser = require("ollama/browser");
40
39
 
41
40
  // src/utils/tool-calling-reliability.ts
@@ -375,6 +374,29 @@ ${finalInstruction}`;
375
374
  }
376
375
 
377
376
  // src/utils/convert-to-ollama-messages.ts
377
+ function normalizeImageDataForOllama(imageData) {
378
+ if (imageData instanceof URL) {
379
+ if (imageData.protocol === "data:") {
380
+ const base64Match = imageData.href.match(/data:[^;]+;base64,(.+)/);
381
+ const extracted = base64Match?.[1];
382
+ if (typeof extracted === "string") return extracted;
383
+ return imageData.href;
384
+ }
385
+ return imageData.href;
386
+ }
387
+ if (typeof imageData === "string") {
388
+ if (imageData.startsWith("data:")) {
389
+ const base64Match = imageData.match(/data:[^;]+;base64,(.+)/);
390
+ const extracted = base64Match?.[1];
391
+ if (typeof extracted === "string") return extracted;
392
+ }
393
+ return imageData;
394
+ }
395
+ if (imageData instanceof Uint8Array) {
396
+ return Buffer.from(imageData).toString("base64");
397
+ }
398
+ return null;
399
+ }
378
400
  function convertToOllamaChatMessages(prompt) {
379
401
  const messages = [];
380
402
  for (const message of prompt) {
@@ -396,35 +418,7 @@ function convertToOllamaChatMessages(prompt) {
396
418
  const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
397
419
  const imageParts = message.content.filter(
398
420
  (part) => part.type === "file"
399
- ).filter((part) => {
400
- return part.mediaType?.startsWith("image/") || false;
401
- }).map((part) => {
402
- const imageData = part.data;
403
- if (imageData instanceof URL) {
404
- if (imageData.protocol === "data:") {
405
- const base64Match = imageData.href.match(
406
- /data:[^;]+;base64,(.+)/
407
- );
408
- if (base64Match) {
409
- return base64Match[1];
410
- }
411
- return imageData.href;
412
- }
413
- return imageData.href;
414
- } else if (typeof imageData === "string") {
415
- if (imageData.startsWith("data:")) {
416
- const base64Match = imageData.match(/data:[^;]+;base64,(.+)/);
417
- if (base64Match) {
418
- return base64Match[1];
419
- }
420
- }
421
- return imageData;
422
- } else if (imageData instanceof Uint8Array) {
423
- return Buffer.from(imageData).toString("base64");
424
- } else {
425
- return null;
426
- }
427
- }).filter((img) => img !== null);
421
+ ).filter((part) => part.mediaType?.startsWith("image/") ?? false).map((part) => normalizeImageDataForOllama(part.data)).filter((img) => img !== null);
428
422
  messages.push({
429
423
  role: "user",
430
424
  content: textParts || "",
@@ -473,14 +467,48 @@ function convertToOllamaChatMessages(prompt) {
473
467
  });
474
468
  } else {
475
469
  for (const part of message.content) {
476
- if (part.type === "tool-result") {
477
- 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);
470
+ if (part.type !== "tool-result") continue;
471
+ if (part.output.type === "content") {
472
+ const textParts = [];
473
+ const imageParts = [];
474
+ for (const item of part.output.value) {
475
+ switch (item.type) {
476
+ case "text": {
477
+ textParts.push(item.text);
478
+ break;
479
+ }
480
+ case "image-data": {
481
+ const normalized = normalizeImageDataForOllama(item.data);
482
+ if (normalized) imageParts.push(normalized);
483
+ break;
484
+ }
485
+ case "image-url": {
486
+ imageParts.push(item.url);
487
+ break;
488
+ }
489
+ case "file-data": {
490
+ if (item.mediaType?.startsWith("image/")) {
491
+ const normalized = normalizeImageDataForOllama(item.data);
492
+ if (normalized) imageParts.push(normalized);
493
+ }
494
+ break;
495
+ }
496
+ }
497
+ }
478
498
  messages.push({
479
499
  role: "tool",
480
- content: contentValue,
481
- tool_name: part.toolName
500
+ content: textParts.join("\n") || "",
501
+ tool_name: part.toolName,
502
+ images: imageParts.length > 0 ? imageParts : void 0
482
503
  });
504
+ continue;
483
505
  }
506
+ 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);
507
+ messages.push({
508
+ role: "tool",
509
+ content: contentValue,
510
+ tool_name: part.toolName
511
+ });
484
512
  }
485
513
  }
486
514
  break;
@@ -16394,6 +16422,7 @@ var ollamaRerankingResponseSchema = external_exports.object({
16394
16422
  )
16395
16423
  });
16396
16424
  var ollamaRerankingProviderOptionsSchema = external_exports.object({
16425
+ /** Custom instruction for this specific reranking call. Overrides the instruction set in model settings. */
16397
16426
  instruction: external_exports.string().optional()
16398
16427
  });
16399
16428
  var ollamaErrorSchema = external_exports.object({
@@ -16426,15 +16455,11 @@ var OllamaRerankingModel = class {
16426
16455
  providerOptions
16427
16456
  }) {
16428
16457
  const warnings = [];
16429
- let rerankingOptions;
16430
- if (providerOptions?.ollama) {
16431
- const parsed = ollamaRerankingProviderOptionsSchema.safeParse(
16432
- providerOptions.ollama
16433
- );
16434
- if (parsed.success) {
16435
- rerankingOptions = parsed.data;
16436
- }
16437
- }
16458
+ const rerankingOptions = await (0, import_provider_utils2.parseProviderOptions)({
16459
+ provider: "ollama",
16460
+ providerOptions,
16461
+ schema: ollamaRerankingProviderOptionsSchema
16462
+ });
16438
16463
  let documentValues;
16439
16464
  if (documents.type === "object") {
16440
16465
  warnings.push({
@@ -16628,6 +16653,115 @@ var OllamaEmbeddingRerankingModel = class {
16628
16653
  }
16629
16654
  };
16630
16655
 
16656
+ // src/models/image-model.ts
16657
+ var import_provider_utils3 = require("@ai-sdk/provider-utils");
16658
+ var ollamaImageProviderOptionsSchema = external_exports.object({
16659
+ steps: external_exports.number().optional(),
16660
+ negative_prompt: external_exports.string().optional()
16661
+ });
16662
+ var OllamaImageModel = class {
16663
+ constructor(modelId, config2) {
16664
+ this.config = config2;
16665
+ __publicField(this, "specificationVersion", "v3");
16666
+ __publicField(this, "provider");
16667
+ __publicField(this, "modelId");
16668
+ __publicField(this, "maxImagesPerCall", 1);
16669
+ this.modelId = modelId;
16670
+ this.provider = config2.provider;
16671
+ }
16672
+ async doGenerate(options) {
16673
+ const {
16674
+ prompt,
16675
+ size,
16676
+ aspectRatio,
16677
+ seed,
16678
+ providerOptions,
16679
+ abortSignal,
16680
+ headers: optHeaders
16681
+ } = options;
16682
+ if (prompt == null || prompt === "") {
16683
+ throw new OllamaError({ message: "Image generation requires a prompt" });
16684
+ }
16685
+ const url2 = `${this.config.baseURL.replace(/\/$/, "")}/api/generate`;
16686
+ const body = {
16687
+ model: this.modelId,
16688
+ prompt,
16689
+ stream: false
16690
+ };
16691
+ if (size) {
16692
+ const [w, h] = size.split("x").map(Number);
16693
+ if (!Number.isNaN(w)) body.width = w;
16694
+ if (!Number.isNaN(h)) body.height = h;
16695
+ }
16696
+ if (aspectRatio && body.width == null && body.height == null) {
16697
+ const [ratioW, ratioH] = aspectRatio.split(":").map(Number);
16698
+ if (ratioW != null && ratioH != null && !Number.isNaN(ratioW) && !Number.isNaN(ratioH) && ratioW > 0 && ratioH > 0) {
16699
+ const target = 1024;
16700
+ body.width = Math.round(target * Math.sqrt(ratioW / ratioH));
16701
+ body.height = Math.round(target * Math.sqrt(ratioH / ratioW));
16702
+ }
16703
+ }
16704
+ if (seed != null) {
16705
+ const opts = typeof body.options === "object" && body.options ? body.options : {};
16706
+ body.options = { ...opts, seed };
16707
+ }
16708
+ const po = await (0, import_provider_utils3.parseProviderOptions)({
16709
+ provider: "ollama",
16710
+ providerOptions,
16711
+ schema: ollamaImageProviderOptionsSchema
16712
+ });
16713
+ if (po?.steps != null) body.steps = po.steps;
16714
+ if (po?.negative_prompt != null) body.negative_prompt = po.negative_prompt;
16715
+ const headers = {
16716
+ "Content-Type": "application/json",
16717
+ ...this.config.headers(),
16718
+ ...optHeaders
16719
+ };
16720
+ const res = await (this.config.fetch ?? fetch)(url2, {
16721
+ method: "POST",
16722
+ headers,
16723
+ body: JSON.stringify(body),
16724
+ signal: abortSignal
16725
+ });
16726
+ if (!res.ok) {
16727
+ const errBody = await res.json().catch(() => ({}));
16728
+ throw new OllamaError({
16729
+ message: errBody.error ?? `Ollama API error: ${res.status} ${res.statusText}`
16730
+ });
16731
+ }
16732
+ const data = await res.json();
16733
+ const images = [];
16734
+ if (Array.isArray(data.images) && data.images.length > 0) {
16735
+ images.push(...data.images);
16736
+ } else if (typeof data.image === "string" && data.image.trim().length > 0) {
16737
+ images.push(data.image.trim());
16738
+ } else if (typeof data.response === "string" && data.response.trim().length > 0) {
16739
+ const trimmed = data.response.trim();
16740
+ if (/^[A-Za-z0-9+/=]+$/.test(trimmed) && trimmed.length > 100) {
16741
+ images.push(trimmed);
16742
+ }
16743
+ }
16744
+ const responseHeaders = {};
16745
+ for (const [key, value] of res.headers.entries()) {
16746
+ responseHeaders[key] = value;
16747
+ }
16748
+ return {
16749
+ images,
16750
+ warnings: [],
16751
+ response: {
16752
+ timestamp: /* @__PURE__ */ new Date(),
16753
+ modelId: data.model ?? this.modelId,
16754
+ headers: Object.keys(responseHeaders).length > 0 ? responseHeaders : void 0
16755
+ },
16756
+ usage: data.eval_count == null ? void 0 : {
16757
+ inputTokens: void 0,
16758
+ outputTokens: data.eval_count,
16759
+ totalTokens: data.eval_count
16760
+ }
16761
+ };
16762
+ }
16763
+ };
16764
+
16631
16765
  // src/tool/web-search.ts
16632
16766
  var import_ai = require("ai");
16633
16767
  var webSearchInputSchema = external_exports.object({
@@ -16880,10 +17014,10 @@ function createOllama(options = {}) {
16880
17014
  });
16881
17015
  };
16882
17016
  const createRerankingModel = (modelId, settings = {}) => {
16883
- const baseURL = options.baseURL ?? "http://127.0.0.1:11434";
17017
+ const baseURL2 = options.baseURL ?? "http://127.0.0.1:11434";
16884
17018
  return new OllamaRerankingModel(modelId, settings, {
16885
17019
  provider: "ollama.reranking",
16886
- baseURL,
17020
+ baseURL: baseURL2,
16887
17021
  headers: () => normalizedHeaders,
16888
17022
  fetch: options.fetch
16889
17023
  });
@@ -16910,13 +17044,15 @@ function createOllama(options = {}) {
16910
17044
  provider.reranking = createRerankingModel;
16911
17045
  provider.rerankingModel = createRerankingModel;
16912
17046
  provider.embeddingReranking = createEmbeddingRerankingModel;
16913
- provider.imageModel = (modelId) => {
16914
- throw new import_provider.NoSuchModelError({
16915
- modelId,
16916
- modelType: "imageModel",
16917
- message: "Image generation is not supported by Ollama"
16918
- });
16919
- };
17047
+ const baseURL = options.baseURL ?? "http://127.0.0.1:11434";
17048
+ const createImageModel = (modelId) => new OllamaImageModel(modelId, {
17049
+ provider: "ollama",
17050
+ modelId,
17051
+ baseURL,
17052
+ headers: () => normalizedHeaders,
17053
+ fetch: options.fetch
17054
+ });
17055
+ provider.imageModel = createImageModel;
16920
17056
  const toolsWithClient = {
16921
17057
  webSearch: (options2 = {}) => ollamaTools.webSearch({ ...options2, client }),
16922
17058
  webFetch: (options2 = {}) => ollamaTools.webFetch({ ...options2, client })