Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/beige-monkeys-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ai-sdk/google': patch
---

Adds url-based pdf and image support for google tool results
50 changes: 50 additions & 0 deletions examples/ai-core/src/generate-text/google-image-tool-result-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { google } from '@ai-sdk/google';
import { generateText, stepCountIs, tool } from 'ai';
import { run } from '../lib/run';
import { z } from 'zod';

run(async () => {
const readImage = tool({
description: `Read and return an image`,
inputSchema: z.object({}),
execute: async () => {
try {
return {
success: true,
description: 'Successfully loaded image',
imageUrl:
'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true',
};
} catch (error) {
throw new Error(`Failed to analyze image: ${error}`);
}
},
toModelOutput(result) {
return {
type: 'content',
value: [
{
type: 'text',
text: result.description,
},
{
type: 'image-url',
url: result.imageUrl,
},
],
};
},
});

const result = await generateText({
model: google('gemini-2.5-flash'),
prompt:
'Please read the image using the tool provided and return the summary of that image',
tools: {
readImage,
},
stopWhen: stepCountIs(4),
});

console.log(`Assistant response: ${JSON.stringify(result.text, null, 2)}`);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { google } from '@ai-sdk/google';
import { generateText, stepCountIs, tool } from 'ai';
import { run } from '../lib/run';
import { z } from 'zod';
import path from 'path';
import fs from 'fs/promises';

run(async () => {
const readPDFDocument = tool({
description: `Read and return a PDF document`,
inputSchema: z.object({}),
execute: async () => {
try {
const pdfPath = path.join(__dirname, '../../data/ai.pdf');
const pdfData = await fs.readFile(pdfPath);

const base64Data = pdfData.toString('base64');

console.log(`PDF document read successfully`);

return {
success: true,
description: 'Successfully loaded PDF document',
pdfData: base64Data,
};
} catch (error) {
throw new Error(`Failed to analyze PDF: ${error}`);
}
},
toModelOutput(result) {
return {
type: 'content',
value: [
{
type: 'text',
text: result.description,
},
{
type: 'file-data',
data: result.pdfData,
mediaType: 'application/pdf',
filename: 'ai.pdf',
},
],
};
},
});

const result = await generateText({
model: google('gemini-2.5-flash'),
prompt:
'Please read the pdf document using the tool provided and return the summary of that pdf',
tools: {
readPDFDocument,
},
stopWhen: stepCountIs(4),
});

console.log(`Assistant response: ${JSON.stringify(result.text, null, 2)}`);
});
50 changes: 50 additions & 0 deletions examples/ai-core/src/generate-text/google-pdf-tool-results-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { google } from '@ai-sdk/google';
import { generateText, stepCountIs, tool } from 'ai';
import { run } from '../lib/run';
import { z } from 'zod';

run(async () => {
const readPDFDocument = tool({
description: `Read and return a PDF document`,
inputSchema: z.object({}),
execute: async () => {
try {
return {
success: true,
description: 'Successfully loaded PDF document',
pdfUrl:
'https://github.com/vercel/ai/blob/main/examples/ai-core/data/ai.pdf?raw=true',
};
} catch (error) {
throw new Error(`Failed to analyze PDF: ${error}`);
}
},
toModelOutput(result) {
return {
type: 'content',
value: [
{
type: 'text',
text: result.description,
},
{
type: 'file-url',
url: result.pdfUrl,
},
],
};
},
});

const result = await generateText({
model: google('gemini-2.5-flash'),
prompt:
'Please read the pdf document using the tool provided and return the summary of that pdf',
tools: {
readPDFDocument,
},
stopWhen: stepCountIs(4),
});

console.log(`Assistant response: ${JSON.stringify(result.text, null, 2)}`);
});
Original file line number Diff line number Diff line change
Expand Up @@ -410,4 +410,92 @@ describe('assistant messages', () => {
],
});
});

