Skip to content

Commit a9df182

Browse files
authored
feat (provider/google): Add support for search grounding. (#4067)
1 parent 4017b0f commit a9df182

8 files changed

Lines changed: 389 additions & 9 deletions

File tree

.changeset/wicked-suns-sing.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@ai-sdk/google-vertex': patch
3+
'@ai-sdk/google': patch
4+
---
5+
6+
feat (provider/google): Add support for search grounding.

content/providers/01-ai-sdk-providers/10-google-generative-ai.mdx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,127 @@ const { text: meatLasangaRecipe } = await generateText({
226226
});
227227
```
228228

229+
### Search Grounding
230+
231+
With [search grounding](https://ai.google.dev/gemini-api/docs/grounding),
232+
the model has access to the latest information using Google search.
233+
Search grounding can be used to provide answers around current events:
234+
235+
```ts highlight="7,14-20"
236+
import { google } from '@ai-sdk/google';
237+
import { GoogleGenerativeAIProviderMetadata } from '@ai-sdk/google';
238+
import { generateText } from 'ai';
239+
240+
const { text, experimental_providerMetadata } = await generateText({
241+
model: google('gemini-1.5-pro', {
242+
useSearchGrounding: true,
243+
}),
244+
prompt:
245+
'List the top 5 San Francisco news from the past week.' +
246+
'You must include the date of each article.',
247+
});
248+
249+
// access the grounding metadata. Casting to the provider metadata type
250+
// is optional but provides autocomplete and type safety.
251+
const metadata = experimental_providerMetadata?.google as
252+
| GoogleGenerativeAIProviderMetadata
253+
| undefined;
254+
const groundingMetadata = metadata?.groundingMetadata;
255+
const safetyRatings = metadata?.safetyRatings;
256+
```
257+
258+
The grounding metadata includes detailed information about how search results were used to ground the model's response. Here are the available fields:
259+
260+
- **`webSearchQueries`** (`string[] | null`)
261+
262+
- Array of search queries used to retrieve information
263+
- Example: `["What's the weather in Chicago this weekend?"]`
264+
265+
- **`searchEntryPoint`** (`{ renderedContent: string } | null`)
266+
267+
- Contains the main search result content used as an entry point
268+
- The `renderedContent` field contains the formatted content
269+
270+
- **`groundingSupports`** (Array of support objects | null)
271+
- Contains details about how specific response parts are supported by search results
272+
- Each support object includes:
273+
- **`segment`**: Information about the grounded text segment
274+
- `text`: The actual text segment
275+
- `startIndex`: Starting position in the response
276+
- `endIndex`: Ending position in the response
277+
- **`groundingChunkIndices`**: References to supporting search result chunks
278+
- **`confidenceScores`**: Confidence scores (0-1) for each supporting chunk
279+
280+
Example response:
281+
282+
```json
283+
{
284+
"groundingMetadata": {
285+
"webSearchQueries": ["What's the weather in Chicago this weekend?"],
286+
"searchEntryPoint": {
287+
"renderedContent": "..."
288+
},
289+
"groundingSupports": [
290+
{
291+
"segment": {
292+
"startIndex": 0,
293+
"endIndex": 65,
294+
"text": "Chicago weather changes rapidly, so layers let you adjust easily."
295+
},
296+
"groundingChunkIndices": [0],
297+
"confidenceScores": [0.99]
298+
}
299+
]
300+
}
301+
}
302+
```
303+
304+
The safety ratings provide insight into how the model's response was grounded to search results. See [Google AI documentation on safety settings](https://ai.google.dev/gemini-api/docs/safety-settings).
305+
306+
Example response excerpt:
307+
308+
```json
309+
{
310+
"safetyRatings": [
311+
{
312+
"category": "HARM_CATEGORY_HATE_SPEECH",
313+
"probability": "NEGLIGIBLE",
314+
"probabilityScore": 0.11027937,
315+
"severity": "HARM_SEVERITY_LOW",
316+
"severityScore": 0.28487435
317+
},
318+
{
319+
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
320+
"probability": "HIGH",
321+
"blocked": true,
322+
"probabilityScore": 0.95422274,
323+
"severity": "HARM_SEVERITY_MEDIUM",
324+
"severityScore": 0.43398145
325+
},
326+
{
327+
"category": "HARM_CATEGORY_HARASSMENT",
328+
"probability": "NEGLIGIBLE",
329+
"probabilityScore": 0.11085559,
330+
"severity": "HARM_SEVERITY_NEGLIGIBLE",
331+
"severityScore": 0.19027223
332+
},
333+
{
334+
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
335+
"probability": "NEGLIGIBLE",
336+
"probabilityScore": 0.22901751,
337+
"severity": "HARM_SEVERITY_NEGLIGIBLE",
338+
"severityScore": 0.09089675
339+
}
340+
]
341+
}
342+
```
343+
344+
<Note>
345+
The Google Generative AI provider does not yet support [dynamic retrieval mode
346+
and
347+
threshold](https://ai.google.dev/gemini-api/docs/grounding?lang=rest#dynamic-retrieval).
348+
</Note>
349+
229350
### Troubleshooting
230351

231352
#### Schema Limitations

content/providers/01-ai-sdk-providers/11-google-vertex.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ Example response excerpt:
402402
```json
403403
{
404404
"groundingMetadata": {
405-
"webSearchQueries": ["What's the weather in Chicago this weekend?"],
405+
"retrievalQueries": ["What's the weather in Chicago this weekend?"],
406406
"searchEntryPoint": {
407407
"renderedContent": "..."
408408
},
@@ -461,6 +461,11 @@ Example response excerpt:
461461
}
462462
```
463463

464+
<Note>
465+
The Google Vertex provider does not yet support [dynamic retrieval mode and
466+
threshold](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/ground-gemini#dynamic-retrieval).
467+
</Note>
468+
464469
For more details, see the [Google Vertex AI documentation on grounding with Google Search](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/ground-gemini#ground-to-search).
465470

466471
### Troubleshooting

examples/ai-core/src/e2e/google.test.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import 'dotenv/config';
22
import { describe, it, expect, vi } from 'vitest';
3-
import { createGoogleGenerativeAI } from '@ai-sdk/google';
3+
import {
4+
createGoogleGenerativeAI,
5+
GoogleGenerativeAIProviderMetadata,
6+
} from '@ai-sdk/google';
47
import { z } from 'zod';
58
import {
69
generateText,
@@ -67,6 +70,44 @@ describe('Google E2E Tests', () => {
6770
expect(result.usage?.totalTokens).toBeGreaterThan(0);
6871
});
6972

73+
it('should generate text with search grounding metadata in response when search grounding is enabled', async () => {
74+
const model = provider(modelId, {
75+
useSearchGrounding: true,
76+
});
77+
78+
const result = await generateText({
79+
model,
80+
prompt: 'What is the current population of Tokyo?',
81+
});
82+
83+
expect(result.text).toBeTruthy();
84+
expect(result.text.toLowerCase()).toContain('tokyo');
85+
expect(result.usage?.totalTokens).toBeGreaterThan(0);
86+
87+
// Verify specific grounding metadata fields
88+
const metadata = result.experimental_providerMetadata?.google as
89+
| GoogleGenerativeAIProviderMetadata
90+
| undefined;
91+
const groundingMetadata = metadata?.groundingMetadata;
92+
expect(Array.isArray(groundingMetadata?.webSearchQueries)).toBe(true);
93+
expect(groundingMetadata?.webSearchQueries?.length).toBeGreaterThan(0);
94+
95+
// Verify search entry point exists
96+
expect(
97+
groundingMetadata?.searchEntryPoint?.renderedContent,
98+
).toBeDefined();
99+
100+
// Verify grounding supports
101+
expect(Array.isArray(groundingMetadata?.groundingSupports)).toBe(true);
102+
const support = groundingMetadata?.groundingSupports?.[0];
103+
expect(support?.segment).toBeDefined();
104+
expect(Array.isArray(support?.groundingChunkIndices)).toBe(true);
105+
expect(Array.isArray(support?.confidenceScores)).toBe(true);
106+
107+
// Basic response checks
108+
expect(result.text).toBeTruthy();
109+
expect(result.usage?.totalTokens).toBeGreaterThan(0);
110+
});
70111
it('should generate text with PDF input', async () => {
71112
const model = provider(modelId);
72113
const result = await generateText({
@@ -166,6 +207,53 @@ describe('Google E2E Tests', () => {
166207
expect((await result.usage)?.totalTokens).toBeGreaterThan(0);
167208
});
168209

210+
it('should stream text with search grounding metadata when search grounding is enabled', async () => {
211+
const model = provider(modelId, {
212+
useSearchGrounding: true,
213+
});
214+
215+
const result = streamText({
216+
model,
217+
prompt: 'What is the current population of Tokyo?',
218+
});
219+
220+
const chunks: string[] = [];
221+
for await (const chunk of result.textStream) {
222+
chunks.push(chunk);
223+
}
224+
225+
// Get the complete response metadata
226+
const metadata = (await result.experimental_providerMetadata)?.google as
227+
| GoogleGenerativeAIProviderMetadata
228+
| undefined;
229+
const groundingMetadata = metadata?.groundingMetadata;
230+
231+
const completeText = chunks.join('');
232+
expect(completeText).toBeTruthy();
233+
expect(completeText.toLowerCase()).toContain('tokyo');
234+
expect((await result.usage)?.totalTokens).toBeGreaterThan(0);
235+
236+
// Verify specific grounding metadata fields
237+
expect(Array.isArray(groundingMetadata?.webSearchQueries)).toBe(true);
238+
expect(groundingMetadata?.webSearchQueries?.length).toBeGreaterThan(0);
239+
240+
// Verify search entry point exists
241+
expect(
242+
groundingMetadata?.searchEntryPoint?.renderedContent,
243+
).toBeDefined();
244+
245+
// Verify grounding supports
246+
expect(Array.isArray(groundingMetadata?.groundingSupports)).toBe(true);
247+
const support = groundingMetadata?.groundingSupports?.[0];
248+
expect(support?.segment).toBeDefined();
249+
expect(Array.isArray(support?.groundingChunkIndices)).toBe(true);
250+
expect(Array.isArray(support?.confidenceScores)).toBe(true);
251+
252+
// Basic response checks
253+
expect(chunks.join('')).toBeTruthy();
254+
expect((await result.usage)?.totalTokens).toBeGreaterThan(0);
255+
});
256+
169257
it('should stream object', async () => {
170258
const model = provider(modelId);
171259
const result = streamObject({

0 commit comments

Comments
 (0)