@@ -5,7 +5,11 @@ import {
55 withTestServer ,
66} from '@ai-sdk/provider-utils/test' ;
77import { 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
1014const 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+
57166describe ( '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
688873describe ( '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