@ai-sdk/google 3.0.12 → 3.0.13

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.
@@ -1,4616 +0,0 @@
1
- import {
2
- LanguageModelV3Prompt,
3
- LanguageModelV3ProviderTool,
4
- } from '@ai-sdk/provider';
5
- import { createTestServer } from '@ai-sdk/test-server/with-vitest';
6
- import { convertReadableStreamToArray } from '@ai-sdk/provider-utils/test';
7
- import {
8
- GoogleGenerativeAILanguageModel,
9
- getGroundingMetadataSchema,
10
- getUrlContextMetadataSchema,
11
- } from './google-generative-ai-language-model';
12
-
13
- import {
14
- GoogleGenerativeAIGroundingMetadata,
15
- GoogleGenerativeAIUrlContextMetadata,
16
- } from './google-generative-ai-prompt';
17
- import { createGoogleGenerativeAI } from './google-provider';
18
- import { describe, it, expect, vi } from 'vitest';
19
-
20
- vi.mock('./version', () => ({
21
- VERSION: '0.0.0-test',
22
- }));
23
-
24
- const TEST_PROMPT: LanguageModelV3Prompt = [
25
- { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
26
- ];
27
-
28
- const SAFETY_RATINGS = [
29
- {
30
- category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
31
- probability: 'NEGLIGIBLE',
32
- },
33
- {
34
- category: 'HARM_CATEGORY_HATE_SPEECH',
35
- probability: 'NEGLIGIBLE',
36
- },
37
- {
38
- category: 'HARM_CATEGORY_HARASSMENT',
39
- probability: 'NEGLIGIBLE',
40
- },
41
- {
42
- category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
43
- probability: 'NEGLIGIBLE',
44
- },
45
- ];
46
-
47
- const provider = createGoogleGenerativeAI({
48
- apiKey: 'test-api-key',
49
- generateId: () => 'test-id',
50
- });
51
- const model = provider.chat('gemini-pro');
52
-
53
- const groundingMetadataSchema = getGroundingMetadataSchema();
54
- const urlContextMetadataSchema = getUrlContextMetadataSchema();
55
-
56
- describe('groundingMetadataSchema', () => {
57
- it('validates complete grounding metadata with web search results', () => {
58
- const metadata = {
59
- webSearchQueries: ["What's the weather in Chicago this weekend?"],
60
- searchEntryPoint: {
61
- renderedContent: 'Sample rendered content for search results',
62
- },
63
- groundingChunks: [
64
- {
65
- web: {
66
- uri: 'https://example.com/weather',
67
- title: 'Chicago Weather Forecast',
68
- },
69
- },
70
- ],
71
- groundingSupports: [
72
- {
73
- segment: {
74
- startIndex: 0,
75
- endIndex: 65,
76
- text: 'Chicago weather changes rapidly, so layers let you adjust easily.',
77
- },
78
- groundingChunkIndices: [0],
79
- confidenceScores: [0.99],
80
- },
81
- ],
82
- retrievalMetadata: {
83
- webDynamicRetrievalScore: 0.96879,
84
- },
85
- };
86
-
87
- const result = groundingMetadataSchema.safeParse(metadata);
88
- expect(result.success).toBe(true);
89
- });
90
-
91
- it('validates groundingChunks[].web with missing title', () => {
92
- const metadata = {
93
- groundingChunks: [
94
- {
95
- web: {
96
- // Missing `title`
97
- uri: 'https://example.com/weather',
98
- },
99
- },
100
- ],
101
- };
102
-
103
- const result = groundingMetadataSchema.safeParse(metadata);
104
- expect(result.success).toBe(true);
105
- });
106
-
107
- it('validates complete grounding metadata with Vertex AI Search results', () => {
108
- const metadata = {
109
- retrievalQueries: ['How to make appointment to renew driving license?'],
110
- groundingChunks: [
111
- {
112
- retrievedContext: {
113
- uri: 'https://vertexaisearch.cloud.google.com/grounding-api-redirect/AXiHM.....QTN92V5ePQ==',
114
- title: 'dmv',
115
- },
116
- },
117
- ],
118
- groundingSupports: [
119
- {
120
- segment: {
121
- startIndex: 25,
122
- endIndex: 147,
123
- },
124
- segment_text: 'ipsum lorem ...',
125
- supportChunkIndices: [1, 2],
126
- confidenceScore: [0.9541752, 0.97726375],
127
- },
128
- ],
129
- };
130
-
131
- const result = groundingMetadataSchema.safeParse(metadata);
132
- expect(result.success).toBe(true);
133
- });
134
-
135
- it('validates groundingChunks[].retrievedContext with missing title', () => {
136
- const metadata = {
137
- groundingChunks: [
138
- {
139
- retrievedContext: {
140
- // Missing `title`
141
- uri: 'https://vertexaisearch.cloud.google.com/grounding-api-redirect/AXiHM.....QTN92V5ePQ==',
142
- },
143
- },
144
- ],
145
- };
146
-
147
- const result = groundingMetadataSchema.safeParse(metadata);
148
- expect(result.success).toBe(true);
149
- });
150
-
151
- it('validates groundingChunks[].retrievedContext with fileSearchStore (new format)', () => {
152
- const metadata = {
153
- groundingChunks: [
154
- {
155
- retrievedContext: {
156
- text: 'Sample content for testing...',
157
- fileSearchStore: 'fileSearchStores/test-store-xyz',
158
- title: 'Test Document',
159
- },
160
- },
161
- ],
162
- };
163
-
164
- const result = groundingMetadataSchema.safeParse(metadata);
165
- expect(result.success).toBe(true);
166
- });
167
-
168
- it('validates groundingChunks[].retrievedContext with fileSearchStore and missing uri', () => {
169
- const metadata = {
170
- groundingChunks: [
171
- {
172
- retrievedContext: {
173
- text: 'Content without uri field',
174
- fileSearchStore: 'fileSearchStores/store-abc',
175
- // Missing `uri` - should still be valid
176
- },
177
- },
178
- ],
179
- };
180
-
181
- const result = groundingMetadataSchema.safeParse(metadata);
182
- expect(result.success).toBe(true);
183
- });
184
-
185
- it('validates partial grounding metadata', () => {
186
- const metadata = {
187
- webSearchQueries: ['sample query'],
188
- // Missing other optional fields
189
- };
190
-
191
- const result = groundingMetadataSchema.safeParse(metadata);
192
- expect(result.success).toBe(true);
193
- });
194
-
195
- it('validates empty grounding metadata', () => {
196
- const metadata = {};
197
-
198
- const result = groundingMetadataSchema.safeParse(metadata);
199
- expect(result.success).toBe(true);
200
- });
201
-
202
- it('validates grounding metadata with maps chunks', () => {
203
- const metadata = {
204
- groundingChunks: [
205
- {
206
- maps: {
207
- uri: 'https://maps.google.com/maps?cid=12345',
208
- title: 'Best Italian Restaurant',
209
- text: 'A great Italian restaurant',
210
- placeId: 'ChIJ12345',
211
- },
212
- },
213
- ],
214
- };
215
-
216
- const result = groundingMetadataSchema.safeParse(metadata);
217
- expect(result.success).toBe(true);
218
- });
219
-
220
- it('validates groundingChunks[].maps with missing optional fields', () => {
221
- const metadata = {
222
- groundingChunks: [
223
- {
224
- maps: {
225
- uri: 'https://maps.google.com/maps?cid=12345',
226
- },
227
- },
228
- ],
229
- };
230
-
231
- const result = groundingMetadataSchema.safeParse(metadata);
232
- expect(result.success).toBe(true);
233
- });
234
-
235
- it('validates metadata with empty retrievalMetadata', () => {
236
- const metadata = {
237
- webSearchQueries: ['sample query'],
238
- retrievalMetadata: {},
239
- };
240
-
241
- const result = groundingMetadataSchema.safeParse(metadata);
242
- expect(result.success).toBe(true);
243
- });
244
-
245
- it('rejects invalid data types', () => {
246
- const metadata = {
247
- webSearchQueries: 'not an array', // Should be an array
248
- groundingSupports: [
249
- {
250
- confidenceScores: 'not an array', // Should be an array of numbers
251
- },
252
- ],
253
- };
254
-
255
- const result = groundingMetadataSchema.safeParse(metadata);
256
- expect(result.success).toBe(false);
257
- });
258
- });
259
-
260
- describe('urlContextMetadata', () => {
261
- it('validates complete url context output', () => {
262
- const output = {
263
- urlMetadata: [
264
- {
265
- retrievedUrl: 'https://example.com/weather',
266
- urlRetrievalStatus: 'URL_RETRIEVAL_STATUS_SUCCESS',
267
- },
268
- ],
269
- };
270
-
271
- const result = urlContextMetadataSchema.safeParse(output);
272
- expect(result.success).toBe(true);
273
- });
274
-
275
- it('validates empty url context output', () => {
276
- const output = {
277
- urlMetadata: [],
278
- };
279
-
280
- const result = urlContextMetadataSchema.safeParse(output);
281
- expect(result.success).toBe(true);
282
- });
283
- });
284
-
285
- describe('doGenerate', () => {
286
- const TEST_URL_GEMINI_PRO =
287
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent';
288
-
289
- const TEST_URL_GEMINI_2_0_PRO =
290
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-pro:generateContent';
291
-
292
- const TEST_URL_GEMINI_2_0_FLASH_EXP =
293
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent';
294
-
295
- const TEST_URL_GEMINI_1_0_PRO =
296
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.0-pro:generateContent';
297
-
298
- const TEST_URL_GEMINI_1_5_FLASH =
299
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent';
300
-
301
- const server = createTestServer({
302
- [TEST_URL_GEMINI_PRO]: {},
303
- [TEST_URL_GEMINI_2_0_PRO]: {},
304
- [TEST_URL_GEMINI_2_0_FLASH_EXP]: {},
305
- [TEST_URL_GEMINI_1_0_PRO]: {},
306
- [TEST_URL_GEMINI_1_5_FLASH]: {},
307
- });
308
-
309
- const prepareJsonResponse = ({
310
- content = '',
311
- usage = {
312
- promptTokenCount: 1,
313
- candidatesTokenCount: 2,
314
- totalTokenCount: 3,
315
- },
316
- headers,
317
- groundingMetadata,
318
- url = TEST_URL_GEMINI_PRO,
319
- }: {
320
- content?: string;
321
- usage?: {
322
- promptTokenCount: number;
323
- candidatesTokenCount: number;
324
- totalTokenCount: number;
325
- };
326
- headers?: Record<string, string>;
327
- groundingMetadata?: GoogleGenerativeAIGroundingMetadata;
328
- url?:
329
- | typeof TEST_URL_GEMINI_PRO
330
- | typeof TEST_URL_GEMINI_2_0_PRO
331
- | typeof TEST_URL_GEMINI_2_0_FLASH_EXP
332
- | typeof TEST_URL_GEMINI_1_0_PRO
333
- | typeof TEST_URL_GEMINI_1_5_FLASH;
334
- }) => {
335
- server.urls[url].response = {
336
- type: 'json-value',
337
- headers,
338
- body: {
339
- candidates: [
340
- {
341
- content: {
342
- parts: [{ text: content }],
343
- role: 'model',
344
- },
345
- finishReason: 'STOP',
346
- index: 0,
347
- safetyRatings: SAFETY_RATINGS,
348
- ...(groundingMetadata && { groundingMetadata }),
349
- },
350
- ],
351
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
352
- usageMetadata: usage,
353
- },
354
- };
355
- };
356
-
357
- it('should extract text response', async () => {
358
- prepareJsonResponse({ content: 'Hello, World!' });
359
-
360
- const { content } = await model.doGenerate({
361
- prompt: TEST_PROMPT,
362
- });
363
-
364
- expect(content).toMatchInlineSnapshot(`
365
- [
366
- {
367
- "providerMetadata": undefined,
368
- "text": "Hello, World!",
369
- "type": "text",
370
- },
371
- ]
372
- `);
373
- });
374
-
375
- it('should extract usage', async () => {
376
- prepareJsonResponse({
377
- usage: {
378
- promptTokenCount: 20,
379
- candidatesTokenCount: 5,
380
- totalTokenCount: 25,
381
- },
382
- });
383
-
384
- const { usage } = await model.doGenerate({
385
- prompt: TEST_PROMPT,
386
- });
387
-
388
- expect(usage).toMatchInlineSnapshot(`
389
- {
390
- "inputTokens": {
391
- "cacheRead": 0,
392
- "cacheWrite": undefined,
393
- "noCache": 20,
394
- "total": 20,
395
- },
396
- "outputTokens": {
397
- "reasoning": 0,
398
- "text": 5,
399
- "total": 5,
400
- },
401
- "raw": {
402
- "candidatesTokenCount": 5,
403
- "promptTokenCount": 20,
404
- "totalTokenCount": 25,
405
- },
406
- }
407
- `);
408
- });
409
- it('should handle MALFORMED_FUNCTION_CALL finish reason and empty content object', async () => {
410
- server.urls[TEST_URL_GEMINI_PRO].response = {
411
- type: 'json-value',
412
- body: {
413
- candidates: [
414
- {
415
- content: {},
416
- finishReason: 'MALFORMED_FUNCTION_CALL',
417
- },
418
- ],
419
- usageMetadata: {
420
- promptTokenCount: 9056,
421
- totalTokenCount: 9056,
422
- promptTokensDetails: [
423
- {
424
- modality: 'TEXT',
425
- tokenCount: 9056,
426
- },
427
- ],
428
- },
429
- modelVersion: 'gemini-2.0-flash-lite',
430
- },
431
- };
432
-
433
- const { content, finishReason } = await model.doGenerate({
434
- prompt: TEST_PROMPT,
435
- });
436
-
437
- expect(content).toMatchInlineSnapshot(`[]`);
438
- expect(finishReason).toMatchInlineSnapshot(`
439
- {
440
- "raw": "MALFORMED_FUNCTION_CALL",
441
- "unified": "error",
442
- }
443
- `);
444
- });
445
-
446
- it('should extract tool calls', async () => {
447
- server.urls[TEST_URL_GEMINI_PRO].response = {
448
- type: 'json-value',
449
- body: {
450
- candidates: [
451
- {
452
- content: {
453
- parts: [
454
- {
455
- functionCall: {
456
- name: 'test-tool',
457
- args: { value: 'example value' },
458
- },
459
- },
460
- ],
461
- role: 'model',
462
- },
463
- finishReason: 'STOP',
464
- index: 0,
465
- safetyRatings: SAFETY_RATINGS,
466
- },
467
- ],
468
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
469
- },
470
- };
471
-
472
- const { content, finishReason } = await model.doGenerate({
473
- tools: [
474
- {
475
- type: 'function',
476
- name: 'test-tool',
477
- inputSchema: {
478
- type: 'object',
479
- properties: { value: { type: 'string' } },
480
- required: ['value'],
481
- additionalProperties: false,
482
- $schema: 'http://json-schema.org/draft-07/schema#',
483
- },
484
- },
485
- ],
486
- prompt: TEST_PROMPT,
487
- });
488
-
489
- expect(content).toMatchInlineSnapshot(`
490
- [
491
- {
492
- "input": "{"value":"example value"}",
493
- "providerMetadata": undefined,
494
- "toolCallId": "test-id",
495
- "toolName": "test-tool",
496
- "type": "tool-call",
497
- },
498
- ]
499
- `);
500
- expect(finishReason).toMatchInlineSnapshot(`
501
- {
502
- "raw": "STOP",
503
- "unified": "tool-calls",
504
- }
505
- `);
506
- });
507
-
508
- it('should expose the raw response headers', async () => {
509
- prepareJsonResponse({ headers: { 'test-header': 'test-value' } });
510
-
511
- const { response } = await model.doGenerate({
512
- prompt: TEST_PROMPT,
513
- });
514
-
515
- expect(response?.headers).toStrictEqual({
516
- // default headers:
517
- 'content-length': '804',
518
- 'content-type': 'application/json',
519
-
520
- // custom header
521
- 'test-header': 'test-value',
522
- });
523
- });
524
-
525
- it('should pass the model, messages, and options', async () => {
526
- prepareJsonResponse({});
527
-
528
- await model.doGenerate({
529
- prompt: [
530
- { role: 'system', content: 'test system instruction' },
531
- { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
532
- ],
533
- seed: 123,
534
- temperature: 0.5,
535
- });
536
-
537
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
538
- contents: [
539
- {
540
- role: 'user',
541
- parts: [{ text: 'Hello' }],
542
- },
543
- ],
544
- systemInstruction: { parts: [{ text: 'test system instruction' }] },
545
- generationConfig: {
546
- seed: 123,
547
- temperature: 0.5,
548
- },
549
- });
550
- });
551
-
552
- it('should only pass valid provider options', async () => {
553
- prepareJsonResponse({});
554
-
555
- await model.doGenerate({
556
- prompt: [
557
- { role: 'system', content: 'test system instruction' },
558
- { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
559
- ],
560
- seed: 123,
561
- temperature: 0.5,
562
- providerOptions: {
563
- google: { foo: 'bar', responseModalities: ['TEXT', 'IMAGE'] },
564
- },
565
- });
566
-
567
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
568
- contents: [
569
- {
570
- role: 'user',
571
- parts: [{ text: 'Hello' }],
572
- },
573
- ],
574
- systemInstruction: { parts: [{ text: 'test system instruction' }] },
575
- generationConfig: {
576
- seed: 123,
577
- temperature: 0.5,
578
- responseModalities: ['TEXT', 'IMAGE'],
579
- },
580
- });
581
- });
582
-
583
- it('should pass tools and toolChoice', async () => {
584
- prepareJsonResponse({});
585
-
586
- await model.doGenerate({
587
- tools: [
588
- {
589
- type: 'function',
590
- name: 'test-tool',
591
- inputSchema: {
592
- type: 'object',
593
- properties: { value: { type: 'string' } },
594
- required: ['value'],
595
- additionalProperties: false,
596
- $schema: 'http://json-schema.org/draft-07/schema#',
597
- },
598
- },
599
- ],
600
- toolChoice: {
601
- type: 'tool',
602
- toolName: 'test-tool',
603
- },
604
- prompt: TEST_PROMPT,
605
- });
606
-
607
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
608
- generationConfig: {},
609
- contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
610
- tools: [
611
- {
612
- functionDeclarations: [
613
- {
614
- name: 'test-tool',
615
- description: '',
616
- parameters: {
617
- type: 'object',
618
- properties: { value: { type: 'string' } },
619
- required: ['value'],
620
- },
621
- },
622
- ],
623
- },
624
- ],
625
- toolConfig: {
626
- functionCallingConfig: {
627
- mode: 'ANY',
628
- allowedFunctionNames: ['test-tool'],
629
- },
630
- },
631
- });
632
- });
633
-
634
- it('should set response mime type with responseFormat', async () => {
635
- prepareJsonResponse({});
636
-
637
- await model.doGenerate({
638
- responseFormat: {
639
- type: 'json',
640
- schema: {
641
- type: 'object',
642
- properties: { location: { type: 'string' } },
643
- },
644
- },
645
- prompt: TEST_PROMPT,
646
- });
647
-
648
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
649
- contents: [
650
- {
651
- role: 'user',
652
- parts: [{ text: 'Hello' }],
653
- },
654
- ],
655
- generationConfig: {
656
- responseMimeType: 'application/json',
657
- responseSchema: {
658
- type: 'object',
659
- properties: {
660
- location: {
661
- type: 'string',
662
- },
663
- },
664
- },
665
- },
666
- });
667
- });
668
-
669
- it('should pass specification with responseFormat and structuredOutputs = true (default)', async () => {
670
- prepareJsonResponse({});
671
-
672
- await provider.languageModel('gemini-pro').doGenerate({
673
- responseFormat: {
674
- type: 'json',
675
- schema: {
676
- type: 'object',
677
- properties: {
678
- property1: { type: 'string' },
679
- property2: { type: 'number' },
680
- },
681
- required: ['property1', 'property2'],
682
- additionalProperties: false,
683
- },
684
- },
685
- prompt: TEST_PROMPT,
686
- });
687
-
688
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
689
- contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
690
- generationConfig: {
691
- responseMimeType: 'application/json',
692
- responseSchema: {
693
- properties: {
694
- property1: { type: 'string' },
695
- property2: { type: 'number' },
696
- },
697
- required: ['property1', 'property2'],
698
- type: 'object',
699
- },
700
- },
701
- });
702
- });
703
-
704
- it('should not pass specification with responseFormat and structuredOutputs = false', async () => {
705
- prepareJsonResponse({});
706
-
707
- await provider.languageModel('gemini-pro').doGenerate({
708
- providerOptions: {
709
- google: {
710
- structuredOutputs: false,
711
- },
712
- },
713
- responseFormat: {
714
- type: 'json',
715
- schema: {
716
- type: 'object',
717
- properties: {
718
- property1: { type: 'string' },
719
- property2: { type: 'number' },
720
- },
721
- required: ['property1', 'property2'],
722
- additionalProperties: false,
723
- },
724
- },
725
- prompt: TEST_PROMPT,
726
- });
727
-
728
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
729
- contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
730
- generationConfig: {
731
- responseMimeType: 'application/json',
732
- },
733
- });
734
- });
735
-
736
- it('should pass tools and toolChoice', async () => {
737
- prepareJsonResponse({});
738
-
739
- await provider.languageModel('gemini-pro').doGenerate({
740
- tools: [
741
- {
742
- name: 'test-tool',
743
- type: 'function',
744
- inputSchema: {
745
- type: 'object',
746
- properties: {
747
- property1: { type: 'string' },
748
- property2: { type: 'number' },
749
- },
750
- required: ['property1', 'property2'],
751
- additionalProperties: false,
752
- },
753
- },
754
- ],
755
- toolChoice: { type: 'required' },
756
- prompt: TEST_PROMPT,
757
- });
758
-
759
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
760
- contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
761
- generationConfig: {},
762
- toolConfig: { functionCallingConfig: { mode: 'ANY' } },
763
- tools: [
764
- {
765
- functionDeclarations: [
766
- {
767
- name: 'test-tool',
768
- description: '',
769
- parameters: {
770
- properties: {
771
- property1: { type: 'string' },
772
- property2: { type: 'number' },
773
- },
774
- required: ['property1', 'property2'],
775
- type: 'object',
776
- },
777
- },
778
- ],
779
- },
780
- ],
781
- });
782
- });
783
-
784
- it('should pass headers', async () => {
785
- prepareJsonResponse({});
786
-
787
- const provider = createGoogleGenerativeAI({
788
- apiKey: 'test-api-key',
789
- headers: {
790
- 'Custom-Provider-Header': 'provider-header-value',
791
- },
792
- });
793
-
794
- await provider.chat('gemini-pro').doGenerate({
795
- prompt: TEST_PROMPT,
796
- headers: {
797
- 'Custom-Request-Header': 'request-header-value',
798
- },
799
- });
800
-
801
- const requestHeaders = server.calls[0].requestHeaders;
802
-
803
- expect(requestHeaders).toStrictEqual({
804
- 'content-type': 'application/json',
805
- 'custom-provider-header': 'provider-header-value',
806
- 'custom-request-header': 'request-header-value',
807
- 'x-goog-api-key': 'test-api-key',
808
- });
809
- expect(server.calls[0].requestUserAgent).toContain(
810
- `ai-sdk/google/0.0.0-test`,
811
- );
812
- });
813
-
814
- it('should pass response format', async () => {
815
- prepareJsonResponse({});
816
-
817
- await model.doGenerate({
818
- prompt: TEST_PROMPT,
819
- responseFormat: {
820
- type: 'json',
821
- schema: {
822
- type: 'object',
823
- properties: {
824
- text: { type: 'string' },
825
- },
826
- required: ['text'],
827
- },
828
- },
829
- });
830
-
831
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
832
- contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
833
- generationConfig: {
834
- responseMimeType: 'application/json',
835
- responseSchema: {
836
- type: 'object',
837
- properties: {
838
- text: { type: 'string' },
839
- },
840
- required: ['text'],
841
- },
842
- },
843
- });
844
- });
845
-
846
- it('should send request body', async () => {
847
- prepareJsonResponse({});
848
-
849
- await model.doGenerate({
850
- prompt: TEST_PROMPT,
851
- });
852
-
853
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
854
- contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
855
- generationConfig: {},
856
- });
857
- });
858
-
859
- it('should extract sources from grounding metadata', async () => {
860
- prepareJsonResponse({
861
- content: 'test response',
862
- groundingMetadata: {
863
- groundingChunks: [
864
- {
865
- web: { uri: 'https://source.example.com', title: 'Source Title' },
866
- },
867
- ],
868
- },
869
- });
870
-
871
- const { content } = await model.doGenerate({
872
- prompt: TEST_PROMPT,
873
- });
874
-
875
- expect(content).toMatchInlineSnapshot(`
876
- [
877
- {
878
- "providerMetadata": undefined,
879
- "text": "test response",
880
- "type": "text",
881
- },
882
- {
883
- "id": "test-id",
884
- "sourceType": "url",
885
- "title": "Source Title",
886
- "type": "source",
887
- "url": "https://source.example.com",
888
- },
889
- ]
890
- `);
891
- });
892
-
893
- it('should extract sources from RAG retrievedContext chunks', async () => {
894
- prepareJsonResponse({
895
- content: 'test response with RAG',
896
- groundingMetadata: {
897
- groundingChunks: [
898
- {
899
- web: { uri: 'https://web.example.com', title: 'Web Source' },
900
- },
901
- {
902
- retrievedContext: {
903
- uri: 'gs://rag-corpus/document.pdf',
904
- title: 'RAG Document',
905
- text: 'Retrieved context...',
906
- },
907
- },
908
- {
909
- retrievedContext: {
910
- uri: 'https://external-rag-source.com/page',
911
- title: 'External RAG Source',
912
- text: 'External retrieved context...',
913
- },
914
- },
915
- ],
916
- },
917
- });
918
-
919
- const { content } = await model.doGenerate({
920
- prompt: TEST_PROMPT,
921
- });
922
-
923
- expect(content).toMatchInlineSnapshot(`
924
- [
925
- {
926
- "providerMetadata": undefined,
927
- "text": "test response with RAG",
928
- "type": "text",
929
- },
930
- {
931
- "id": "test-id",
932
- "sourceType": "url",
933
- "title": "Web Source",
934
- "type": "source",
935
- "url": "https://web.example.com",
936
- },
937
- {
938
- "filename": "document.pdf",
939
- "id": "test-id",
940
- "mediaType": "application/pdf",
941
- "sourceType": "document",
942
- "title": "RAG Document",
943
- "type": "source",
944
- },
945
- {
946
- "id": "test-id",
947
- "sourceType": "url",
948
- "title": "External RAG Source",
949
- "type": "source",
950
- "url": "https://external-rag-source.com/page",
951
- },
952
- ]
953
- `);
954
- });
955
-
956
- it('should extract sources from File Search retrievedContext (new format)', async () => {
957
- prepareJsonResponse({
958
- content: 'test response with File Search',
959
- groundingMetadata: {
960
- groundingChunks: [
961
- {
962
- retrievedContext: {
963
- text: 'Sample content for testing...',
964
- fileSearchStore: 'fileSearchStores/test-store-xyz',
965
- title: 'Test Document',
966
- },
967
- },
968
- {
969
- retrievedContext: {
970
- text: 'Another document content...',
971
- fileSearchStore: 'fileSearchStores/another-store-abc',
972
- // Missing title - should default to 'Unknown Document'
973
- },
974
- },
975
- ],
976
- },
977
- });
978
-
979
- const { content } = await model.doGenerate({
980
- prompt: TEST_PROMPT,
981
- });
982
-
983
- expect(content).toMatchInlineSnapshot(`
984
- [
985
- {
986
- "providerMetadata": undefined,
987
- "text": "test response with File Search",
988
- "type": "text",
989
- },
990
- {
991
- "filename": "test-store-xyz",
992
- "id": "test-id",
993
- "mediaType": "application/octet-stream",
994
- "sourceType": "document",
995
- "title": "Test Document",
996
- "type": "source",
997
- },
998
- {
999
- "filename": "another-store-abc",
1000
- "id": "test-id",
1001
- "mediaType": "application/octet-stream",
1002
- "sourceType": "document",
1003
- "title": "Unknown Document",
1004
- "type": "source",
1005
- },
1006
- ]
1007
- `);
1008
- });
1009
-
1010
- it('should handle URL sources with undefined title correctly', async () => {
1011
- prepareJsonResponse({
1012
- content: 'test response with URLs',
1013
- groundingMetadata: {
1014
- groundingChunks: [
1015
- {
1016
- web: {
1017
- uri: 'https://example.com/page1',
1018
- // No title provided
1019
- },
1020
- },
1021
- {
1022
- retrievedContext: {
1023
- uri: 'https://example.com/page2',
1024
- // No title provided
1025
- },
1026
- },
1027
- ],
1028
- },
1029
- });
1030
-
1031
- const { content } = await model.doGenerate({
1032
- prompt: TEST_PROMPT,
1033
- });
1034
-
1035
- expect(content).toMatchInlineSnapshot(`
1036
- [
1037
- {
1038
- "providerMetadata": undefined,
1039
- "text": "test response with URLs",
1040
- "type": "text",
1041
- },
1042
- {
1043
- "id": "test-id",
1044
- "sourceType": "url",
1045
- "title": undefined,
1046
- "type": "source",
1047
- "url": "https://example.com/page1",
1048
- },
1049
- {
1050
- "id": "test-id",
1051
- "sourceType": "url",
1052
- "title": undefined,
1053
- "type": "source",
1054
- "url": "https://example.com/page2",
1055
- },
1056
- ]
1057
- `);
1058
- });
1059
-
1060
- it('should extract sources from maps grounding metadata', async () => {
1061
- prepareJsonResponse({
1062
- content: 'test response with Maps',
1063
- groundingMetadata: {
1064
- groundingChunks: [
1065
- {
1066
- maps: {
1067
- uri: 'https://maps.google.com/maps?cid=12345',
1068
- title: 'Best Italian Restaurant',
1069
- placeId: 'ChIJ12345',
1070
- },
1071
- },
1072
- {
1073
- maps: {
1074
- uri: 'https://maps.google.com/maps?cid=67890',
1075
- },
1076
- },
1077
- ],
1078
- },
1079
- });
1080
-
1081
- const { content } = await model.doGenerate({
1082
- prompt: TEST_PROMPT,
1083
- });
1084
-
1085
- expect(content).toMatchInlineSnapshot(`
1086
- [
1087
- {
1088
- "providerMetadata": undefined,
1089
- "text": "test response with Maps",
1090
- "type": "text",
1091
- },
1092
- {
1093
- "id": "test-id",
1094
- "sourceType": "url",
1095
- "title": "Best Italian Restaurant",
1096
- "type": "source",
1097
- "url": "https://maps.google.com/maps?cid=12345",
1098
- },
1099
- {
1100
- "id": "test-id",
1101
- "sourceType": "url",
1102
- "title": undefined,
1103
- "type": "source",
1104
- "url": "https://maps.google.com/maps?cid=67890",
1105
- },
1106
- ]
1107
- `);
1108
- });
1109
-
1110
- it('should handle mixed source types with correct title defaults', async () => {
1111
- prepareJsonResponse({
1112
- content: 'test response with mixed sources',
1113
- groundingMetadata: {
1114
- groundingChunks: [
1115
- {
1116
- web: { uri: 'https://web.example.com' },
1117
- },
1118
- {
1119
- retrievedContext: {
1120
- uri: 'https://external.example.com',
1121
- },
1122
- },
1123
- {
1124
- retrievedContext: {
1125
- uri: 'gs://bucket/document.pdf',
1126
- },
1127
- },
1128
- {
1129
- retrievedContext: {
1130
- fileSearchStore: 'fileSearchStores/store-123',
1131
- },
1132
- },
1133
- ],
1134
- },
1135
- });
1136
-
1137
- const { content } = await model.doGenerate({
1138
- prompt: TEST_PROMPT,
1139
- });
1140
-
1141
- expect(content).toMatchInlineSnapshot(`
1142
- [
1143
- {
1144
- "providerMetadata": undefined,
1145
- "text": "test response with mixed sources",
1146
- "type": "text",
1147
- },
1148
- {
1149
- "id": "test-id",
1150
- "sourceType": "url",
1151
- "title": undefined,
1152
- "type": "source",
1153
- "url": "https://web.example.com",
1154
- },
1155
- {
1156
- "id": "test-id",
1157
- "sourceType": "url",
1158
- "title": undefined,
1159
- "type": "source",
1160
- "url": "https://external.example.com",
1161
- },
1162
- {
1163
- "filename": "document.pdf",
1164
- "id": "test-id",
1165
- "mediaType": "application/pdf",
1166
- "sourceType": "document",
1167
- "title": "Unknown Document",
1168
- "type": "source",
1169
- },
1170
- {
1171
- "filename": "store-123",
1172
- "id": "test-id",
1173
- "mediaType": "application/octet-stream",
1174
- "sourceType": "document",
1175
- "title": "Unknown Document",
1176
- "type": "source",
1177
- },
1178
- ]
1179
- `);
1180
- });
1181
-
1182
- describe('async headers handling', () => {
1183
- it('merges async config headers with sync request headers', async () => {
1184
- server.urls[TEST_URL_GEMINI_PRO].response = {
1185
- type: 'json-value',
1186
- body: {
1187
- candidates: [
1188
- {
1189
- content: {
1190
- parts: [{ text: '' }],
1191
- role: 'model',
1192
- },
1193
- finishReason: 'STOP',
1194
- index: 0,
1195
- safetyRatings: SAFETY_RATINGS,
1196
- },
1197
- ],
1198
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
1199
- usageMetadata: {
1200
- promptTokenCount: 1,
1201
- candidatesTokenCount: 2,
1202
- totalTokenCount: 3,
1203
- },
1204
- },
1205
- };
1206
-
1207
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
1208
- provider: 'google.generative-ai',
1209
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
1210
- headers: async () => ({
1211
- 'X-Async-Config': 'async-config-value',
1212
- 'X-Common': 'config-value',
1213
- }),
1214
- generateId: () => 'test-id',
1215
- supportedUrls: () => ({
1216
- '*': [/^https?:\/\/.*$/],
1217
- }),
1218
- });
1219
-
1220
- await model.doGenerate({
1221
- prompt: TEST_PROMPT,
1222
- headers: {
1223
- 'X-Sync-Request': 'sync-request-value',
1224
- 'X-Common': 'request-value', // Should override config value
1225
- },
1226
- });
1227
-
1228
- expect(server.calls[0].requestHeaders).toStrictEqual({
1229
- 'content-type': 'application/json',
1230
- 'x-async-config': 'async-config-value',
1231
- 'x-sync-request': 'sync-request-value',
1232
- 'x-common': 'request-value', // Request headers take precedence
1233
- });
1234
- });
1235
-
1236
- it('handles Promise-based headers', async () => {
1237
- server.urls[TEST_URL_GEMINI_PRO].response = {
1238
- type: 'json-value',
1239
- body: {
1240
- candidates: [
1241
- {
1242
- content: {
1243
- parts: [{ text: '' }],
1244
- role: 'model',
1245
- },
1246
- finishReason: 'STOP',
1247
- index: 0,
1248
- safetyRatings: SAFETY_RATINGS,
1249
- },
1250
- ],
1251
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
1252
- usageMetadata: {
1253
- promptTokenCount: 1,
1254
- candidatesTokenCount: 2,
1255
- totalTokenCount: 3,
1256
- },
1257
- },
1258
- };
1259
-
1260
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
1261
- provider: 'google.generative-ai',
1262
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
1263
- headers: async () => ({
1264
- 'X-Promise-Header': 'promise-value',
1265
- }),
1266
- generateId: () => 'test-id',
1267
- });
1268
-
1269
- await model.doGenerate({
1270
- prompt: TEST_PROMPT,
1271
- });
1272
-
1273
- expect(server.calls[0].requestHeaders).toStrictEqual({
1274
- 'content-type': 'application/json',
1275
- 'x-promise-header': 'promise-value',
1276
- });
1277
- });
1278
-
1279
- it('handles async function headers from config', async () => {
1280
- prepareJsonResponse({});
1281
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
1282
- provider: 'google.generative-ai',
1283
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
1284
- headers: async () => ({
1285
- 'X-Async-Header': 'async-value',
1286
- }),
1287
- generateId: () => 'test-id',
1288
- });
1289
-
1290
- await model.doGenerate({
1291
- prompt: TEST_PROMPT,
1292
- });
1293
-
1294
- expect(server.calls[0].requestHeaders).toStrictEqual({
1295
- 'content-type': 'application/json',
1296
- 'x-async-header': 'async-value',
1297
- });
1298
- });
1299
- });
1300
-
1301
- it('should expose safety ratings in provider metadata', async () => {
1302
- server.urls[TEST_URL_GEMINI_PRO].response = {
1303
- type: 'json-value',
1304
- body: {
1305
- candidates: [
1306
- {
1307
- content: {
1308
- parts: [{ text: 'test response' }],
1309
- role: 'model',
1310
- },
1311
- finishReason: 'STOP',
1312
- index: 0,
1313
- safetyRatings: [
1314
- {
1315
- category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
1316
- probability: 'NEGLIGIBLE',
1317
- probabilityScore: 0.1,
1318
- severity: 'LOW',
1319
- severityScore: 0.2,
1320
- blocked: false,
1321
- },
1322
- ],
1323
- },
1324
- ],
1325
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
1326
- },
1327
- };
1328
-
1329
- const { providerMetadata } = await model.doGenerate({
1330
- prompt: TEST_PROMPT,
1331
- });
1332
-
1333
- expect(providerMetadata?.google.safetyRatings).toStrictEqual([
1334
- {
1335
- category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
1336
- probability: 'NEGLIGIBLE',
1337
- probabilityScore: 0.1,
1338
- severity: 'LOW',
1339
- severityScore: 0.2,
1340
- blocked: false,
1341
- },
1342
- ]);
1343
- });
1344
-
1345
- it('should expose PromptFeedback in provider metadata', async () => {
1346
- server.urls[TEST_URL_GEMINI_PRO].response = {
1347
- type: 'json-value',
1348
- body: {
1349
- candidates: [
1350
- {
1351
- content: { parts: [{ text: 'No' }], role: 'model' },
1352
- finishReason: 'SAFETY',
1353
- index: 0,
1354
- safetyRatings: SAFETY_RATINGS,
1355
- },
1356
- ],
1357
- promptFeedback: {
1358
- blockReason: 'SAFETY',
1359
- safetyRatings: SAFETY_RATINGS,
1360
- },
1361
- },
1362
- };
1363
-
1364
- const { providerMetadata } = await model.doGenerate({
1365
- prompt: TEST_PROMPT,
1366
- });
1367
-
1368
- expect(providerMetadata?.google.promptFeedback).toStrictEqual({
1369
- blockReason: 'SAFETY',
1370
- safetyRatings: SAFETY_RATINGS,
1371
- });
1372
- });
1373
-
1374
- it('should expose grounding metadata in provider metadata', async () => {
1375
- prepareJsonResponse({
1376
- content: 'test response',
1377
- groundingMetadata: {
1378
- webSearchQueries: ["What's the weather in Chicago this weekend?"],
1379
- searchEntryPoint: {
1380
- renderedContent: 'Sample rendered content for search results',
1381
- },
1382
- groundingChunks: [
1383
- {
1384
- web: {
1385
- uri: 'https://example.com/weather',
1386
- title: 'Chicago Weather Forecast',
1387
- },
1388
- },
1389
- ],
1390
- groundingSupports: [
1391
- {
1392
- segment: {
1393
- startIndex: 0,
1394
- endIndex: 65,
1395
- text: 'Chicago weather changes rapidly, so layers let you adjust easily.',
1396
- },
1397
- groundingChunkIndices: [0],
1398
- confidenceScores: [0.99],
1399
- },
1400
- ],
1401
- retrievalMetadata: {
1402
- webDynamicRetrievalScore: 0.96879,
1403
- },
1404
- },
1405
- });
1406
-
1407
- const { providerMetadata } = await model.doGenerate({
1408
- prompt: TEST_PROMPT,
1409
- });
1410
-
1411
- expect(providerMetadata?.google.groundingMetadata).toStrictEqual({
1412
- webSearchQueries: ["What's the weather in Chicago this weekend?"],
1413
- searchEntryPoint: {
1414
- renderedContent: 'Sample rendered content for search results',
1415
- },
1416
- groundingChunks: [
1417
- {
1418
- web: {
1419
- uri: 'https://example.com/weather',
1420
- title: 'Chicago Weather Forecast',
1421
- },
1422
- },
1423
- ],
1424
- groundingSupports: [
1425
- {
1426
- segment: {
1427
- startIndex: 0,
1428
- endIndex: 65,
1429
- text: 'Chicago weather changes rapidly, so layers let you adjust easily.',
1430
- },
1431
- groundingChunkIndices: [0],
1432
- confidenceScores: [0.99],
1433
- },
1434
- ],
1435
- retrievalMetadata: {
1436
- webDynamicRetrievalScore: 0.96879,
1437
- },
1438
- });
1439
- });
1440
-
1441
- it('should handle code execution tool calls', async () => {
1442
- server.urls[TEST_URL_GEMINI_2_0_PRO].response = {
1443
- type: 'json-value',
1444
- body: {
1445
- candidates: [
1446
- {
1447
- content: {
1448
- parts: [
1449
- {
1450
- executableCode: {
1451
- language: 'PYTHON',
1452
- code: 'print(1+1)',
1453
- },
1454
- },
1455
- {
1456
- codeExecutionResult: {
1457
- outcome: 'OUTCOME_OK',
1458
- output: '2',
1459
- },
1460
- },
1461
- ],
1462
- role: 'model',
1463
- },
1464
- finishReason: 'STOP',
1465
- },
1466
- ],
1467
- },
1468
- };
1469
-
1470
- const model = provider.languageModel('gemini-2.0-pro');
1471
- const { content } = await model.doGenerate({
1472
- tools: [
1473
- {
1474
- type: 'provider',
1475
- id: 'google.code_execution',
1476
- name: 'code_execution',
1477
- args: {},
1478
- },
1479
- ],
1480
- prompt: TEST_PROMPT,
1481
- });
1482
-
1483
- const requestBody = await server.calls[0].requestBodyJson;
1484
- expect(requestBody.tools).toEqual([{ codeExecution: {} }]);
1485
-
1486
- expect(content).toMatchInlineSnapshot(`
1487
- [
1488
- {
1489
- "input": "{"language":"PYTHON","code":"print(1+1)"}",
1490
- "providerExecuted": true,
1491
- "toolCallId": "test-id",
1492
- "toolName": "code_execution",
1493
- "type": "tool-call",
1494
- },
1495
- {
1496
- "result": {
1497
- "outcome": "OUTCOME_OK",
1498
- "output": "2",
1499
- },
1500
- "toolCallId": "test-id",
1501
- "toolName": "code_execution",
1502
- "type": "tool-result",
1503
- },
1504
- ]
1505
- `);
1506
- });
1507
-
1508
- it('should return stop finish reason for code execution (provider-executed tool)', async () => {
1509
- server.urls[TEST_URL_GEMINI_2_0_PRO].response = {
1510
- type: 'json-value',
1511
- body: {
1512
- candidates: [
1513
- {
1514
- content: {
1515
- parts: [
1516
- {
1517
- executableCode: {
1518
- language: 'PYTHON',
1519
- code: 'print(1+1)',
1520
- },
1521
- },
1522
- {
1523
- codeExecutionResult: {
1524
- outcome: 'OUTCOME_OK',
1525
- output: '2',
1526
- },
1527
- },
1528
- ],
1529
- role: 'model',
1530
- },
1531
- finishReason: 'STOP',
1532
- },
1533
- ],
1534
- },
1535
- };
1536
-
1537
- const model = provider.languageModel('gemini-2.0-pro');
1538
- const { finishReason } = await model.doGenerate({
1539
- tools: [
1540
- {
1541
- type: 'provider',
1542
- id: 'google.code_execution',
1543
- name: 'code_execution',
1544
- args: {},
1545
- },
1546
- ],
1547
- prompt: TEST_PROMPT,
1548
- });
1549
-
1550
- // Provider-executed tools should not trigger 'tool-calls' finish reason
1551
- // since they don't require SDK iteration
1552
- expect(finishReason).toMatchInlineSnapshot(`
1553
- {
1554
- "raw": "STOP",
1555
- "unified": "stop",
1556
- }
1557
- `);
1558
- });
1559
-
1560
- it('should return stop finish reason for code execution with text response (structured output scenario)', async () => {
1561
- server.urls[TEST_URL_GEMINI_2_0_PRO].response = {
1562
- type: 'json-value',
1563
- body: {
1564
- candidates: [
1565
- {
1566
- content: {
1567
- parts: [
1568
- {
1569
- executableCode: {
1570
- language: 'PYTHON',
1571
- code: 'primes = [2, 3, 5, 7, 11]\nprint(sum(primes))',
1572
- },
1573
- },
1574
- {
1575
- codeExecutionResult: {
1576
- outcome: 'OUTCOME_OK',
1577
- output: '28',
1578
- },
1579
- },
1580
- {
1581
- text: '{"answer": 28, "explanation": "Sum of first 5 primes"}',
1582
- },
1583
- ],
1584
- role: 'model',
1585
- },
1586
- finishReason: 'STOP',
1587
- },
1588
- ],
1589
- },
1590
- };
1591
-
1592
- const model = provider.languageModel('gemini-2.0-pro');
1593
- const { finishReason, content } = await model.doGenerate({
1594
- tools: [
1595
- {
1596
- type: 'provider',
1597
- id: 'google.code_execution',
1598
- name: 'code_execution',
1599
- args: {},
1600
- },
1601
- ],
1602
- prompt: TEST_PROMPT,
1603
- });
1604
-
1605
- // Should return 'stop' so structured output can be parsed
1606
- expect(finishReason).toMatchInlineSnapshot(`
1607
- {
1608
- "raw": "STOP",
1609
- "unified": "stop",
1610
- }
1611
- `);
1612
-
1613
- // Verify text content is included
1614
- const textPart = content.find(part => part.type === 'text');
1615
- expect(textPart).toBeDefined();
1616
- expect((textPart as { type: 'text'; text: string }).text).toBe(
1617
- '{"answer": 28, "explanation": "Sum of first 5 primes"}',
1618
- );
1619
- });
1620
-
1621
- it('should return tool-calls finish reason when code execution is combined with function tools', async () => {
1622
- server.urls[TEST_URL_GEMINI_2_0_PRO].response = {
1623
- type: 'json-value',
1624
- body: {
1625
- candidates: [
1626
- {
1627
- content: {
1628
- parts: [
1629
- {
1630
- executableCode: {
1631
- language: 'PYTHON',
1632
- code: 'print(1+1)',
1633
- },
1634
- },
1635
- {
1636
- codeExecutionResult: {
1637
- outcome: 'OUTCOME_OK',
1638
- output: '2',
1639
- },
1640
- },
1641
- {
1642
- functionCall: {
1643
- name: 'test-tool',
1644
- args: { value: 'test' },
1645
- },
1646
- },
1647
- ],
1648
- role: 'model',
1649
- },
1650
- finishReason: 'STOP',
1651
- },
1652
- ],
1653
- },
1654
- };
1655
-
1656
- const model = provider.languageModel('gemini-2.0-pro');
1657
- const { finishReason } = await model.doGenerate({
1658
- tools: [
1659
- {
1660
- type: 'provider',
1661
- id: 'google.code_execution',
1662
- name: 'code_execution',
1663
- args: {},
1664
- },
1665
- {
1666
- type: 'function',
1667
- name: 'test-tool',
1668
- inputSchema: {
1669
- type: 'object',
1670
- properties: { value: { type: 'string' } },
1671
- },
1672
- },
1673
- ],
1674
- prompt: TEST_PROMPT,
1675
- });
1676
-
1677
- // Should return 'tool-calls' because there's a client-executed function tool
1678
- expect(finishReason).toMatchInlineSnapshot(`
1679
- {
1680
- "raw": "STOP",
1681
- "unified": "tool-calls",
1682
- }
1683
- `);
1684
- });
1685
-
1686
- describe('search tool selection', () => {
1687
- const provider = createGoogleGenerativeAI({
1688
- apiKey: 'test-api-key',
1689
- generateId: () => 'test-id',
1690
- });
1691
-
1692
- it('should use googleSearch for gemini-2.0-pro', async () => {
1693
- prepareJsonResponse({
1694
- url: TEST_URL_GEMINI_2_0_PRO,
1695
- });
1696
-
1697
- const gemini2Pro = provider.languageModel('gemini-2.0-pro');
1698
- await gemini2Pro.doGenerate({
1699
- prompt: TEST_PROMPT,
1700
- tools: [
1701
- {
1702
- type: 'provider',
1703
- id: 'google.google_search',
1704
- name: 'google_search',
1705
- args: {},
1706
- },
1707
- ],
1708
- });
1709
-
1710
- expect(await server.calls[0].requestBodyJson).toMatchObject({
1711
- tools: [{ googleSearch: {} }],
1712
- });
1713
- });
1714
-
1715
- it('should use googleSearch for gemini-2.0-flash-exp', async () => {
1716
- prepareJsonResponse({
1717
- url: TEST_URL_GEMINI_2_0_FLASH_EXP,
1718
- });
1719
-
1720
- const gemini2Flash = provider.languageModel('gemini-2.0-flash-exp');
1721
- await gemini2Flash.doGenerate({
1722
- prompt: TEST_PROMPT,
1723
- tools: [
1724
- {
1725
- type: 'provider',
1726
- id: 'google.google_search',
1727
- name: 'google_search',
1728
- args: {},
1729
- },
1730
- ],
1731
- });
1732
-
1733
- expect(await server.calls[0].requestBodyJson).toMatchObject({
1734
- tools: [{ googleSearch: {} }],
1735
- });
1736
- });
1737
-
1738
- it('should use googleSearchRetrieval for non-gemini-2 models', async () => {
1739
- prepareJsonResponse({
1740
- url: TEST_URL_GEMINI_1_0_PRO,
1741
- });
1742
-
1743
- const geminiPro = provider.languageModel('gemini-1.0-pro');
1744
- await geminiPro.doGenerate({
1745
- prompt: TEST_PROMPT,
1746
- tools: [
1747
- {
1748
- type: 'provider',
1749
- id: 'google.google_search',
1750
- name: 'google_search',
1751
- args: {},
1752
- },
1753
- ],
1754
- });
1755
-
1756
- expect(await server.calls[0].requestBodyJson).toMatchObject({
1757
- tools: [{ googleSearchRetrieval: {} }],
1758
- });
1759
- });
1760
-
1761
- it('should use dynamic retrieval for gemini-1-5', async () => {
1762
- prepareJsonResponse({
1763
- url: TEST_URL_GEMINI_1_5_FLASH,
1764
- });
1765
-
1766
- const geminiPro = provider.languageModel('gemini-1.5-flash');
1767
-
1768
- await geminiPro.doGenerate({
1769
- prompt: TEST_PROMPT,
1770
- tools: [
1771
- {
1772
- type: 'provider',
1773
- id: 'google.google_search',
1774
- name: 'google_search',
1775
- args: {
1776
- mode: 'MODE_DYNAMIC',
1777
- dynamicThreshold: 1,
1778
- },
1779
- },
1780
- ],
1781
- });
1782
-
1783
- expect(await server.calls[0].requestBodyJson).toMatchObject({
1784
- tools: [
1785
- {
1786
- googleSearchRetrieval: {
1787
- dynamicRetrievalConfig: {
1788
- mode: 'MODE_DYNAMIC',
1789
- dynamicThreshold: 1,
1790
- },
1791
- },
1792
- },
1793
- ],
1794
- });
1795
- });
1796
- it('should use urlContextTool for gemini-2.0-pro', async () => {
1797
- prepareJsonResponse({
1798
- url: TEST_URL_GEMINI_2_0_PRO,
1799
- });
1800
-
1801
- const gemini2Pro = provider.languageModel('gemini-2.0-pro');
1802
- await gemini2Pro.doGenerate({
1803
- prompt: TEST_PROMPT,
1804
- tools: [
1805
- {
1806
- type: 'provider',
1807
- id: 'google.url_context',
1808
- name: 'url_context',
1809
- args: {},
1810
- },
1811
- ],
1812
- });
1813
-
1814
- expect(await server.calls[0].requestBodyJson).toMatchObject({
1815
- tools: [{ urlContext: {} }],
1816
- });
1817
- });
1818
- it('should use vertexRagStore for gemini-2.0-pro', async () => {
1819
- prepareJsonResponse({
1820
- url: TEST_URL_GEMINI_2_0_PRO,
1821
- });
1822
-
1823
- const gemini2Pro = provider.languageModel('gemini-2.0-pro');
1824
- await gemini2Pro.doGenerate({
1825
- prompt: TEST_PROMPT,
1826
- tools: [
1827
- {
1828
- type: 'provider',
1829
- id: 'google.vertex_rag_store',
1830
- name: 'vertex_rag_store',
1831
- args: {
1832
- ragCorpus:
1833
- 'projects/my-project/locations/us-central1/ragCorpora/my-rag-corpus',
1834
- topK: 5,
1835
- },
1836
- },
1837
- ],
1838
- });
1839
-
1840
- expect(await server.calls[0].requestBodyJson).toMatchObject({
1841
- tools: [
1842
- {
1843
- retrieval: {
1844
- vertex_rag_store: {
1845
- rag_resources: {
1846
- rag_corpus:
1847
- 'projects/my-project/locations/us-central1/ragCorpora/my-rag-corpus',
1848
- },
1849
- similarity_top_k: 5,
1850
- },
1851
- },
1852
- },
1853
- ],
1854
- });
1855
- });
1856
- });
1857
-
1858
- it('should extract image file outputs', async () => {
1859
- server.urls[TEST_URL_GEMINI_PRO].response = {
1860
- type: 'json-value',
1861
- body: {
1862
- candidates: [
1863
- {
1864
- content: {
1865
- parts: [
1866
- { text: 'Here is an image:' },
1867
- {
1868
- inlineData: {
1869
- mimeType: 'image/jpeg',
1870
- data: 'base64encodedimagedata',
1871
- },
1872
- },
1873
- { text: 'And another image:' },
1874
- {
1875
- inlineData: {
1876
- mimeType: 'image/png',
1877
- data: 'anotherbase64encodedimagedata',
1878
- },
1879
- },
1880
- ],
1881
- role: 'model',
1882
- },
1883
- finishReason: 'STOP',
1884
- index: 0,
1885
- safetyRatings: SAFETY_RATINGS,
1886
- },
1887
- ],
1888
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
1889
- usageMetadata: {
1890
- promptTokenCount: 10,
1891
- candidatesTokenCount: 20,
1892
- totalTokenCount: 30,
1893
- },
1894
- },
1895
- };
1896
-
1897
- const { content } = await model.doGenerate({
1898
- prompt: TEST_PROMPT,
1899
- });
1900
-
1901
- expect(content).toMatchInlineSnapshot(`
1902
- [
1903
- {
1904
- "providerMetadata": undefined,
1905
- "text": "Here is an image:",
1906
- "type": "text",
1907
- },
1908
- {
1909
- "data": "base64encodedimagedata",
1910
- "mediaType": "image/jpeg",
1911
- "providerMetadata": undefined,
1912
- "type": "file",
1913
- },
1914
- {
1915
- "providerMetadata": undefined,
1916
- "text": "And another image:",
1917
- "type": "text",
1918
- },
1919
- {
1920
- "data": "anotherbase64encodedimagedata",
1921
- "mediaType": "image/png",
1922
- "providerMetadata": undefined,
1923
- "type": "file",
1924
- },
1925
- ]
1926
- `);
1927
- });
1928
-
1929
- it('should handle responses with only images and no text', async () => {
1930
- server.urls[TEST_URL_GEMINI_PRO].response = {
1931
- type: 'json-value',
1932
- body: {
1933
- candidates: [
1934
- {
1935
- content: {
1936
- parts: [
1937
- {
1938
- inlineData: {
1939
- mimeType: 'image/jpeg',
1940
- data: 'imagedata1',
1941
- },
1942
- },
1943
- {
1944
- inlineData: {
1945
- mimeType: 'image/png',
1946
- data: 'imagedata2',
1947
- },
1948
- },
1949
- ],
1950
- role: 'model',
1951
- },
1952
- finishReason: 'STOP',
1953
- index: 0,
1954
- safetyRatings: SAFETY_RATINGS,
1955
- },
1956
- ],
1957
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
1958
- usageMetadata: {
1959
- promptTokenCount: 10,
1960
- candidatesTokenCount: 20,
1961
- totalTokenCount: 30,
1962
- },
1963
- },
1964
- };
1965
-
1966
- const { content } = await model.doGenerate({
1967
- prompt: TEST_PROMPT,
1968
- });
1969
-
1970
- expect(content).toMatchInlineSnapshot(`
1971
- [
1972
- {
1973
- "data": "imagedata1",
1974
- "mediaType": "image/jpeg",
1975
- "providerMetadata": undefined,
1976
- "type": "file",
1977
- },
1978
- {
1979
- "data": "imagedata2",
1980
- "mediaType": "image/png",
1981
- "providerMetadata": undefined,
1982
- "type": "file",
1983
- },
1984
- ]
1985
- `);
1986
- });
1987
-
1988
- it('should pass responseModalities in provider options', async () => {
1989
- prepareJsonResponse({});
1990
-
1991
- await model.doGenerate({
1992
- prompt: TEST_PROMPT,
1993
- providerOptions: {
1994
- google: {
1995
- responseModalities: ['TEXT', 'IMAGE'],
1996
- },
1997
- },
1998
- });
1999
-
2000
- expect(await server.calls[0].requestBodyJson).toMatchObject({
2001
- generationConfig: {
2002
- responseModalities: ['TEXT', 'IMAGE'],
2003
- },
2004
- });
2005
- });
2006
-
2007
- it('should pass mediaResolution in provider options', async () => {
2008
- prepareJsonResponse({});
2009
-
2010
- await model.doGenerate({
2011
- prompt: TEST_PROMPT,
2012
- providerOptions: {
2013
- google: {
2014
- mediaResolution: 'MEDIA_RESOLUTION_LOW',
2015
- },
2016
- },
2017
- });
2018
-
2019
- expect(await server.calls[0].requestBodyJson).toMatchObject({
2020
- generationConfig: {
2021
- mediaResolution: 'MEDIA_RESOLUTION_LOW',
2022
- },
2023
- });
2024
- });
2025
-
2026
- it('should pass imageConfig.aspectRatio in provider options', async () => {
2027
- prepareJsonResponse({});
2028
-
2029
- await model.doGenerate({
2030
- prompt: TEST_PROMPT,
2031
- providerOptions: {
2032
- google: {
2033
- imageConfig: {
2034
- aspectRatio: '16:9',
2035
- },
2036
- },
2037
- },
2038
- });
2039
-
2040
- expect(await server.calls[0].requestBodyJson).toMatchObject({
2041
- generationConfig: {
2042
- imageConfig: {
2043
- aspectRatio: '16:9',
2044
- },
2045
- },
2046
- });
2047
- });
2048
-
2049
- it('should pass retrievalConfig in provider options', async () => {
2050
- prepareJsonResponse({ url: TEST_URL_GEMINI_2_0_FLASH_EXP });
2051
-
2052
- const gemini2Model = provider.chat('gemini-2.0-flash-exp');
2053
-
2054
- await gemini2Model.doGenerate({
2055
- prompt: TEST_PROMPT,
2056
- tools: [
2057
- {
2058
- type: 'provider',
2059
- id: 'google.google_maps',
2060
- name: 'google_maps',
2061
- args: {},
2062
- },
2063
- ],
2064
- providerOptions: {
2065
- google: {
2066
- retrievalConfig: {
2067
- latLng: {
2068
- latitude: 34.090199,
2069
- longitude: -117.881081,
2070
- },
2071
- },
2072
- },
2073
- },
2074
- });
2075
-
2076
- expect(await server.calls[0].requestBodyJson).toMatchObject({
2077
- tools: [{ googleMaps: {} }],
2078
- toolConfig: {
2079
- retrievalConfig: {
2080
- latLng: {
2081
- latitude: 34.090199,
2082
- longitude: -117.881081,
2083
- },
2084
- },
2085
- },
2086
- });
2087
- });
2088
-
2089
- it('should include non-image inlineData parts', async () => {
2090
- server.urls[TEST_URL_GEMINI_PRO].response = {
2091
- type: 'json-value',
2092
- body: {
2093
- candidates: [
2094
- {
2095
- content: {
2096
- parts: [
2097
- { text: 'Here is content:' },
2098
- {
2099
- inlineData: {
2100
- mimeType: 'image/jpeg',
2101
- data: 'validimagedata',
2102
- },
2103
- },
2104
- {
2105
- inlineData: {
2106
- mimeType: 'application/pdf',
2107
- data: 'pdfdata',
2108
- },
2109
- },
2110
- ],
2111
- role: 'model',
2112
- },
2113
- finishReason: 'STOP',
2114
- index: 0,
2115
- safetyRatings: SAFETY_RATINGS,
2116
- },
2117
- ],
2118
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
2119
- },
2120
- };
2121
-
2122
- const { content } = await model.doGenerate({
2123
- prompt: TEST_PROMPT,
2124
- });
2125
-
2126
- expect(content).toMatchInlineSnapshot(`
2127
- [
2128
- {
2129
- "providerMetadata": undefined,
2130
- "text": "Here is content:",
2131
- "type": "text",
2132
- },
2133
- {
2134
- "data": "validimagedata",
2135
- "mediaType": "image/jpeg",
2136
- "providerMetadata": undefined,
2137
- "type": "file",
2138
- },
2139
- {
2140
- "data": "pdfdata",
2141
- "mediaType": "application/pdf",
2142
- "providerMetadata": undefined,
2143
- "type": "file",
2144
- },
2145
- ]
2146
- `);
2147
- });
2148
- it('should correctly parse and separate reasoning parts from text output', async () => {
2149
- server.urls[TEST_URL_GEMINI_PRO].response = {
2150
- type: 'json-value',
2151
- body: {
2152
- candidates: [
2153
- {
2154
- content: {
2155
- parts: [
2156
- { text: 'Visible text part 1. ' },
2157
- { text: 'This is a thought process.', thought: true },
2158
- { text: 'Visible text part 2.' },
2159
- { text: 'Another internal thought.', thought: true },
2160
- ],
2161
- role: 'model',
2162
- },
2163
- finishReason: 'STOP',
2164
- index: 0,
2165
- safetyRatings: SAFETY_RATINGS,
2166
- },
2167
- ],
2168
- usageMetadata: {
2169
- promptTokenCount: 10,
2170
- candidatesTokenCount: 20,
2171
- totalTokenCount: 30,
2172
- },
2173
- },
2174
- };
2175
-
2176
- const { content } = await model.doGenerate({
2177
- prompt: TEST_PROMPT,
2178
- });
2179
-
2180
- expect(content).toMatchInlineSnapshot(`
2181
- [
2182
- {
2183
- "providerMetadata": undefined,
2184
- "text": "Visible text part 1. ",
2185
- "type": "text",
2186
- },
2187
- {
2188
- "providerMetadata": undefined,
2189
- "text": "This is a thought process.",
2190
- "type": "reasoning",
2191
- },
2192
- {
2193
- "providerMetadata": undefined,
2194
- "text": "Visible text part 2.",
2195
- "type": "text",
2196
- },
2197
- {
2198
- "providerMetadata": undefined,
2199
- "text": "Another internal thought.",
2200
- "type": "reasoning",
2201
- },
2202
- ]
2203
- `);
2204
- });
2205
-
2206
- it('should correctly parse thought signatures with reasoning parts', async () => {
2207
- server.urls[TEST_URL_GEMINI_PRO].response = {
2208
- type: 'json-value',
2209
- body: {
2210
- candidates: [
2211
- {
2212
- content: {
2213
- parts: [
2214
- { text: 'Visible text part 1. ', thoughtSignature: 'sig1' },
2215
- {
2216
- text: 'This is a thought process.',
2217
- thought: true,
2218
- thoughtSignature: 'sig2',
2219
- },
2220
- { text: 'Visible text part 2.', thoughtSignature: 'sig3' },
2221
- ],
2222
- role: 'model',
2223
- },
2224
- finishReason: 'STOP',
2225
- index: 0,
2226
- safetyRatings: SAFETY_RATINGS,
2227
- },
2228
- ],
2229
- usageMetadata: {
2230
- promptTokenCount: 10,
2231
- candidatesTokenCount: 20,
2232
- totalTokenCount: 30,
2233
- },
2234
- },
2235
- };
2236
-
2237
- const { content } = await model.doGenerate({
2238
- prompt: TEST_PROMPT,
2239
- });
2240
-
2241
- expect(content).toMatchInlineSnapshot(`
2242
- [
2243
- {
2244
- "providerMetadata": {
2245
- "google": {
2246
- "thoughtSignature": "sig1",
2247
- },
2248
- },
2249
- "text": "Visible text part 1. ",
2250
- "type": "text",
2251
- },
2252
- {
2253
- "providerMetadata": {
2254
- "google": {
2255
- "thoughtSignature": "sig2",
2256
- },
2257
- },
2258
- "text": "This is a thought process.",
2259
- "type": "reasoning",
2260
- },
2261
- {
2262
- "providerMetadata": {
2263
- "google": {
2264
- "thoughtSignature": "sig3",
2265
- },
2266
- },
2267
- "text": "Visible text part 2.",
2268
- "type": "text",
2269
- },
2270
- ]
2271
- `);
2272
- });
2273
-
2274
- it('should correctly parse thought signatures with function calls', async () => {
2275
- server.urls[TEST_URL_GEMINI_PRO].response = {
2276
- type: 'json-value',
2277
- body: {
2278
- candidates: [
2279
- {
2280
- content: {
2281
- parts: [
2282
- {
2283
- functionCall: {
2284
- name: 'test-tool',
2285
- args: { value: 'test' },
2286
- },
2287
- thoughtSignature: 'func_sig1',
2288
- },
2289
- ],
2290
- role: 'model',
2291
- },
2292
- finishReason: 'STOP',
2293
- index: 0,
2294
- safetyRatings: SAFETY_RATINGS,
2295
- },
2296
- ],
2297
- usageMetadata: {
2298
- promptTokenCount: 10,
2299
- candidatesTokenCount: 20,
2300
- totalTokenCount: 30,
2301
- },
2302
- },
2303
- };
2304
-
2305
- const { content } = await model.doGenerate({
2306
- prompt: TEST_PROMPT,
2307
- });
2308
-
2309
- expect(content).toMatchInlineSnapshot(`
2310
- [
2311
- {
2312
- "input": "{"value":"test"}",
2313
- "providerMetadata": {
2314
- "google": {
2315
- "thoughtSignature": "func_sig1",
2316
- },
2317
- },
2318
- "toolCallId": "test-id",
2319
- "toolName": "test-tool",
2320
- "type": "tool-call",
2321
- },
2322
- ]
2323
- `);
2324
- });
2325
-
2326
- it('should support includeThoughts with google generative ai provider', async () => {
2327
- server.urls[TEST_URL_GEMINI_PRO].response = {
2328
- type: 'json-value',
2329
- body: {
2330
- candidates: [
2331
- {
2332
- content: {
2333
- parts: [
2334
- {
2335
- text: 'let me think about this problem',
2336
- thought: true,
2337
- thoughtSignature: 'reasoning_sig',
2338
- },
2339
- { text: 'the answer is 42' },
2340
- ],
2341
- role: 'model',
2342
- },
2343
- finishReason: 'STOP',
2344
- safetyRatings: SAFETY_RATINGS,
2345
- },
2346
- ],
2347
- usageMetadata: {
2348
- promptTokenCount: 10,
2349
- candidatesTokenCount: 15,
2350
- totalTokenCount: 25,
2351
- thoughtsTokenCount: 8,
2352
- },
2353
- },
2354
- };
2355
-
2356
- const { content, usage } = await model.doGenerate({
2357
- prompt: TEST_PROMPT,
2358
- providerOptions: {
2359
- google: {
2360
- thinkingConfig: {
2361
- includeThoughts: true,
2362
- thinkingBudget: 1024,
2363
- },
2364
- },
2365
- },
2366
- });
2367
-
2368
- expect(content).toMatchInlineSnapshot(`
2369
- [
2370
- {
2371
- "providerMetadata": {
2372
- "google": {
2373
- "thoughtSignature": "reasoning_sig",
2374
- },
2375
- },
2376
- "text": "let me think about this problem",
2377
- "type": "reasoning",
2378
- },
2379
- {
2380
- "providerMetadata": undefined,
2381
- "text": "the answer is 42",
2382
- "type": "text",
2383
- },
2384
- ]
2385
- `);
2386
-
2387
- expect(usage).toMatchInlineSnapshot(`
2388
- {
2389
- "inputTokens": {
2390
- "cacheRead": 0,
2391
- "cacheWrite": undefined,
2392
- "noCache": 10,
2393
- "total": 10,
2394
- },
2395
- "outputTokens": {
2396
- "reasoning": 8,
2397
- "text": 15,
2398
- "total": 23,
2399
- },
2400
- "raw": {
2401
- "candidatesTokenCount": 15,
2402
- "promptTokenCount": 10,
2403
- "thoughtsTokenCount": 8,
2404
- "totalTokenCount": 25,
2405
- },
2406
- }
2407
- `);
2408
- });
2409
-
2410
- it('should pass thinkingLevel in provider options', async () => {
2411
- prepareJsonResponse({});
2412
-
2413
- await model.doGenerate({
2414
- prompt: TEST_PROMPT,
2415
- providerOptions: {
2416
- google: {
2417
- thinkingConfig: {
2418
- thinkingLevel: 'high',
2419
- },
2420
- },
2421
- },
2422
- });
2423
-
2424
- expect(await server.calls[0].requestBodyJson).toMatchObject({
2425
- generationConfig: {
2426
- thinkingConfig: {
2427
- thinkingLevel: 'high',
2428
- },
2429
- },
2430
- });
2431
- });
2432
-
2433
- it('should pass thinkingLevel "minimal" in provider options', async () => {
2434
- prepareJsonResponse({});
2435
-
2436
- await model.doGenerate({
2437
- prompt: TEST_PROMPT,
2438
- providerOptions: {
2439
- google: {
2440
- thinkingConfig: {
2441
- thinkingLevel: 'minimal',
2442
- },
2443
- },
2444
- },
2445
- });
2446
-
2447
- expect(await server.calls[0].requestBodyJson).toMatchObject({
2448
- generationConfig: {
2449
- thinkingConfig: {
2450
- thinkingLevel: 'minimal',
2451
- },
2452
- },
2453
- });
2454
- });
2455
-
2456
- it('should pass thinkingLevel "medium" in provider options', async () => {
2457
- prepareJsonResponse({});
2458
-
2459
- await model.doGenerate({
2460
- prompt: TEST_PROMPT,
2461
- providerOptions: {
2462
- google: {
2463
- thinkingConfig: {
2464
- thinkingLevel: 'medium',
2465
- },
2466
- },
2467
- },
2468
- });
2469
-
2470
- expect(await server.calls[0].requestBodyJson).toMatchObject({
2471
- generationConfig: {
2472
- thinkingConfig: {
2473
- thinkingLevel: 'medium',
2474
- },
2475
- },
2476
- });
2477
- });
2478
-
2479
- describe('providerMetadata key based on provider string', () => {
2480
- it('should use "vertex" as providerMetadata key when provider includes "vertex"', async () => {
2481
- server.urls[TEST_URL_GEMINI_PRO].response = {
2482
- type: 'json-value',
2483
- body: {
2484
- candidates: [
2485
- {
2486
- content: { parts: [{ text: 'Hello!' }], role: 'model' },
2487
- finishReason: 'STOP',
2488
- safetyRatings: SAFETY_RATINGS,
2489
- groundingMetadata: {
2490
- webSearchQueries: ['test query'],
2491
- },
2492
- },
2493
- ],
2494
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
2495
- usageMetadata: {
2496
- promptTokenCount: 1,
2497
- candidatesTokenCount: 2,
2498
- totalTokenCount: 3,
2499
- },
2500
- },
2501
- };
2502
-
2503
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
2504
- provider: 'google.vertex.chat',
2505
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
2506
- headers: { 'x-goog-api-key': 'test-api-key' },
2507
- generateId: () => 'test-id',
2508
- });
2509
-
2510
- const { providerMetadata } = await model.doGenerate({
2511
- prompt: TEST_PROMPT,
2512
- });
2513
-
2514
- expect(providerMetadata).toHaveProperty('vertex');
2515
- expect(providerMetadata).not.toHaveProperty('google');
2516
- expect(providerMetadata?.vertex).toMatchObject({
2517
- promptFeedback: expect.any(Object),
2518
- groundingMetadata: expect.any(Object),
2519
- safetyRatings: expect.any(Array),
2520
- });
2521
- });
2522
-
2523
- it('should use "google" as providerMetadata key when provider does not include "vertex"', async () => {
2524
- server.urls[TEST_URL_GEMINI_PRO].response = {
2525
- type: 'json-value',
2526
- body: {
2527
- candidates: [
2528
- {
2529
- content: { parts: [{ text: 'Hello!' }], role: 'model' },
2530
- finishReason: 'STOP',
2531
- safetyRatings: SAFETY_RATINGS,
2532
- groundingMetadata: {
2533
- webSearchQueries: ['test query'],
2534
- },
2535
- },
2536
- ],
2537
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
2538
- usageMetadata: {
2539
- promptTokenCount: 1,
2540
- candidatesTokenCount: 2,
2541
- totalTokenCount: 3,
2542
- },
2543
- },
2544
- };
2545
-
2546
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
2547
- provider: 'google.generative-ai',
2548
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
2549
- headers: { 'x-goog-api-key': 'test-api-key' },
2550
- generateId: () => 'test-id',
2551
- //should default to 'google'
2552
- });
2553
-
2554
- const { providerMetadata } = await model.doGenerate({
2555
- prompt: TEST_PROMPT,
2556
- });
2557
-
2558
- expect(providerMetadata).toHaveProperty('google');
2559
- expect(providerMetadata).not.toHaveProperty('vertex');
2560
- expect(providerMetadata?.google).toMatchObject({
2561
- promptFeedback: expect.any(Object),
2562
- groundingMetadata: expect.any(Object),
2563
- safetyRatings: expect.any(Array),
2564
- });
2565
- });
2566
-
2567
- it('should use "vertex" as providerMetadata key in thoughtSignature content when provider includes "vertex"', async () => {
2568
- server.urls[TEST_URL_GEMINI_PRO].response = {
2569
- type: 'json-value',
2570
- body: {
2571
- candidates: [
2572
- {
2573
- content: {
2574
- parts: [
2575
- {
2576
- text: 'thinking...',
2577
- thought: true,
2578
- thoughtSignature: 'sig123',
2579
- },
2580
- { text: 'Final answer' },
2581
- ],
2582
- role: 'model',
2583
- },
2584
- finishReason: 'STOP',
2585
- safetyRatings: SAFETY_RATINGS,
2586
- },
2587
- ],
2588
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
2589
- usageMetadata: {
2590
- promptTokenCount: 1,
2591
- candidatesTokenCount: 2,
2592
- totalTokenCount: 3,
2593
- },
2594
- },
2595
- };
2596
-
2597
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
2598
- provider: 'google.vertex.chat',
2599
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
2600
- headers: { 'x-goog-api-key': 'test-api-key' },
2601
- generateId: () => 'test-id',
2602
- });
2603
-
2604
- const { content } = await model.doGenerate({
2605
- prompt: TEST_PROMPT,
2606
- });
2607
-
2608
- const reasoningPart = content.find(part => part.type === 'reasoning');
2609
- expect(reasoningPart?.providerMetadata).toHaveProperty('vertex');
2610
- expect(reasoningPart?.providerMetadata).not.toHaveProperty('google');
2611
- expect(reasoningPart?.providerMetadata?.vertex).toMatchObject({
2612
- thoughtSignature: 'sig123',
2613
- });
2614
- });
2615
-
2616
- it('should use "vertex" as providerMetadata key in tool call content when provider includes "vertex"', async () => {
2617
- server.urls[TEST_URL_GEMINI_PRO].response = {
2618
- type: 'json-value',
2619
- body: {
2620
- candidates: [
2621
- {
2622
- content: {
2623
- parts: [
2624
- {
2625
- functionCall: {
2626
- name: 'test-tool',
2627
- args: { value: 'test' },
2628
- },
2629
- thoughtSignature: 'tool_sig',
2630
- },
2631
- ],
2632
- role: 'model',
2633
- },
2634
- finishReason: 'STOP',
2635
- safetyRatings: SAFETY_RATINGS,
2636
- },
2637
- ],
2638
- promptFeedback: { safetyRatings: SAFETY_RATINGS },
2639
- usageMetadata: {
2640
- promptTokenCount: 1,
2641
- candidatesTokenCount: 2,
2642
- totalTokenCount: 3,
2643
- },
2644
- },
2645
- };
2646
-
2647
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
2648
- provider: 'google.vertex.chat',
2649
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
2650
- headers: { 'x-goog-api-key': 'test-api-key' },
2651
- generateId: () => 'test-id',
2652
- });
2653
-
2654
- const { content } = await model.doGenerate({
2655
- prompt: TEST_PROMPT,
2656
- });
2657
-
2658
- const toolCallPart = content.find(part => part.type === 'tool-call');
2659
- expect(toolCallPart?.providerMetadata).toHaveProperty('vertex');
2660
- expect(toolCallPart?.providerMetadata).not.toHaveProperty('google');
2661
- expect(toolCallPart?.providerMetadata?.vertex).toMatchObject({
2662
- thoughtSignature: 'tool_sig',
2663
- });
2664
- });
2665
- });
2666
- });
2667
-
2668
- describe('doStream', () => {
2669
- const TEST_URL_GEMINI_PRO =
2670
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:streamGenerateContent';
2671
-
2672
- const TEST_URL_GEMINI_2_0_PRO =
2673
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-pro:streamGenerateContent';
2674
-
2675
- const TEST_URL_GEMINI_2_0_FLASH_EXP =
2676
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:streamGenerateContent';
2677
-
2678
- const TEST_URL_GEMINI_1_0_PRO =
2679
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.0-pro:streamGenerateContent';
2680
-
2681
- const TEST_URL_GEMINI_1_5_FLASH =
2682
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:streamGenerateContent';
2683
-
2684
- const server = createTestServer({
2685
- [TEST_URL_GEMINI_PRO]: {},
2686
- [TEST_URL_GEMINI_2_0_PRO]: {},
2687
- [TEST_URL_GEMINI_2_0_FLASH_EXP]: {},
2688
- [TEST_URL_GEMINI_1_0_PRO]: {},
2689
- [TEST_URL_GEMINI_1_5_FLASH]: {},
2690
- });
2691
-
2692
- const prepareStreamResponse = ({
2693
- content,
2694
- headers,
2695
- groundingMetadata,
2696
- urlContextMetadata,
2697
- url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:streamGenerateContent',
2698
- }: {
2699
- content: string[];
2700
- headers?: Record<string, string>;
2701
- groundingMetadata?: GoogleGenerativeAIGroundingMetadata;
2702
- urlContextMetadata?: GoogleGenerativeAIUrlContextMetadata;
2703
- url?:
2704
- | typeof TEST_URL_GEMINI_PRO
2705
- | typeof TEST_URL_GEMINI_2_0_PRO
2706
- | typeof TEST_URL_GEMINI_2_0_FLASH_EXP
2707
- | typeof TEST_URL_GEMINI_1_0_PRO
2708
- | typeof TEST_URL_GEMINI_1_5_FLASH;
2709
- }) => {
2710
- server.urls[url].response = {
2711
- headers,
2712
- type: 'stream-chunks',
2713
- chunks: content.map(
2714
- (text, index) =>
2715
- `data: ${JSON.stringify({
2716
- candidates: [
2717
- {
2718
- content: { parts: [{ text }], role: 'model' },
2719
- finishReason: 'STOP',
2720
- index: 0,
2721
- safetyRatings: SAFETY_RATINGS,
2722
- ...(groundingMetadata && { groundingMetadata }),
2723
- ...(urlContextMetadata && { urlContextMetadata }),
2724
- },
2725
- ],
2726
- // Include usage metadata only in the last chunk
2727
- ...(index === content.length - 1 && {
2728
- usageMetadata: {
2729
- promptTokenCount: 294,
2730
- candidatesTokenCount: 233,
2731
- totalTokenCount: 527,
2732
- },
2733
- }),
2734
- })}\n\n`,
2735
- ),
2736
- };
2737
- };
2738
-
2739
- it('should expose grounding metadata in provider metadata on finish', async () => {
2740
- prepareStreamResponse({
2741
- content: ['test'],
2742
- groundingMetadata: {
2743
- webSearchQueries: ["What's the weather in Chicago this weekend?"],
2744
- searchEntryPoint: {
2745
- renderedContent: 'Sample rendered content for search results',
2746
- },
2747
- groundingChunks: [
2748
- {
2749
- web: {
2750
- uri: 'https://example.com/weather',
2751
- title: 'Chicago Weather Forecast',
2752
- },
2753
- },
2754
- ],
2755
- groundingSupports: [
2756
- {
2757
- segment: {
2758
- startIndex: 0,
2759
- endIndex: 65,
2760
- text: 'Chicago weather changes rapidly, so layers let you adjust easily.',
2761
- },
2762
- groundingChunkIndices: [0],
2763
- confidenceScores: [0.99],
2764
- },
2765
- ],
2766
- retrievalMetadata: {
2767
- webDynamicRetrievalScore: 0.96879,
2768
- },
2769
- },
2770
- });
2771
-
2772
- const { stream } = await model.doStream({
2773
- prompt: TEST_PROMPT,
2774
- includeRawChunks: false,
2775
- });
2776
-
2777
- const events = await convertReadableStreamToArray(stream);
2778
- const finishEvent = events.find(event => event.type === 'finish');
2779
-
2780
- expect(
2781
- finishEvent?.type === 'finish' &&
2782
- finishEvent.providerMetadata?.google.groundingMetadata,
2783
- ).toStrictEqual({
2784
- webSearchQueries: ["What's the weather in Chicago this weekend?"],
2785
- searchEntryPoint: {
2786
- renderedContent: 'Sample rendered content for search results',
2787
- },
2788
- groundingChunks: [
2789
- {
2790
- web: {
2791
- uri: 'https://example.com/weather',
2792
- title: 'Chicago Weather Forecast',
2793
- },
2794
- },
2795
- ],
2796
- groundingSupports: [
2797
- {
2798
- segment: {
2799
- startIndex: 0,
2800
- endIndex: 65,
2801
- text: 'Chicago weather changes rapidly, so layers let you adjust easily.',
2802
- },
2803
- groundingChunkIndices: [0],
2804
- confidenceScores: [0.99],
2805
- },
2806
- ],
2807
- retrievalMetadata: {
2808
- webDynamicRetrievalScore: 0.96879,
2809
- },
2810
- });
2811
- });
2812
-
2813
- it('should expose url context metadata in provider metadata on finish', async () => {
2814
- prepareStreamResponse({
2815
- content: ['test'],
2816
- urlContextMetadata: {
2817
- urlMetadata: [
2818
- {
2819
- retrievedUrl: 'https://example.com/weather',
2820
- urlRetrievalStatus: 'URL_RETRIEVAL_STATUS_SUCCESS',
2821
- },
2822
- ],
2823
- },
2824
- });
2825
-
2826
- const { stream } = await model.doStream({
2827
- prompt: TEST_PROMPT,
2828
- includeRawChunks: false,
2829
- });
2830
-
2831
- const events = await convertReadableStreamToArray(stream);
2832
- const finishEvent = events.find(event => event.type === 'finish');
2833
-
2834
- expect(
2835
- finishEvent?.type === 'finish' &&
2836
- finishEvent.providerMetadata?.google.urlContextMetadata,
2837
- ).toStrictEqual({
2838
- urlMetadata: [
2839
- {
2840
- retrievedUrl: 'https://example.com/weather',
2841
- urlRetrievalStatus: 'URL_RETRIEVAL_STATUS_SUCCESS',
2842
- },
2843
- ],
2844
- });
2845
- });
2846
-
2847
- it('should stream text deltas', async () => {
2848
- prepareStreamResponse({ content: ['Hello', ', ', 'world!'] });
2849
-
2850
- const { stream } = await model.doStream({
2851
- prompt: TEST_PROMPT,
2852
- includeRawChunks: false,
2853
- });
2854
-
2855
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
2856
- [
2857
- {
2858
- "type": "stream-start",
2859
- "warnings": [],
2860
- },
2861
- {
2862
- "id": "0",
2863
- "providerMetadata": undefined,
2864
- "type": "text-start",
2865
- },
2866
- {
2867
- "delta": "Hello",
2868
- "id": "0",
2869
- "providerMetadata": undefined,
2870
- "type": "text-delta",
2871
- },
2872
- {
2873
- "delta": ", ",
2874
- "id": "0",
2875
- "providerMetadata": undefined,
2876
- "type": "text-delta",
2877
- },
2878
- {
2879
- "delta": "world!",
2880
- "id": "0",
2881
- "providerMetadata": undefined,
2882
- "type": "text-delta",
2883
- },
2884
- {
2885
- "id": "0",
2886
- "type": "text-end",
2887
- },
2888
- {
2889
- "finishReason": {
2890
- "raw": "STOP",
2891
- "unified": "stop",
2892
- },
2893
- "providerMetadata": {
2894
- "google": {
2895
- "groundingMetadata": null,
2896
- "promptFeedback": null,
2897
- "safetyRatings": [
2898
- {
2899
- "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
2900
- "probability": "NEGLIGIBLE",
2901
- },
2902
- {
2903
- "category": "HARM_CATEGORY_HATE_SPEECH",
2904
- "probability": "NEGLIGIBLE",
2905
- },
2906
- {
2907
- "category": "HARM_CATEGORY_HARASSMENT",
2908
- "probability": "NEGLIGIBLE",
2909
- },
2910
- {
2911
- "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
2912
- "probability": "NEGLIGIBLE",
2913
- },
2914
- ],
2915
- "urlContextMetadata": null,
2916
- "usageMetadata": {
2917
- "candidatesTokenCount": 233,
2918
- "promptTokenCount": 294,
2919
- "totalTokenCount": 527,
2920
- },
2921
- },
2922
- },
2923
- "type": "finish",
2924
- "usage": {
2925
- "inputTokens": {
2926
- "cacheRead": 0,
2927
- "cacheWrite": undefined,
2928
- "noCache": 294,
2929
- "total": 294,
2930
- },
2931
- "outputTokens": {
2932
- "reasoning": 0,
2933
- "text": 233,
2934
- "total": 233,
2935
- },
2936
- "raw": {
2937
- "candidatesTokenCount": 233,
2938
- "promptTokenCount": 294,
2939
- "totalTokenCount": 527,
2940
- },
2941
- },
2942
- },
2943
- ]
2944
- `);
2945
- });
2946
-
2947
- it('should expose safety ratings in provider metadata on finish', async () => {
2948
- server.urls[TEST_URL_GEMINI_PRO].response = {
2949
- type: 'stream-chunks',
2950
- chunks: [
2951
- `data: {"candidates": [{"content": {"parts": [{"text": "test"}],"role": "model"},` +
2952
- `"finishReason": "STOP","index": 0,"safetyRatings": [` +
2953
- `{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE",` +
2954
- `"probabilityScore": 0.1,"severity": "LOW","severityScore": 0.2,"blocked": false}]}]}\n\n`,
2955
- ],
2956
- };
2957
-
2958
- const { stream } = await model.doStream({
2959
- prompt: TEST_PROMPT,
2960
- includeRawChunks: false,
2961
- });
2962
-
2963
- const events = await convertReadableStreamToArray(stream);
2964
- const finishEvent = events.find(event => event.type === 'finish');
2965
-
2966
- expect(
2967
- finishEvent?.type === 'finish' &&
2968
- finishEvent.providerMetadata?.google.safetyRatings,
2969
- ).toStrictEqual([
2970
- {
2971
- category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
2972
- probability: 'NEGLIGIBLE',
2973
- probabilityScore: 0.1,
2974
- severity: 'LOW',
2975
- severityScore: 0.2,
2976
- blocked: false,
2977
- },
2978
- ]);
2979
- });
2980
-
2981
- it('should expose PromptFeedback in provider metadata on finish', async () => {
2982
- server.urls[TEST_URL_GEMINI_PRO].response = {
2983
- type: 'stream-chunks',
2984
- chunks: [
2985
- `data: {"candidates": [{"content": {"parts": [{"text": "No"}],"role": "model"},` +
2986
- `"finishReason": "PROHIBITED_CONTENT","index": 0}],` +
2987
- `"promptFeedback": {"blockReason": "PROHIBITED_CONTENT","safetyRatings": [` +
2988
- `{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},` +
2989
- `{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},` +
2990
- `{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},` +
2991
- `{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}}\n\n`,
2992
- ],
2993
- };
2994
-
2995
- const { stream } = await model.doStream({
2996
- prompt: TEST_PROMPT,
2997
- });
2998
-
2999
- const events = await convertReadableStreamToArray(stream);
3000
- const finishEvent = events.find(event => event.type === 'finish');
3001
-
3002
- expect(
3003
- finishEvent?.type === 'finish' &&
3004
- finishEvent.providerMetadata?.google.promptFeedback,
3005
- ).toStrictEqual({
3006
- blockReason: 'PROHIBITED_CONTENT',
3007
- safetyRatings: SAFETY_RATINGS,
3008
- });
3009
- });
3010
-
3011
- it('should stream code execution tool calls and results', async () => {
3012
- server.urls[TEST_URL_GEMINI_2_0_PRO].response = {
3013
- type: 'stream-chunks',
3014
- chunks: [
3015
- `data: ${JSON.stringify({
3016
- candidates: [
3017
- {
3018
- content: {
3019
- parts: [
3020
- {
3021
- executableCode: {
3022
- language: 'PYTHON',
3023
- code: 'print("hello")',
3024
- },
3025
- },
3026
- ],
3027
- },
3028
- },
3029
- ],
3030
- })}\n\n`,
3031
- `data: ${JSON.stringify({
3032
- candidates: [
3033
- {
3034
- content: {
3035
- parts: [
3036
- {
3037
- codeExecutionResult: {
3038
- outcome: 'OUTCOME_OK',
3039
- output: 'hello\n',
3040
- },
3041
- },
3042
- ],
3043
- },
3044
- finishReason: 'STOP',
3045
- },
3046
- ],
3047
- })}\n\n`,
3048
- ],
3049
- };
3050
-
3051
- const model = provider.languageModel('gemini-2.0-pro');
3052
- const { stream } = await model.doStream({
3053
- tools: [
3054
- {
3055
- type: 'provider',
3056
- id: 'google.code_execution',
3057
- name: 'code_execution',
3058
- args: {},
3059
- },
3060
- ],
3061
- prompt: TEST_PROMPT,
3062
- });
3063
-
3064
- const events = await convertReadableStreamToArray(stream);
3065
-
3066
- const toolEvents = events.filter(
3067
- e => e.type === 'tool-call' || e.type === 'tool-result',
3068
- );
3069
-
3070
- expect(toolEvents).toMatchInlineSnapshot(`
3071
- [
3072
- {
3073
- "input": "{"language":"PYTHON","code":"print(\\"hello\\")"}",
3074
- "providerExecuted": true,
3075
- "toolCallId": "test-id",
3076
- "toolName": "code_execution",
3077
- "type": "tool-call",
3078
- },
3079
- {
3080
- "result": {
3081
- "outcome": "OUTCOME_OK",
3082
- "output": "hello
3083
- ",
3084
- },
3085
- "toolCallId": "test-id",
3086
- "toolName": "code_execution",
3087
- "type": "tool-result",
3088
- },
3089
- ]
3090
- `);
3091
- });
3092
-
3093
- it('should return stop finish reason for streamed code execution (provider-executed tool)', async () => {
3094
- server.urls[TEST_URL_GEMINI_2_0_PRO].response = {
3095
- type: 'stream-chunks',
3096
- chunks: [
3097
- `data: ${JSON.stringify({
3098
- candidates: [
3099
- {
3100
- content: {
3101
- parts: [
3102
- {
3103
- executableCode: {
3104
- language: 'PYTHON',
3105
- code: 'print("hello")',
3106
- },
3107
- },
3108
- ],
3109
- },
3110
- },
3111
- ],
3112
- })}\n\n`,
3113
- `data: ${JSON.stringify({
3114
- candidates: [
3115
- {
3116
- content: {
3117
- parts: [
3118
- {
3119
- codeExecutionResult: {
3120
- outcome: 'OUTCOME_OK',
3121
- output: 'hello\n',
3122
- },
3123
- },
3124
- {
3125
- text: '{"result": "hello"}',
3126
- },
3127
- ],
3128
- },
3129
- finishReason: 'STOP',
3130
- },
3131
- ],
3132
- })}\n\n`,
3133
- ],
3134
- };
3135
-
3136
- const model = provider.languageModel('gemini-2.0-pro');
3137
- const { stream } = await model.doStream({
3138
- tools: [
3139
- {
3140
- type: 'provider',
3141
- id: 'google.code_execution',
3142
- name: 'code_execution',
3143
- args: {},
3144
- },
3145
- ],
3146
- prompt: TEST_PROMPT,
3147
- });
3148
-
3149
- const events = await convertReadableStreamToArray(stream);
3150
-
3151
- const finishEvent = events.find(e => e.type === 'finish');
3152
-
3153
- // Provider-executed tools should not trigger 'tool-calls' finish reason
3154
- // since they don't require SDK iteration - allows structured output to work
3155
- expect(finishEvent).toMatchObject({
3156
- type: 'finish',
3157
- finishReason: {
3158
- raw: 'STOP',
3159
- unified: 'stop',
3160
- },
3161
- });
3162
- });
3163
-
3164
- describe('search tool selection', () => {
3165
- const provider = createGoogleGenerativeAI({
3166
- apiKey: 'test-api-key',
3167
- generateId: () => 'test-id',
3168
- });
3169
-
3170
- it('should use googleSearch for gemini-2.0-pro', async () => {
3171
- prepareStreamResponse({
3172
- content: [''],
3173
- url: TEST_URL_GEMINI_2_0_PRO,
3174
- });
3175
-
3176
- const gemini2Pro = provider.languageModel('gemini-2.0-pro');
3177
- await gemini2Pro.doStream({
3178
- prompt: TEST_PROMPT,
3179
- includeRawChunks: false,
3180
- tools: [
3181
- {
3182
- type: 'provider',
3183
- id: 'google.google_search',
3184
- name: 'google_search',
3185
- args: {},
3186
- },
3187
- ],
3188
- });
3189
-
3190
- expect(await server.calls[0].requestBodyJson).toMatchObject({
3191
- tools: [{ googleSearch: {} }],
3192
- });
3193
- });
3194
-
3195
- it('should use googleSearch for gemini-2.0-flash-exp', async () => {
3196
- prepareStreamResponse({
3197
- content: [''],
3198
- url: TEST_URL_GEMINI_2_0_FLASH_EXP,
3199
- });
3200
-
3201
- const gemini2Flash = provider.languageModel('gemini-2.0-flash-exp');
3202
-
3203
- await gemini2Flash.doStream({
3204
- prompt: TEST_PROMPT,
3205
- includeRawChunks: false,
3206
- tools: [
3207
- {
3208
- type: 'provider',
3209
- id: 'google.google_search',
3210
- name: 'google_search',
3211
- args: {},
3212
- },
3213
- ],
3214
- });
3215
-
3216
- expect(await server.calls[0].requestBodyJson).toMatchObject({
3217
- tools: [{ googleSearch: {} }],
3218
- });
3219
- });
3220
-
3221
- it('should use googleSearchRetrieval for non-gemini-2 models', async () => {
3222
- prepareStreamResponse({
3223
- content: [''],
3224
- url: TEST_URL_GEMINI_1_0_PRO,
3225
- });
3226
-
3227
- const geminiPro = provider.languageModel('gemini-1.0-pro');
3228
- await geminiPro.doStream({
3229
- prompt: TEST_PROMPT,
3230
- includeRawChunks: false,
3231
- tools: [
3232
- {
3233
- type: 'provider',
3234
- id: 'google.google_search',
3235
- name: 'google_search',
3236
- args: {},
3237
- },
3238
- ],
3239
- });
3240
-
3241
- expect(await server.calls[0].requestBodyJson).toMatchObject({
3242
- tools: [{ googleSearchRetrieval: {} }],
3243
- });
3244
- });
3245
-
3246
- it('should use dynamic retrieval for gemini-1-5', async () => {
3247
- prepareStreamResponse({
3248
- content: [''],
3249
- url: TEST_URL_GEMINI_1_5_FLASH,
3250
- });
3251
-
3252
- const geminiPro = provider.languageModel('gemini-1.5-flash');
3253
-
3254
- await geminiPro.doStream({
3255
- prompt: TEST_PROMPT,
3256
- includeRawChunks: false,
3257
- tools: [
3258
- {
3259
- type: 'provider',
3260
- id: 'google.google_search',
3261
- name: 'google_search',
3262
- args: {
3263
- mode: 'MODE_DYNAMIC',
3264
- dynamicThreshold: 1,
3265
- },
3266
- },
3267
- ],
3268
- });
3269
-
3270
- expect(await server.calls[0].requestBodyJson).toMatchObject({
3271
- tools: [
3272
- {
3273
- googleSearchRetrieval: {
3274
- dynamicRetrievalConfig: {
3275
- mode: 'MODE_DYNAMIC',
3276
- dynamicThreshold: 1,
3277
- },
3278
- },
3279
- },
3280
- ],
3281
- });
3282
- });
3283
- });
3284
-
3285
- it('should stream source events', async () => {
3286
- prepareStreamResponse({
3287
- content: ['Some initial text'],
3288
- groundingMetadata: {
3289
- groundingChunks: [
3290
- {
3291
- web: {
3292
- uri: 'https://source.example.com',
3293
- title: 'Source Title',
3294
- },
3295
- },
3296
- ],
3297
- },
3298
- });
3299
-
3300
- const { stream } = await model.doStream({
3301
- prompt: TEST_PROMPT,
3302
- includeRawChunks: false,
3303
- });
3304
-
3305
- const events = await convertReadableStreamToArray(stream);
3306
- const sourceEvents = events.filter(event => event.type === 'source');
3307
-
3308
- expect(sourceEvents).toMatchInlineSnapshot(`
3309
- [
3310
- {
3311
- "id": "test-id",
3312
- "sourceType": "url",
3313
- "title": "Source Title",
3314
- "type": "source",
3315
- "url": "https://source.example.com",
3316
- },
3317
- ]
3318
- `);
3319
- });
3320
-
3321
- it('should stream sources during intermediate chunks', async () => {
3322
- server.urls[TEST_URL_GEMINI_PRO].response = {
3323
- type: 'stream-chunks',
3324
- chunks: [
3325
- `data: ${JSON.stringify({
3326
- candidates: [
3327
- {
3328
- content: { parts: [{ text: 'text' }], role: 'model' },
3329
- index: 0,
3330
- safetyRatings: SAFETY_RATINGS,
3331
- groundingMetadata: {
3332
- groundingChunks: [
3333
- { web: { uri: 'https://a.com', title: 'A' } },
3334
- { web: { uri: 'https://b.com', title: 'B' } },
3335
- ],
3336
- },
3337
- },
3338
- ],
3339
- })}\n\n`,
3340
- `data: ${JSON.stringify({
3341
- candidates: [
3342
- {
3343
- content: { parts: [{ text: 'more' }], role: 'model' },
3344
- finishReason: 'STOP',
3345
- index: 0,
3346
- safetyRatings: SAFETY_RATINGS,
3347
- },
3348
- ],
3349
- })}\n\n`,
3350
- ],
3351
- };
3352
-
3353
- const { stream } = await model.doStream({
3354
- prompt: TEST_PROMPT,
3355
- includeRawChunks: false,
3356
- });
3357
-
3358
- const events = await convertReadableStreamToArray(stream);
3359
- const sourceEvents = events.filter(event => event.type === 'source');
3360
-
3361
- expect(sourceEvents).toMatchInlineSnapshot(`
3362
- [
3363
- {
3364
- "id": "test-id",
3365
- "sourceType": "url",
3366
- "title": "A",
3367
- "type": "source",
3368
- "url": "https://a.com",
3369
- },
3370
- {
3371
- "id": "test-id",
3372
- "sourceType": "url",
3373
- "title": "B",
3374
- "type": "source",
3375
- "url": "https://b.com",
3376
- },
3377
- ]
3378
- `);
3379
- });
3380
-
3381
- it('should deduplicate sources across chunks', async () => {
3382
- server.urls[TEST_URL_GEMINI_PRO].response = {
3383
- type: 'stream-chunks',
3384
- chunks: [
3385
- `data: ${JSON.stringify({
3386
- candidates: [
3387
- {
3388
- content: { parts: [{ text: 'first chunk' }], role: 'model' },
3389
- index: 0,
3390
- safetyRatings: SAFETY_RATINGS,
3391
- groundingMetadata: {
3392
- groundingChunks: [
3393
- { web: { uri: 'https://example.com', title: 'Example' } },
3394
- { web: { uri: 'https://unique.com', title: 'Unique' } },
3395
- ],
3396
- },
3397
- },
3398
- ],
3399
- })}\n\n`,
3400
- `data: ${JSON.stringify({
3401
- candidates: [
3402
- {
3403
- content: { parts: [{ text: 'second chunk' }], role: 'model' },
3404
- index: 0,
3405
- safetyRatings: SAFETY_RATINGS,
3406
- groundingMetadata: {
3407
- groundingChunks: [
3408
- {
3409
- web: {
3410
- uri: 'https://example.com',
3411
- title: 'Example Duplicate',
3412
- },
3413
- },
3414
- { web: { uri: 'https://another.com', title: 'Another' } },
3415
- ],
3416
- },
3417
- },
3418
- ],
3419
- })}\n\n`,
3420
- `data: ${JSON.stringify({
3421
- candidates: [
3422
- {
3423
- content: { parts: [{ text: 'final chunk' }], role: 'model' },
3424
- finishReason: 'STOP',
3425
- index: 0,
3426
- safetyRatings: SAFETY_RATINGS,
3427
- },
3428
- ],
3429
- })}\n\n`,
3430
- ],
3431
- };
3432
-
3433
- const { stream } = await model.doStream({
3434
- prompt: TEST_PROMPT,
3435
- includeRawChunks: false,
3436
- });
3437
-
3438
- const events = await convertReadableStreamToArray(stream);
3439
- const sourceEvents = events.filter(event => event.type === 'source');
3440
-
3441
- expect(sourceEvents).toMatchInlineSnapshot(`
3442
- [
3443
- {
3444
- "id": "test-id",
3445
- "sourceType": "url",
3446
- "title": "Example",
3447
- "type": "source",
3448
- "url": "https://example.com",
3449
- },
3450
- {
3451
- "id": "test-id",
3452
- "sourceType": "url",
3453
- "title": "Unique",
3454
- "type": "source",
3455
- "url": "https://unique.com",
3456
- },
3457
- {
3458
- "id": "test-id",
3459
- "sourceType": "url",
3460
- "title": "Another",
3461
- "type": "source",
3462
- "url": "https://another.com",
3463
- },
3464
- ]
3465
- `);
3466
- });
3467
-
3468
- it('should stream files', async () => {
3469
- server.urls[TEST_URL_GEMINI_PRO].response = {
3470
- type: 'stream-chunks',
3471
- chunks: [
3472
- `data: {"candidates": [{"content": {"parts": [{"inlineData": {"data": "test","mimeType": "text/plain"}}]` +
3473
- `,"role": "model"},` +
3474
- `"finishReason": "STOP","index": 0,"safetyRatings": [` +
3475
- `{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},` +
3476
- `{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},` +
3477
- `{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},` +
3478
- `{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]}\n\n`,
3479
- `data: {"usageMetadata": {"promptTokenCount": 294,"candidatesTokenCount": 233,"totalTokenCount": 527}}\n\n`,
3480
- ],
3481
- };
3482
- const { stream } = await model.doStream({
3483
- prompt: TEST_PROMPT,
3484
- includeRawChunks: false,
3485
- });
3486
-
3487
- const events = await convertReadableStreamToArray(stream);
3488
-
3489
- expect(events).toMatchInlineSnapshot(`
3490
- [
3491
- {
3492
- "type": "stream-start",
3493
- "warnings": [],
3494
- },
3495
- {
3496
- "data": "test",
3497
- "mediaType": "text/plain",
3498
- "type": "file",
3499
- },
3500
- {
3501
- "finishReason": {
3502
- "raw": "STOP",
3503
- "unified": "stop",
3504
- },
3505
- "providerMetadata": {
3506
- "google": {
3507
- "groundingMetadata": null,
3508
- "promptFeedback": null,
3509
- "safetyRatings": [
3510
- {
3511
- "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
3512
- "probability": "NEGLIGIBLE",
3513
- },
3514
- {
3515
- "category": "HARM_CATEGORY_HATE_SPEECH",
3516
- "probability": "NEGLIGIBLE",
3517
- },
3518
- {
3519
- "category": "HARM_CATEGORY_HARASSMENT",
3520
- "probability": "NEGLIGIBLE",
3521
- },
3522
- {
3523
- "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
3524
- "probability": "NEGLIGIBLE",
3525
- },
3526
- ],
3527
- "urlContextMetadata": null,
3528
- },
3529
- },
3530
- "type": "finish",
3531
- "usage": {
3532
- "inputTokens": {
3533
- "cacheRead": 0,
3534
- "cacheWrite": undefined,
3535
- "noCache": 294,
3536
- "total": 294,
3537
- },
3538
- "outputTokens": {
3539
- "reasoning": 0,
3540
- "text": 233,
3541
- "total": 233,
3542
- },
3543
- "raw": {
3544
- "candidatesTokenCount": 233,
3545
- "promptTokenCount": 294,
3546
- "totalTokenCount": 527,
3547
- },
3548
- },
3549
- },
3550
- ]
3551
- `);
3552
- });
3553
-
3554
- it('should stream text and files in correct order', async () => {
3555
- server.urls[TEST_URL_GEMINI_PRO].response = {
3556
- type: 'stream-chunks',
3557
- chunks: [
3558
- `data: ${JSON.stringify({
3559
- candidates: [
3560
- {
3561
- content: {
3562
- parts: [
3563
- { text: 'Step 1: ' },
3564
- { inlineData: { data: 'image1', mimeType: 'image/png' } },
3565
- { text: ' Step 2: ' },
3566
- { inlineData: { data: 'image2', mimeType: 'image/jpeg' } },
3567
- { text: ' Done' },
3568
- ],
3569
- role: 'model',
3570
- },
3571
- finishReason: 'STOP',
3572
- index: 0,
3573
- safetyRatings: SAFETY_RATINGS,
3574
- },
3575
- ],
3576
- usageMetadata: {
3577
- promptTokenCount: 10,
3578
- candidatesTokenCount: 20,
3579
- totalTokenCount: 30,
3580
- },
3581
- })}\n\n`,
3582
- ],
3583
- };
3584
-
3585
- const { stream } = await model.doStream({
3586
- prompt: TEST_PROMPT,
3587
- includeRawChunks: false,
3588
- });
3589
-
3590
- const events = await convertReadableStreamToArray(stream);
3591
-
3592
- // Filter to content events only (excluding metadata)
3593
- const contentEvents = events.filter(
3594
- event =>
3595
- event.type === 'text-start' ||
3596
- event.type === 'text-delta' ||
3597
- event.type === 'text-end' ||
3598
- event.type === 'file',
3599
- );
3600
-
3601
- // Verify that text and file parts are interleaved in the correct order
3602
- expect(contentEvents).toMatchInlineSnapshot(`
3603
- [
3604
- {
3605
- "id": "0",
3606
- "providerMetadata": undefined,
3607
- "type": "text-start",
3608
- },
3609
- {
3610
- "delta": "Step 1: ",
3611
- "id": "0",
3612
- "providerMetadata": undefined,
3613
- "type": "text-delta",
3614
- },
3615
- {
3616
- "data": "image1",
3617
- "mediaType": "image/png",
3618
- "type": "file",
3619
- },
3620
- {
3621
- "delta": " Step 2: ",
3622
- "id": "0",
3623
- "providerMetadata": undefined,
3624
- "type": "text-delta",
3625
- },
3626
- {
3627
- "data": "image2",
3628
- "mediaType": "image/jpeg",
3629
- "type": "file",
3630
- },
3631
- {
3632
- "delta": " Done",
3633
- "id": "0",
3634
- "providerMetadata": undefined,
3635
- "type": "text-delta",
3636
- },
3637
- {
3638
- "id": "0",
3639
- "type": "text-end",
3640
- },
3641
- ]
3642
- `);
3643
- });
3644
-
3645
- it('should set finishReason to tool-calls when chunk contains functionCall', async () => {
3646
- server.urls[TEST_URL_GEMINI_PRO].response = {
3647
- type: 'stream-chunks',
3648
- chunks: [
3649
- `data: ${JSON.stringify({
3650
- candidates: [
3651
- {
3652
- content: {
3653
- parts: [{ text: 'Initial text response' }],
3654
- role: 'model',
3655
- },
3656
- index: 0,
3657
- safetyRatings: SAFETY_RATINGS,
3658
- },
3659
- ],
3660
- })}\n\n`,
3661
- `data: ${JSON.stringify({
3662
- candidates: [
3663
- {
3664
- content: {
3665
- parts: [
3666
- {
3667
- functionCall: {
3668
- name: 'test-tool',
3669
- args: { value: 'example value' },
3670
- },
3671
- },
3672
- ],
3673
- role: 'model',
3674
- },
3675
- finishReason: 'STOP',
3676
- index: 0,
3677
- safetyRatings: SAFETY_RATINGS,
3678
- },
3679
- ],
3680
- usageMetadata: {
3681
- promptTokenCount: 10,
3682
- candidatesTokenCount: 20,
3683
- totalTokenCount: 30,
3684
- },
3685
- })}\n\n`,
3686
- ],
3687
- };
3688
- const { stream } = await model.doStream({
3689
- tools: [
3690
- {
3691
- type: 'function',
3692
- name: 'test-tool',
3693
- inputSchema: {
3694
- type: 'object',
3695
- properties: { value: { type: 'string' } },
3696
- required: ['value'],
3697
- additionalProperties: false,
3698
- $schema: 'http://json-schema.org/draft-07/schema#',
3699
- },
3700
- },
3701
- ],
3702
- prompt: TEST_PROMPT,
3703
- includeRawChunks: false,
3704
- });
3705
-
3706
- const events = await convertReadableStreamToArray(stream);
3707
- const finishEvent = events.find(event => event.type === 'finish');
3708
-
3709
- expect(finishEvent?.finishReason).toMatchInlineSnapshot(`
3710
- {
3711
- "raw": "STOP",
3712
- "unified": "tool-calls",
3713
- }
3714
- `);
3715
- });
3716
-
3717
- it('should only pass valid provider options', async () => {
3718
- prepareStreamResponse({ content: [''] });
3719
-
3720
- await model.doStream({
3721
- prompt: TEST_PROMPT,
3722
- includeRawChunks: false,
3723
- providerOptions: {
3724
- google: { foo: 'bar', responseModalities: ['TEXT', 'IMAGE'] },
3725
- },
3726
- });
3727
-
3728
- expect(await server.calls[0].requestBodyJson).toMatchObject({
3729
- contents: [
3730
- {
3731
- role: 'user',
3732
- parts: [{ text: 'Hello' }],
3733
- },
3734
- ],
3735
- generationConfig: {
3736
- responseModalities: ['TEXT', 'IMAGE'],
3737
- },
3738
- });
3739
- });
3740
-
3741
- it('should stream reasoning parts separately from text parts', async () => {
3742
- server.urls[TEST_URL_GEMINI_PRO].response = {
3743
- type: 'stream-chunks',
3744
- chunks: [
3745
- `data: ${JSON.stringify({
3746
- candidates: [
3747
- {
3748
- content: {
3749
- parts: [
3750
- {
3751
- text: 'I need to think about this carefully. The user wants a simple explanation.',
3752
- thought: true,
3753
- },
3754
- ],
3755
- role: 'model',
3756
- },
3757
- index: 0,
3758
- },
3759
- ],
3760
- usageMetadata: {
3761
- promptTokenCount: 14,
3762
- totalTokenCount: 84,
3763
- thoughtsTokenCount: 70,
3764
- },
3765
- })}\n\n`,
3766
- `data: ${JSON.stringify({
3767
- candidates: [
3768
- {
3769
- content: {
3770
- parts: [
3771
- {
3772
- text: 'Let me organize my thoughts and provide a clear answer.',
3773
- thought: true,
3774
- },
3775
- ],
3776
- role: 'model',
3777
- },
3778
- index: 0,
3779
- },
3780
- ],
3781
- usageMetadata: {
3782
- promptTokenCount: 14,
3783
- totalTokenCount: 156,
3784
- thoughtsTokenCount: 142,
3785
- },
3786
- })}\n\n`,
3787
- `data: ${JSON.stringify({
3788
- candidates: [
3789
- {
3790
- content: {
3791
- parts: [
3792
- {
3793
- text: 'Here is a simple explanation: ',
3794
- },
3795
- ],
3796
- role: 'model',
3797
- },
3798
- index: 0,
3799
- },
3800
- ],
3801
- usageMetadata: {
3802
- promptTokenCount: 14,
3803
- candidatesTokenCount: 8,
3804
- totalTokenCount: 164,
3805
- thoughtsTokenCount: 142,
3806
- },
3807
- })}\n\n`,
3808
- `data: ${JSON.stringify({
3809
- candidates: [
3810
- {
3811
- content: {
3812
- parts: [
3813
- {
3814
- text: 'The concept works because of basic principles.',
3815
- },
3816
- ],
3817
- role: 'model',
3818
- },
3819
- finishReason: 'STOP',
3820
- index: 0,
3821
- },
3822
- ],
3823
- usageMetadata: {
3824
- promptTokenCount: 14,
3825
- candidatesTokenCount: 18,
3826
- totalTokenCount: 174,
3827
- thoughtsTokenCount: 142,
3828
- },
3829
- })}\n\n`,
3830
- ],
3831
- };
3832
-
3833
- const { stream } = await model.doStream({
3834
- prompt: TEST_PROMPT,
3835
- includeRawChunks: false,
3836
- });
3837
-
3838
- const allEvents = await convertReadableStreamToArray(stream);
3839
-
3840
- expect(allEvents).toMatchInlineSnapshot(`
3841
- [
3842
- {
3843
- "type": "stream-start",
3844
- "warnings": [],
3845
- },
3846
- {
3847
- "id": "0",
3848
- "providerMetadata": undefined,
3849
- "type": "reasoning-start",
3850
- },
3851
- {
3852
- "delta": "I need to think about this carefully. The user wants a simple explanation.",
3853
- "id": "0",
3854
- "providerMetadata": undefined,
3855
- "type": "reasoning-delta",
3856
- },
3857
- {
3858
- "delta": "Let me organize my thoughts and provide a clear answer.",
3859
- "id": "0",
3860
- "providerMetadata": undefined,
3861
- "type": "reasoning-delta",
3862
- },
3863
- {
3864
- "id": "0",
3865
- "type": "reasoning-end",
3866
- },
3867
- {
3868
- "id": "1",
3869
- "providerMetadata": undefined,
3870
- "type": "text-start",
3871
- },
3872
- {
3873
- "delta": "Here is a simple explanation: ",
3874
- "id": "1",
3875
- "providerMetadata": undefined,
3876
- "type": "text-delta",
3877
- },
3878
- {
3879
- "delta": "The concept works because of basic principles.",
3880
- "id": "1",
3881
- "providerMetadata": undefined,
3882
- "type": "text-delta",
3883
- },
3884
- {
3885
- "id": "1",
3886
- "type": "text-end",
3887
- },
3888
- {
3889
- "finishReason": {
3890
- "raw": "STOP",
3891
- "unified": "stop",
3892
- },
3893
- "providerMetadata": {
3894
- "google": {
3895
- "groundingMetadata": null,
3896
- "promptFeedback": null,
3897
- "safetyRatings": null,
3898
- "urlContextMetadata": null,
3899
- "usageMetadata": {
3900
- "candidatesTokenCount": 18,
3901
- "promptTokenCount": 14,
3902
- "thoughtsTokenCount": 142,
3903
- "totalTokenCount": 174,
3904
- },
3905
- },
3906
- },
3907
- "type": "finish",
3908
- "usage": {
3909
- "inputTokens": {
3910
- "cacheRead": 0,
3911
- "cacheWrite": undefined,
3912
- "noCache": 14,
3913
- "total": 14,
3914
- },
3915
- "outputTokens": {
3916
- "reasoning": 142,
3917
- "text": 18,
3918
- "total": 160,
3919
- },
3920
- "raw": {
3921
- "candidatesTokenCount": 18,
3922
- "promptTokenCount": 14,
3923
- "thoughtsTokenCount": 142,
3924
- "totalTokenCount": 174,
3925
- },
3926
- },
3927
- },
3928
- ]
3929
- `);
3930
- });
3931
-
3932
- it('should stream thought signatures with reasoning and text parts', async () => {
3933
- server.urls[TEST_URL_GEMINI_PRO].response = {
3934
- type: 'stream-chunks',
3935
- chunks: [
3936
- `data: ${JSON.stringify({
3937
- candidates: [
3938
- {
3939
- content: {
3940
- parts: [
3941
- {
3942
- text: 'I need to think about this.',
3943
- thought: true,
3944
- thoughtSignature: 'reasoning_sig1',
3945
- },
3946
- ],
3947
- role: 'model',
3948
- },
3949
- index: 0,
3950
- },
3951
- ],
3952
- })}\n\n`,
3953
- `data: ${JSON.stringify({
3954
- candidates: [
3955
- {
3956
- content: {
3957
- parts: [
3958
- {
3959
- text: 'Here is the answer.',
3960
- thoughtSignature: 'text_sig1',
3961
- },
3962
- ],
3963
- role: 'model',
3964
- },
3965
- index: 0,
3966
- finishReason: 'STOP',
3967
- safetyRatings: SAFETY_RATINGS,
3968
- },
3969
- ],
3970
- })}\n\n`,
3971
- ],
3972
- };
3973
-
3974
- const { stream } = await model.doStream({
3975
- prompt: TEST_PROMPT,
3976
- });
3977
-
3978
- const chunks = await convertReadableStreamToArray(stream);
3979
-
3980
- expect(chunks).toMatchInlineSnapshot(`
3981
- [
3982
- {
3983
- "type": "stream-start",
3984
- "warnings": [],
3985
- },
3986
- {
3987
- "id": "0",
3988
- "providerMetadata": {
3989
- "google": {
3990
- "thoughtSignature": "reasoning_sig1",
3991
- },
3992
- },
3993
- "type": "reasoning-start",
3994
- },
3995
- {
3996
- "delta": "I need to think about this.",
3997
- "id": "0",
3998
- "providerMetadata": {
3999
- "google": {
4000
- "thoughtSignature": "reasoning_sig1",
4001
- },
4002
- },
4003
- "type": "reasoning-delta",
4004
- },
4005
- {
4006
- "id": "0",
4007
- "type": "reasoning-end",
4008
- },
4009
- {
4010
- "id": "1",
4011
- "providerMetadata": {
4012
- "google": {
4013
- "thoughtSignature": "text_sig1",
4014
- },
4015
- },
4016
- "type": "text-start",
4017
- },
4018
- {
4019
- "delta": "Here is the answer.",
4020
- "id": "1",
4021
- "providerMetadata": {
4022
- "google": {
4023
- "thoughtSignature": "text_sig1",
4024
- },
4025
- },
4026
- "type": "text-delta",
4027
- },
4028
- {
4029
- "id": "1",
4030
- "type": "text-end",
4031
- },
4032
- {
4033
- "finishReason": {
4034
- "raw": "STOP",
4035
- "unified": "stop",
4036
- },
4037
- "providerMetadata": {
4038
- "google": {
4039
- "groundingMetadata": null,
4040
- "promptFeedback": null,
4041
- "safetyRatings": [
4042
- {
4043
- "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
4044
- "probability": "NEGLIGIBLE",
4045
- },
4046
- {
4047
- "category": "HARM_CATEGORY_HATE_SPEECH",
4048
- "probability": "NEGLIGIBLE",
4049
- },
4050
- {
4051
- "category": "HARM_CATEGORY_HARASSMENT",
4052
- "probability": "NEGLIGIBLE",
4053
- },
4054
- {
4055
- "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
4056
- "probability": "NEGLIGIBLE",
4057
- },
4058
- ],
4059
- "urlContextMetadata": null,
4060
- },
4061
- },
4062
- "type": "finish",
4063
- "usage": {
4064
- "inputTokens": {
4065
- "cacheRead": undefined,
4066
- "cacheWrite": undefined,
4067
- "noCache": undefined,
4068
- "total": undefined,
4069
- },
4070
- "outputTokens": {
4071
- "reasoning": undefined,
4072
- "text": undefined,
4073
- "total": undefined,
4074
- },
4075
- "raw": undefined,
4076
- },
4077
- },
4078
- ]
4079
- `);
4080
- });
4081
-
4082
- describe('raw chunks', () => {
4083
- it('should include raw chunks when includeRawChunks is enabled', async () => {
4084
- prepareStreamResponse({
4085
- content: ['Hello', ' World!'],
4086
- });
4087
-
4088
- const { stream } = await model.doStream({
4089
- prompt: TEST_PROMPT,
4090
- includeRawChunks: true,
4091
- });
4092
-
4093
- const chunks = await convertReadableStreamToArray(stream);
4094
-
4095
- expect(chunks.filter(chunk => chunk.type === 'raw'))
4096
- .toMatchInlineSnapshot(`
4097
- [
4098
- {
4099
- "rawValue": {
4100
- "candidates": [
4101
- {
4102
- "content": {
4103
- "parts": [
4104
- {
4105
- "text": "Hello",
4106
- },
4107
- ],
4108
- "role": "model",
4109
- },
4110
- "finishReason": "STOP",
4111
- "index": 0,
4112
- "safetyRatings": [
4113
- {
4114
- "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
4115
- "probability": "NEGLIGIBLE",
4116
- },
4117
- {
4118
- "category": "HARM_CATEGORY_HATE_SPEECH",
4119
- "probability": "NEGLIGIBLE",
4120
- },
4121
- {
4122
- "category": "HARM_CATEGORY_HARASSMENT",
4123
- "probability": "NEGLIGIBLE",
4124
- },
4125
- {
4126
- "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
4127
- "probability": "NEGLIGIBLE",
4128
- },
4129
- ],
4130
- },
4131
- ],
4132
- },
4133
- "type": "raw",
4134
- },
4135
- {
4136
- "rawValue": {
4137
- "candidates": [
4138
- {
4139
- "content": {
4140
- "parts": [
4141
- {
4142
- "text": " World!",
4143
- },
4144
- ],
4145
- "role": "model",
4146
- },
4147
- "finishReason": "STOP",
4148
- "index": 0,
4149
- "safetyRatings": [
4150
- {
4151
- "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
4152
- "probability": "NEGLIGIBLE",
4153
- },
4154
- {
4155
- "category": "HARM_CATEGORY_HATE_SPEECH",
4156
- "probability": "NEGLIGIBLE",
4157
- },
4158
- {
4159
- "category": "HARM_CATEGORY_HARASSMENT",
4160
- "probability": "NEGLIGIBLE",
4161
- },
4162
- {
4163
- "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
4164
- "probability": "NEGLIGIBLE",
4165
- },
4166
- ],
4167
- },
4168
- ],
4169
- "usageMetadata": {
4170
- "candidatesTokenCount": 233,
4171
- "promptTokenCount": 294,
4172
- "totalTokenCount": 527,
4173
- },
4174
- },
4175
- "type": "raw",
4176
- },
4177
- ]
4178
- `);
4179
- });
4180
-
4181
- it('should not include raw chunks when includeRawChunks is false', async () => {
4182
- prepareStreamResponse({
4183
- content: ['Hello', ' World!'],
4184
- });
4185
-
4186
- const { stream } = await model.doStream({
4187
- prompt: TEST_PROMPT,
4188
- includeRawChunks: false,
4189
- });
4190
-
4191
- const chunks = await convertReadableStreamToArray(stream);
4192
-
4193
- expect(chunks.filter(chunk => chunk.type === 'raw')).toHaveLength(0);
4194
- });
4195
- });
4196
-
4197
- describe('providerMetadata key based on provider string', () => {
4198
- it('should use "vertex" as providerMetadata key in finish event when provider includes "vertex"', async () => {
4199
- server.urls[TEST_URL_GEMINI_PRO].response = {
4200
- type: 'stream-chunks',
4201
- chunks: [
4202
- `data: ${JSON.stringify({
4203
- candidates: [
4204
- {
4205
- content: { parts: [{ text: 'Hello' }], role: 'model' },
4206
- },
4207
- ],
4208
- })}\n\n`,
4209
- `data: ${JSON.stringify({
4210
- candidates: [
4211
- {
4212
- content: { parts: [{ text: ' World!' }], role: 'model' },
4213
- finishReason: 'STOP',
4214
- safetyRatings: SAFETY_RATINGS,
4215
- groundingMetadata: {
4216
- webSearchQueries: ['test query'],
4217
- },
4218
- },
4219
- ],
4220
- usageMetadata: {
4221
- promptTokenCount: 1,
4222
- candidatesTokenCount: 2,
4223
- totalTokenCount: 3,
4224
- },
4225
- })}\n\n`,
4226
- ],
4227
- };
4228
-
4229
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
4230
- provider: 'google.vertex.chat',
4231
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
4232
- headers: { 'x-goog-api-key': 'test-api-key' },
4233
- generateId: () => 'test-id',
4234
- });
4235
-
4236
- const { stream } = await model.doStream({
4237
- prompt: TEST_PROMPT,
4238
- });
4239
-
4240
- const events = await convertReadableStreamToArray(stream);
4241
- const finishEvent = events.find(event => event.type === 'finish');
4242
-
4243
- expect(finishEvent?.type === 'finish').toBe(true);
4244
- if (finishEvent?.type === 'finish') {
4245
- expect(finishEvent.providerMetadata).toHaveProperty('vertex');
4246
- expect(finishEvent.providerMetadata?.vertex).toMatchObject({
4247
- promptFeedback: null,
4248
- groundingMetadata: expect.any(Object),
4249
- safetyRatings: expect.any(Array),
4250
- });
4251
- }
4252
- });
4253
-
4254
- it('should use "google" as providerMetadata key in finish event when provider does not include "vertex"', async () => {
4255
- server.urls[TEST_URL_GEMINI_PRO].response = {
4256
- type: 'stream-chunks',
4257
- chunks: [
4258
- `data: ${JSON.stringify({
4259
- candidates: [
4260
- {
4261
- content: { parts: [{ text: 'Hello' }], role: 'model' },
4262
- },
4263
- ],
4264
- })}\n\n`,
4265
- `data: ${JSON.stringify({
4266
- candidates: [
4267
- {
4268
- content: { parts: [{ text: ' World!' }], role: 'model' },
4269
- finishReason: 'STOP',
4270
- safetyRatings: SAFETY_RATINGS,
4271
- groundingMetadata: {
4272
- webSearchQueries: ['test query'],
4273
- },
4274
- },
4275
- ],
4276
- usageMetadata: {
4277
- promptTokenCount: 1,
4278
- candidatesTokenCount: 2,
4279
- totalTokenCount: 3,
4280
- },
4281
- })}\n\n`,
4282
- ],
4283
- };
4284
-
4285
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
4286
- provider: 'google.generative-ai',
4287
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
4288
- headers: { 'x-goog-api-key': 'test-api-key' },
4289
- generateId: () => 'test-id',
4290
- });
4291
-
4292
- const { stream } = await model.doStream({
4293
- prompt: TEST_PROMPT,
4294
- });
4295
-
4296
- const events = await convertReadableStreamToArray(stream);
4297
- const finishEvent = events.find(event => event.type === 'finish');
4298
-
4299
- expect(finishEvent?.type === 'finish').toBe(true);
4300
- if (finishEvent?.type === 'finish') {
4301
- expect(finishEvent.providerMetadata).toHaveProperty('google');
4302
- expect(finishEvent.providerMetadata).not.toHaveProperty('vertex');
4303
- expect(finishEvent.providerMetadata?.google).toMatchObject({
4304
- promptFeedback: null,
4305
- groundingMetadata: expect.any(Object),
4306
- safetyRatings: expect.any(Array),
4307
- });
4308
- }
4309
- });
4310
-
4311
- it('should use "vertex" as providerMetadata key in streaming reasoning events when provider includes "vertex"', async () => {
4312
- server.urls[TEST_URL_GEMINI_PRO].response = {
4313
- type: 'stream-chunks',
4314
- chunks: [
4315
- `data: ${JSON.stringify({
4316
- candidates: [
4317
- {
4318
- content: {
4319
- parts: [
4320
- {
4321
- text: 'thinking...',
4322
- thought: true,
4323
- thoughtSignature: 'stream_sig',
4324
- },
4325
- ],
4326
- role: 'model',
4327
- },
4328
- },
4329
- ],
4330
- })}\n\n`,
4331
- `data: ${JSON.stringify({
4332
- candidates: [
4333
- {
4334
- content: { parts: [{ text: 'Final answer' }], role: 'model' },
4335
- finishReason: 'STOP',
4336
- safetyRatings: SAFETY_RATINGS,
4337
- },
4338
- ],
4339
- usageMetadata: {
4340
- promptTokenCount: 1,
4341
- candidatesTokenCount: 2,
4342
- totalTokenCount: 3,
4343
- },
4344
- })}\n\n`,
4345
- ],
4346
- };
4347
-
4348
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
4349
- provider: 'google.vertex.chat',
4350
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
4351
- headers: { 'x-goog-api-key': 'test-api-key' },
4352
- generateId: () => 'test-id',
4353
- });
4354
-
4355
- const { stream } = await model.doStream({
4356
- prompt: TEST_PROMPT,
4357
- });
4358
-
4359
- const events = await convertReadableStreamToArray(stream);
4360
-
4361
- const reasoningStartEvent = events.find(
4362
- event => event.type === 'reasoning-start',
4363
- );
4364
- expect(reasoningStartEvent?.type === 'reasoning-start').toBe(true);
4365
- if (reasoningStartEvent?.type === 'reasoning-start') {
4366
- expect(reasoningStartEvent.providerMetadata).toHaveProperty('vertex');
4367
- expect(reasoningStartEvent.providerMetadata).not.toHaveProperty(
4368
- 'google',
4369
- );
4370
- expect(reasoningStartEvent.providerMetadata?.vertex).toMatchObject({
4371
- thoughtSignature: 'stream_sig',
4372
- });
4373
- }
4374
-
4375
- const reasoningDeltaEvent = events.find(
4376
- event => event.type === 'reasoning-delta',
4377
- );
4378
- expect(reasoningDeltaEvent?.type === 'reasoning-delta').toBe(true);
4379
- if (reasoningDeltaEvent?.type === 'reasoning-delta') {
4380
- expect(reasoningDeltaEvent.providerMetadata).toHaveProperty('vertex');
4381
- expect(reasoningDeltaEvent.providerMetadata).not.toHaveProperty(
4382
- 'google',
4383
- );
4384
- }
4385
- });
4386
- });
4387
- });
4388
-
4389
- describe('GEMMA Model System Instruction Fix', () => {
4390
- const TEST_PROMPT_WITH_SYSTEM: LanguageModelV3Prompt = [
4391
- { role: 'system', content: 'You are a helpful assistant.' },
4392
- { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
4393
- ];
4394
-
4395
- const TEST_URL_GEMMA_3_12B_IT =
4396
- 'https://generativelanguage.googleapis.com/v1beta/models/gemma-3-12b-it:generateContent';
4397
-
4398
- const TEST_URL_GEMMA_3_27B_IT =
4399
- 'https://generativelanguage.googleapis.com/v1beta/models/gemma-3-27b-it:generateContent';
4400
-
4401
- const TEST_URL_GEMINI_PRO =
4402
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent';
4403
-
4404
- const server = createTestServer({
4405
- [TEST_URL_GEMMA_3_12B_IT]: {},
4406
- [TEST_URL_GEMMA_3_27B_IT]: {},
4407
- [TEST_URL_GEMINI_PRO]: {},
4408
- });
4409
-
4410
- it('should NOT send systemInstruction for GEMMA-3-12b-it model', async () => {
4411
- server.urls[TEST_URL_GEMMA_3_12B_IT].response = {
4412
- type: 'json-value',
4413
- body: {
4414
- candidates: [
4415
- {
4416
- content: { parts: [{ text: 'Hello!' }], role: 'model' },
4417
- finishReason: 'STOP',
4418
- index: 0,
4419
- },
4420
- ],
4421
- },
4422
- };
4423
-
4424
- const model = new GoogleGenerativeAILanguageModel('gemma-3-12b-it', {
4425
- provider: 'google.generative-ai',
4426
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
4427
- headers: { 'x-goog-api-key': 'test-api-key' },
4428
- generateId: () => 'test-id',
4429
- });
4430
-
4431
- await model.doGenerate({
4432
- prompt: TEST_PROMPT_WITH_SYSTEM,
4433
- });
4434
-
4435
- // Verify that systemInstruction was NOT sent for GEMMA model
4436
- const lastCall = server.calls[server.calls.length - 1];
4437
- const requestBody = await lastCall.requestBodyJson;
4438
-
4439
- expect(requestBody).not.toHaveProperty('systemInstruction');
4440
- });
4441
-
4442
- it('should NOT send systemInstruction for GEMMA-3-27b-it model', async () => {
4443
- server.urls[TEST_URL_GEMMA_3_27B_IT].response = {
4444
- type: 'json-value',
4445
- body: {
4446
- candidates: [
4447
- {
4448
- content: { parts: [{ text: 'Hello!' }], role: 'model' },
4449
- finishReason: 'STOP',
4450
- index: 0,
4451
- },
4452
- ],
4453
- },
4454
- };
4455
-
4456
- const model = new GoogleGenerativeAILanguageModel('gemma-3-27b-it', {
4457
- provider: 'google.generative-ai',
4458
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
4459
- headers: { 'x-goog-api-key': 'test-api-key' },
4460
- generateId: () => 'test-id',
4461
- });
4462
-
4463
- await model.doGenerate({
4464
- prompt: TEST_PROMPT_WITH_SYSTEM,
4465
- });
4466
-
4467
- const lastCall = server.calls[server.calls.length - 1];
4468
- const requestBody = await lastCall.requestBodyJson;
4469
-
4470
- expect(requestBody).not.toHaveProperty('systemInstruction');
4471
- });
4472
-
4473
- it('should still send systemInstruction for Gemini models (regression test)', async () => {
4474
- server.urls[TEST_URL_GEMINI_PRO].response = {
4475
- type: 'json-value',
4476
- body: {
4477
- candidates: [
4478
- {
4479
- content: { parts: [{ text: 'Hello!' }], role: 'model' },
4480
- finishReason: 'STOP',
4481
- index: 0,
4482
- },
4483
- ],
4484
- },
4485
- };
4486
-
4487
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
4488
- provider: 'google.generative-ai',
4489
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
4490
- headers: { 'x-goog-api-key': 'test-api-key' },
4491
- generateId: () => 'test-id',
4492
- });
4493
-
4494
- await model.doGenerate({
4495
- prompt: TEST_PROMPT_WITH_SYSTEM,
4496
- });
4497
-
4498
- const lastCall = server.calls[server.calls.length - 1];
4499
- const requestBody = await lastCall.requestBodyJson;
4500
-
4501
- expect(requestBody).toHaveProperty('systemInstruction');
4502
- expect(requestBody.systemInstruction).toEqual({
4503
- parts: [{ text: 'You are a helpful assistant.' }],
4504
- });
4505
- });
4506
-
4507
- it('should NOT generate warning when GEMMA model is used without system instructions', async () => {
4508
- server.urls[TEST_URL_GEMMA_3_12B_IT].response = {
4509
- type: 'json-value',
4510
- body: {
4511
- candidates: [
4512
- {
4513
- content: { parts: [{ text: 'Hello!' }], role: 'model' },
4514
- finishReason: 'STOP',
4515
- index: 0,
4516
- },
4517
- ],
4518
- },
4519
- };
4520
-
4521
- const model = new GoogleGenerativeAILanguageModel('gemma-3-12b-it', {
4522
- provider: 'google.generative-ai',
4523
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
4524
- headers: { 'x-goog-api-key': 'test-api-key' },
4525
- generateId: () => 'test-id',
4526
- });
4527
-
4528
- const TEST_PROMPT_WITHOUT_SYSTEM: LanguageModelV3Prompt = [
4529
- { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
4530
- ];
4531
-
4532
- const { warnings } = await model.doGenerate({
4533
- prompt: TEST_PROMPT_WITHOUT_SYSTEM,
4534
- });
4535
-
4536
- expect(warnings).toHaveLength(0);
4537
- });
4538
-
4539
- it('should NOT generate warning when Gemini model is used with system instructions', async () => {
4540
- server.urls[TEST_URL_GEMINI_PRO].response = {
4541
- type: 'json-value',
4542
- body: {
4543
- candidates: [
4544
- {
4545
- content: { parts: [{ text: 'Hello!' }], role: 'model' },
4546
- finishReason: 'STOP',
4547
- index: 0,
4548
- },
4549
- ],
4550
- },
4551
- };
4552
-
4553
- const model = new GoogleGenerativeAILanguageModel('gemini-pro', {
4554
- provider: 'google.generative-ai',
4555
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
4556
- headers: { 'x-goog-api-key': 'test-api-key' },
4557
- generateId: () => 'test-id',
4558
- });
4559
-
4560
- const { warnings } = await model.doGenerate({
4561
- prompt: TEST_PROMPT_WITH_SYSTEM,
4562
- });
4563
-
4564
- expect(warnings).toHaveLength(0);
4565
- });
4566
-
4567
- it('should prepend system instruction to first user message for GEMMA models', async () => {
4568
- server.urls[TEST_URL_GEMMA_3_12B_IT].response = {
4569
- type: 'json-value',
4570
- body: {
4571
- candidates: [
4572
- {
4573
- content: { parts: [{ text: 'Hello!' }], role: 'model' },
4574
- finishReason: 'STOP',
4575
- index: 0,
4576
- },
4577
- ],
4578
- },
4579
- };
4580
-
4581
- const model = new GoogleGenerativeAILanguageModel('gemma-3-12b-it', {
4582
- provider: 'google.generative-ai',
4583
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
4584
- headers: { 'x-goog-api-key': 'test-api-key' },
4585
- generateId: () => 'test-id',
4586
- });
4587
-
4588
- await model.doGenerate({
4589
- prompt: TEST_PROMPT_WITH_SYSTEM,
4590
- });
4591
-
4592
- const lastCall = server.calls[server.calls.length - 1];
4593
- const requestBody = await lastCall.requestBodyJson;
4594
-
4595
- expect(requestBody).toMatchInlineSnapshot(`
4596
- {
4597
- "contents": [
4598
- {
4599
- "parts": [
4600
- {
4601
- "text": "You are a helpful assistant.
4602
-
4603
- ",
4604
- },
4605
- {
4606
- "text": "Hello",
4607
- },
4608
- ],
4609
- "role": "user",
4610
- },
4611
- ],
4612
- "generationConfig": {},
4613
- }
4614
- `);
4615
- });
4616
- });