Skip to content

Commit 4017b0f

Browse files
authored
feat (provider/google-vertex): Enhance grounding metadata detail. (#4069)
1 parent e07439a commit 4017b0f

3 files changed

Lines changed: 318 additions & 11 deletions

File tree

.changeset/thin-students-return.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-vertex): Enhance grounding metadata response detail.

packages/google/src/google-generative-ai-language-model.test.ts

Lines changed: 287 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import {
55
withTestServer,
66
} from '@ai-sdk/provider-utils/test';
77
import { createGoogleGenerativeAI } from './google-provider';
8-
import { GoogleGenerativeAILanguageModel } from './google-generative-ai-language-model';
8+
import {
9+
GoogleGenerativeAILanguageModel,
10+
groundingMetadataSchema,
11+
} from './google-generative-ai-language-model';
12+
import { GoogleGenerativeAIGroundingMetadata } from './google-generative-ai-prompt';
913

1014
const TEST_PROMPT: LanguageModelV1Prompt = [
1115
{ role: 'user', content: [{ type: 'text', text: 'Hello' }] },
@@ -54,6 +58,111 @@ describe('supportsUrl', () => {
5458
});
5559
});
5660

61+
describe('groundingMetadataSchema', () => {
62+
it('validates complete grounding metadata with web search results', () => {
63+
const metadata = {
64+
webSearchQueries: ["What's the weather in Chicago this weekend?"],
65+
searchEntryPoint: {
66+
renderedContent: 'Sample rendered content for search results',
67+
},
68+
groundingChunks: [
69+
{
70+
web: {
71+
uri: 'https://example.com/weather',
72+
title: 'Chicago Weather Forecast',
73+
},
74+
},
75+
],
76+
groundingSupports: [
77+
{
78+
segment: {
79+
startIndex: 0,
80+
endIndex: 65,
81+
text: 'Chicago weather changes rapidly, so layers let you adjust easily.',
82+
},
83+
groundingChunkIndices: [0],
84+
confidenceScores: [0.99],
85+
},
86+
],
87+
retrievalMetadata: {
88+
webDynamicRetrievalScore: 0.96879,
89+
},
90+
};
91+
92+
const result = groundingMetadataSchema.safeParse(metadata);
93+
expect(result.success).toBe(true);
94+
});
95+
96+
it('validates complete grounding metadata with Vertex AI Search results', () => {
97+
const metadata = {
98+
retrievalQueries: ['How to make appointment to renew driving license?'],
99+
groundingChunks: [
100+
{
101+
retrievedContext: {
102+
uri: 'https://vertexaisearch.cloud.google.com/grounding-api-redirect/AXiHM.....QTN92V5ePQ==',
103+
title: 'dmv',
104+
},
105+
},
106+
],
107+
groundingSupports: [
108+
{
109+
segment: {
110+
startIndex: 25,
111+
endIndex: 147,
112+
},
113+
segment_text: 'ipsum lorem ...',
114+
supportChunkIndices: [1, 2],
115+
confidenceScore: [0.9541752, 0.97726375],
116+
},
117+
],
118+
};
119+
120+
const result = groundingMetadataSchema.safeParse(metadata);
121+
expect(result.success).toBe(true);
122+
});
123+
124+
it('validates partial grounding metadata', () => {
125+
const metadata = {
126+
webSearchQueries: ['sample query'],
127+
// Missing other optional fields
128+
};
129+
130+
const result = groundingMetadataSchema.safeParse(metadata);
131+
expect(result.success).toBe(true);
132+
});
133+
134+
it('validates empty grounding metadata', () => {
135+
const metadata = {};
136+
137+
const result = groundingMetadataSchema.safeParse(metadata);
138+
expect(result.success).toBe(true);
139+
});
140+
141+
it('validates metadata with empty retrievalMetadata', () => {
142+
const metadata = {
143+
webSearchQueries: ['sample query'],
144+
retrievalMetadata: {},
145+
};
146+
147+
const result = groundingMetadataSchema.safeParse(metadata);
148+
expect(result.success).toBe(true);
149+
});
150+
151+
it('rejects invalid data types', () => {
152+
const metadata = {
153+
webSearchQueries: 'not an array', // Should be an array
154+
groundingSupports: [
155+
{
156+
confidenceScores: 'not an array', // Should be an array of numbers
157+
},
158+
],
159+
};
160+
161+
const result = groundingMetadataSchema.safeParse(metadata);
162+
expect(result.success).toBe(false);
163+
});
164+
});
165+
57166
describe('doGenerate', () => {
58167
const prepareJsonResponse = ({
59168
content = '',
@@ -63,6 +172,7 @@ describe('doGenerate', () => {
63172
totalTokenCount: 3,
64173
},
65174
headers,
175+
groundingMetadata,
66176
}: {
67177
content?: string;
68178
usage?: {
@@ -71,6 +181,7 @@ describe('doGenerate', () => {
71181
totalTokenCount: number;
72182
};
73183
headers?: Record<string, string>;
184+
groundingMetadata?: GoogleGenerativeAIGroundingMetadata;
74185
}): TestServerResponse => ({
75186
url: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
76187
type: 'json-value',
@@ -84,6 +195,7 @@ describe('doGenerate', () => {
84195
finishReason: 'STOP',
85196
index: 0,
86197
safetyRatings: SAFETY_RATINGS,
198+
...(groundingMetadata && { groundingMetadata }),
87199
},
88200
],
89201
promptFeedback: { safetyRatings: SAFETY_RATINGS },
@@ -683,31 +795,197 @@ describe('doGenerate', () => {
683795
},
684796
),
685797
);
798+
799+
it(
800+
'should expose grounding metadata in provider metadata',
801+
withTestServer(
802+
prepareJsonResponse({
803+
content: 'test response',
804+
groundingMetadata: {
805+
webSearchQueries: ["What's the weather in Chicago this weekend?"],
806+
searchEntryPoint: {
807+
renderedContent: 'Sample rendered content for search results',
808+
},
809+
groundingChunks: [
810+
{
811+
web: {
812+
uri: 'https://example.com/weather',
813+
title: 'Chicago Weather Forecast',
814+
},
815+
},
816+
],
817+
groundingSupports: [
818+
{
819+
segment: {
820+
startIndex: 0,
821+
endIndex: 65,
822+
text: 'Chicago weather changes rapidly, so layers let you adjust easily.',
823+
},
824+
groundingChunkIndices: [0],
825+
confidenceScores: [0.99],
826+
},
827+
],
828+
retrievalMetadata: {
829+
webDynamicRetrievalScore: 0.96879,
830+
},
831+
},
832+
}),
833+
async () => {
834+
const { providerMetadata } = await model.doGenerate({
835+
inputFormat: 'prompt',
836+
mode: { type: 'regular' },
837+
prompt: TEST_PROMPT,
838+
});
839+
840+
expect(providerMetadata?.google.groundingMetadata).toStrictEqual({
841+
webSearchQueries: ["What's the weather in Chicago this weekend?"],
842+
searchEntryPoint: {
843+
renderedContent: 'Sample rendered content for search results',
844+
},
845+
groundingChunks: [
846+
{
847+
web: {
848+
uri: 'https://example.com/weather',
849+
title: 'Chicago Weather Forecast',
850+
},
851+
},
852+
],
853+
groundingSupports: [
854+
{
855+
segment: {
856+
startIndex: 0,
857+
endIndex: 65,
858+
text: 'Chicago weather changes rapidly, so layers let you adjust easily.',
859+
},
860+
groundingChunkIndices: [0],
861+
confidenceScores: [0.99],
862+
},
863+
],
864+
retrievalMetadata: {
865+
webDynamicRetrievalScore: 0.96879,
866+
},
867+
});
868+
},
869+
),
870+
);
686871
});
687872

688873
describe('doStream', () => {
689874
const prepareStreamResponse = ({
690875
content,
691876
headers,
877+
groundingMetadata,
692878
}: {
693879
content: string[];
694880
headers?: Record<string, string>;
881+
groundingMetadata?: GoogleGenerativeAIGroundingMetadata;
695882
}): TestServerResponse => ({
696883
url: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:streamGenerateContent',
697884
type: 'stream-values',
698885
content: content.map(
699-
text =>
700-
`data: {"candidates": [{"content": {"parts": [{"text": "${text}"}],"role": "model"},` +
701-
`"finishReason": "STOP","index": 0,"safetyRatings": [` +
702-
`{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},` +
703-
`{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},` +
704-
`{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},` +
705-
`{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],` +
706-
`"usageMetadata": {"promptTokenCount": 294,"candidatesTokenCount": 233,"totalTokenCount": 527}}\n\n`,
886+
(text, index) =>
887+
`data: ${JSON.stringify({
888+
candidates: [
889+
{
890+
content: { parts: [{ text }], role: 'model' },
891+
finishReason: 'STOP',
892+
index: 0,
893+
safetyRatings: SAFETY_RATINGS,
894+
...(groundingMetadata && { groundingMetadata }),
895+
},
896+
],
897+
// Include usage metadata only in the last chunk
898+
...(index === content.length - 1 && {
899+
usageMetadata: {
900+
promptTokenCount: 294,
901+
candidatesTokenCount: 233,
902+
totalTokenCount: 527,
903+
},
904+
}),
905+
})}\n\n`,
707906
),
708907
headers,
709908
});
710909

910+
it(
911+
'should expose grounding metadata in provider metadata on finish',
912+
withTestServer(
913+
prepareStreamResponse({
914+
content: ['test'],
915+
groundingMetadata: {
916+
webSearchQueries: ["What's the weather in Chicago this weekend?"],
917+
searchEntryPoint: {
918+
renderedContent: 'Sample rendered content for search results',
919+
},
920+
groundingChunks: [
921+
{
922+
web: {
923+
uri: 'https://example.com/weather',
924+
title: 'Chicago Weather Forecast',
925+
},
926+
},
927+
],
928+
groundingSupports: [
929+
{
930+
segment: {
931+
startIndex: 0,
932+
endIndex: 65,
933+
text: 'Chicago weather changes rapidly, so layers let you adjust easily.',
934+
},
935+
groundingChunkIndices: [0],
936+
confidenceScores: [0.99],
937+
},
938+
],
939+
retrievalMetadata: {
940+
webDynamicRetrievalScore: 0.96879,
941+
},
942+
},
943+
}),
944+
async () => {
945+
const { stream } = await model.doStream({
946+
inputFormat: 'prompt',
947+
mode: { type: 'regular' },
948+
prompt: TEST_PROMPT,
949+
});
950+
951+
const events = await convertReadableStreamToArray(stream);
952+
const finishEvent = events.find(event => event.type === 'finish');
953+
954+
expect(
955+
finishEvent?.type === 'finish' &&
956+
finishEvent.providerMetadata?.google.groundingMetadata,
957+
).toStrictEqual({
958+
webSearchQueries: ["What's the weather in Chicago this weekend?"],
959+
searchEntryPoint: {
960+
renderedContent: 'Sample rendered content for search results',
961+
},
962+
groundingChunks: [
963+
{
964+
web: {
965+
uri: 'https://example.com/weather',
966+
title: 'Chicago Weather Forecast',
967+
},
968+
},
969+
],
970+
groundingSupports: [
971+
{
972+
segment: {
973+
startIndex: 0,
974+
endIndex: 65,
975+
text: 'Chicago weather changes rapidly, so layers let you adjust easily.',
976+
},
977+
groundingChunkIndices: [0],
978+
confidenceScores: [0.99],
979+
},
980+
],
981+
retrievalMetadata: {
982+
webDynamicRetrievalScore: 0.96879,
983+
},
984+
});
985+
},
986+
),
987+
);
988+
711989
it(
712990
'should stream text deltas',
713991
withTestServer(

0 commit comments

Comments
 (0)