it('should handle tool result with url-based pdf content', async () => {
const result = convertToGoogleGenerativeAIMessages([
{
role: 'tool',
content: [
{
type: 'tool-result',
toolName: 'pdf-fetcher',
toolCallId: 'get-pdf-1',
output: {
type: 'content',
value: [
{
type: 'file-url',
url: 'https://example.com/document.pdf',
},
],
},
},
],
},
]);

expect(result).toEqual({
systemInstruction: undefined,
contents: [
{
role: 'user',
parts: [
{
fileData: {
mimeType: 'application/pdf',
fileUri: 'https://example.com/document.pdf',
},
},
{
text: 'Tool executed successfully and returned this file as a response',
},
],
},
],
});
});

it('should handle tool result with url-based image content', async () => {
const result = convertToGoogleGenerativeAIMessages([
{
role: 'tool',
content: [
{
type: 'tool-result',
toolName: 'image-generator',
toolCallId: 'image-gen-1',
output: {
type: 'content',
value: [
{
type: 'image-url',
url: 'https://example.com/image.png',
},
],
},
},
],
},
]);

expect(result).toEqual({
systemInstruction: undefined,
contents: [
{
role: 'user',
parts: [
{
fileData: {
mimeType: 'image/png',
fileUri: 'https://example.com/image.png',
},
},
{
text: 'Tool executed successfully and returned this image as a response',
},
],
},
],
});
});
});
83 changes: 83 additions & 0 deletions packages/google/src/convert-to-google-generative-ai-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,51 @@ import {
} from './google-generative-ai-prompt';
import { convertToBase64 } from '@ai-sdk/provider-utils';

const imageExtensionMimeTypes: Record<string, string> = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
webp: 'image/webp',
gif: 'image/gif',
bmp: 'image/bmp',
svg: 'image/svg+xml',
tif: 'image/tiff',
tiff: 'image/tiff',
heic: 'image/heic',
heif: 'image/heif',
};

const fileExtensionMimeTypes: Record<string, string> = {
pdf: 'application/pdf',
};

function inferMimeTypeFromUrl(
urlString: string,
options: {
extensionMap: Record<string, string>;
defaultMimeType: string;
},
): string {
try {
const url = new URL(urlString);
const pathname = url.pathname.toLowerCase();
const lastDotIndex = pathname.lastIndexOf('.');

if (lastDotIndex !== -1 && lastDotIndex < pathname.length - 1) {
const extension = pathname.slice(lastDotIndex + 1);
const mappedMimeType = options.extensionMap[extension];

if (mappedMimeType) {
return mappedMimeType;
}
}
} catch {
// ignore invalid URLs and fall back to default mime type
}

return options.defaultMimeType;
}

export function convertToGoogleGenerativeAIMessages(
prompt: LanguageModelV3Prompt,
options?: { isGemmaModel?: boolean },
Expand Down Expand Up @@ -178,6 +223,44 @@ export function convertToGoogleGenerativeAIMessages(
},
);
break;
case 'image-url': {
const mimeType = inferMimeTypeFromUrl(contentPart.url, {
extensionMap: imageExtensionMimeTypes,
defaultMimeType: 'image/*',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default MIME type for image-url tool results is 'image/*', but this is a wildcard pattern used in HTTP Accept headers, not a valid concrete MIME type. It should be 'image/jpeg' to be consistent with how the same file handles generic image types elsewhere (line 95) and to match patterns in other providers.

View Details
📝 Patch Details
diff --git a/packages/google/src/convert-to-google-generative-ai-messages.test.ts b/packages/google/src/convert-to-google-generative-ai-messages.test.ts
index 861244b61..771ac99a8 100644
--- a/packages/google/src/convert-to-google-generative-ai-messages.test.ts
+++ b/packages/google/src/convert-to-google-generative-ai-messages.test.ts
@@ -498,4 +498,48 @@ describe('assistant messages', () => {
       ],
     });
   });
+
+  it('should default to image/jpeg for url-based image content with unknown extension', async () => {
+    const result = convertToGoogleGenerativeAIMessages([
+      {
+        role: 'tool',
+        content: [
+          {
+            type: 'tool-result',
+            toolName: 'image-generator',
+            toolCallId: 'image-gen-2',
+            output: {
+              type: 'content',
+              value: [
+                {
+                  type: 'image-url',
+                  url: 'https://example.com/image.xyz',
+                },
+              ],
+            },
+          },
+        ],
+      },
+    ]);
+
+    expect(result).toEqual({
+      systemInstruction: undefined,
+      contents: [
+        {
+          role: 'user',
+          parts: [
+            {
+              fileData: {
+                mimeType: 'image/jpeg',
+                fileUri: 'https://example.com/image.xyz',
+              },
+            },
+            {
+              text: 'Tool executed successfully and returned this image as a response',
+            },
+          ],
+        },
+      ],
+    });
+  });
 });
diff --git a/packages/google/src/convert-to-google-generative-ai-messages.ts b/packages/google/src/convert-to-google-generative-ai-messages.ts
index f41da214d..2341d1893 100644
--- a/packages/google/src/convert-to-google-generative-ai-messages.ts
+++ b/packages/google/src/convert-to-google-generative-ai-messages.ts
@@ -226,7 +226,7 @@ export function convertToGoogleGenerativeAIMessages(
                 case 'image-url': {
                   const mimeType = inferMimeTypeFromUrl(contentPart.url, {
                     extensionMap: imageExtensionMimeTypes,
-                    defaultMimeType: 'image/*',
+                    defaultMimeType: 'image/jpeg',
                   });
 
                   parts.push(

Analysis

Invalid MIME type image/* used as default for tool result image URLs

What fails: convertToGoogleGenerativeAIMessages() in packages/google/src/convert-to-google-generative-ai-messages.ts uses image/* as the default MIME type when a tool result image URL has an unknown file extension, causing Google's Generative AI API to reject requests with an invalid MIME type.

How to reproduce:

const result = convertToGoogleGenerativeAIMessages([
  {
    role: 'tool',
    content: [
      {
        type: 'tool-result',
        toolName: 'image-generator',
        toolCallId: 'gen-1',
        output: {
          type: 'content',
          value: [
            {
              type: 'image-url',
              url: 'https://example.com/image.xyz', // Unknown extension
            },
          ],
        },
      },
    ],
  },
]);
// result.contents[0].parts[0].fileData.mimeType === 'image/*'

Result: Requests with mimeType: 'image/*' are sent to the Google Generative AI API. Since image/* is a wildcard pattern for Accept headers (RFC 2045), not a valid concrete MIME type, the API rejects the request.

Expected: Should default to image/jpeg to match:

  1. The established pattern in the same file (lines 93-95): part.mediaType === 'image/*' ? 'image/jpeg' : part.mediaType
  2. Other providers' implementations: Groq, OpenAI, and OpenAI-compatible all convert image/* to image/jpeg
  3. MIME type specifications: Concrete MIME types (e.g., image/jpeg, image/png) are required in Content-Type headers and API payloads; wildcards are only valid in Accept headers

Fix: Changed line 229 from defaultMimeType: 'image/*' to defaultMimeType: 'image/jpeg' and added test case for unknown file extensions to prevent regression.

});

parts.push(
{
fileData: {
mimeType,
fileUri: contentPart.url,
},
},
{
text: 'Tool executed successfully and returned this image as a response',
},
);
break;
}
case 'file-url': {
const mimeType = inferMimeTypeFromUrl(contentPart.url, {
extensionMap: fileExtensionMimeTypes,
defaultMimeType: 'application/octet-stream',
});

parts.push(
{
fileData: {
mimeType,
fileUri: contentPart.url,
},
},
{
text: 'Tool executed successfully and returned this file as a response',
},
);
break;
}
default:
parts.push({ text: JSON.stringify(contentPart) });
break;
Expand Down