@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,495 +0,0 @@
1
- import { convertToGoogleGenerativeAIMessages } from './convert-to-google-generative-ai-messages';
2
- import { describe, it, expect } from 'vitest';
3
-
4
- describe('system messages', () => {
5
- it('should store system message in system instruction', async () => {
6
- const result = convertToGoogleGenerativeAIMessages([
7
- { role: 'system', content: 'Test' },
8
- ]);
9
-
10
- expect(result).toEqual({
11
- systemInstruction: { parts: [{ text: 'Test' }] },
12
- contents: [],
13
- });
14
- });
15
-
16
- it('should throw error when there was already a user message', async () => {
17
- expect(() =>
18
- convertToGoogleGenerativeAIMessages([
19
- { role: 'user', content: [{ type: 'text', text: 'Test' }] },
20
- { role: 'system', content: 'Test' },
21
- ]),
22
- ).toThrow(
23
- 'system messages are only supported at the beginning of the conversation',
24
- );
25
- });
26
- });
27
-
28
- describe('thought signatures', () => {
29
- it('should preserve thought signatures in assistant messages', async () => {
30
- const result = convertToGoogleGenerativeAIMessages([
31
- {
32
- role: 'assistant',
33
- content: [
34
- {
35
- type: 'text',
36
- text: 'Regular text',
37
- providerOptions: { google: { thoughtSignature: 'sig1' } },
38
- },
39
- {
40
- type: 'reasoning',
41
- text: 'Reasoning text',
42
- providerOptions: { google: { thoughtSignature: 'sig2' } },
43
- },
44
- {
45
- type: 'tool-call',
46
- toolCallId: 'call1',
47
- toolName: 'test',
48
- input: { value: 'test' },
49
- providerOptions: { google: { thoughtSignature: 'sig3' } },
50
- },
51
- ],
52
- },
53
- ]);
54
-
55
- expect(result).toMatchInlineSnapshot(`
56
- {
57
- "contents": [
58
- {
59
- "parts": [
60
- {
61
- "text": "Regular text",
62
- "thoughtSignature": "sig1",
63
- },
64
- {
65
- "text": "Reasoning text",
66
- "thought": true,
67
- "thoughtSignature": "sig2",
68
- },
69
- {
70
- "functionCall": {
71
- "args": {
72
- "value": "test",
73
- },
74
- "name": "test",
75
- },
76
- "thoughtSignature": "sig3",
77
- },
78
- ],
79
- "role": "model",
80
- },
81
- ],
82
- "systemInstruction": undefined,
83
- }
84
- `);
85
- });
86
- });
87
-
88
- describe('Gemma model system instructions', () => {
89
- it('should prepend system instruction to first user message for Gemma models', async () => {
90
- const result = convertToGoogleGenerativeAIMessages(
91
- [
92
- { role: 'system', content: 'You are a helpful assistant.' },
93
- { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
94
- ],
95
- { isGemmaModel: true },
96
- );
97
-
98
- expect(result).toMatchInlineSnapshot(`
99
- {
100
- "contents": [
101
- {
102
- "parts": [
103
- {
104
- "text": "You are a helpful assistant.
105
-
106
- ",
107
- },
108
- {
109
- "text": "Hello",
110
- },
111
- ],
112
- "role": "user",
113
- },
114
- ],
115
- "systemInstruction": undefined,
116
- }
117
- `);
118
- });
119
-
120
- it('should handle multiple system messages for Gemma models', async () => {
121
- const result = convertToGoogleGenerativeAIMessages(
122
- [
123
- { role: 'system', content: 'You are helpful.' },
124
- { role: 'system', content: 'Be concise.' },
125
- { role: 'user', content: [{ type: 'text', text: 'Hi' }] },
126
- ],
127
- { isGemmaModel: true },
128
- );
129
-
130
- expect(result).toMatchInlineSnapshot(`
131
- {
132
- "contents": [
133
- {
134
- "parts": [
135
- {
136
- "text": "You are helpful.
137
-
138
- Be concise.
139
-
140
- ",
141
- },
142
- {
143
- "text": "Hi",
144
- },
145
- ],
146
- "role": "user",
147
- },
148
- ],
149
- "systemInstruction": undefined,
150
- }
151
- `);
152
- });
153
-
154
- it('should not affect non-Gemma models', async () => {
155
- const result = convertToGoogleGenerativeAIMessages(
156
- [
157
- { role: 'system', content: 'You are helpful.' },
158
- { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
159
- ],
160
- { isGemmaModel: false },
161
- );
162
-
163
- expect(result).toMatchInlineSnapshot(`
164
- {
165
- "contents": [
166
- {
167
- "parts": [
168
- {
169
- "text": "Hello",
170
- },
171
- ],
172
- "role": "user",
173
- },
174
- ],
175
- "systemInstruction": {
176
- "parts": [
177
- {
178
- "text": "You are helpful.",
179
- },
180
- ],
181
- },
182
- }
183
- `);
184
- });
185
-
186
- it('should handle Gemma model with system instruction but no user messages', async () => {
187
- const result = convertToGoogleGenerativeAIMessages(
188
- [{ role: 'system', content: 'You are helpful.' }],
189
- { isGemmaModel: true },
190
- );
191
-
192
- expect(result).toMatchInlineSnapshot(`
193
- {
194
- "contents": [],
195
- "systemInstruction": undefined,
196
- }
197
- `);
198
- });
199
- });
200
-
201
- describe('user messages', () => {
202
- it('should add image parts', async () => {
203
- const result = convertToGoogleGenerativeAIMessages([
204
- {
205
- role: 'user',
206
- content: [
207
- {
208
- type: 'file',
209
- data: 'AAECAw==',
210
- mediaType: 'image/png',
211
- },
212
- ],
213
- },
214
- ]);
215
-
216
- expect(result).toEqual({
217
- systemInstruction: undefined,
218
- contents: [
219
- {
220
- role: 'user',
221
- parts: [
222
- {
223
- inlineData: {
224
- data: 'AAECAw==',
225
- mimeType: 'image/png',
226
- },
227
- },
228
- ],
229
- },
230
- ],
231
- });
232
- });
233
-
234
- it('should add file parts for base64 encoded files', async () => {
235
- const result = convertToGoogleGenerativeAIMessages([
236
- {
237
- role: 'user',
238
- content: [{ type: 'file', data: 'AAECAw==', mediaType: 'image/png' }],
239
- },
240
- ]);
241
-
242
- expect(result).toEqual({
243
- systemInstruction: undefined,
244
- contents: [
245
- {
246
- role: 'user',
247
- parts: [
248
- {
249
- inlineData: {
250
- data: 'AAECAw==',
251
- mimeType: 'image/png',
252
- },
253
- },
254
- ],
255
- },
256
- ],
257
- });
258
- });
259
- });
260
-
261
- describe('tool messages', () => {
262
- it('should convert tool result messages to function responses', async () => {
263
- const result = convertToGoogleGenerativeAIMessages([
264
- {
265
- role: 'tool',
266
- content: [
267
- {
268
- type: 'tool-result',
269
- toolName: 'testFunction',
270
- toolCallId: 'testCallId',
271
- output: { type: 'json', value: { someData: 'test result' } },
272
- },
273
- ],
274
- },
275
- ]);
276
-
277
- expect(result).toEqual({
278
- systemInstruction: undefined,
279
- contents: [
280
- {
281
- role: 'user',
282
- parts: [
283
- {
284
- functionResponse: {
285
- name: 'testFunction',
286
- response: {
287
- name: 'testFunction',
288
- content: { someData: 'test result' },
289
- },
290
- },
291
- },
292
- ],
293
- },
294
- ],
295
- });
296
- });
297
- });
298
-
299
- describe('assistant messages', () => {
300
- it('should add PNG image parts for base64 encoded files', async () => {
301
- const result = convertToGoogleGenerativeAIMessages([
302
- {
303
- role: 'assistant',
304
- content: [{ type: 'file', data: 'AAECAw==', mediaType: 'image/png' }],
305
- },
306
- ]);
307
-
308
- expect(result).toEqual({
309
- systemInstruction: undefined,
310
- contents: [
311
- {
312
- role: 'model',
313
- parts: [
314
- {
315
- inlineData: {
316
- data: 'AAECAw==',
317
- mimeType: 'image/png',
318
- },
319
- },
320
- ],
321
- },
322
- ],
323
- });
324
- });
325
-
326
- it('should throw error for URL file data in assistant messages', async () => {
327
- expect(() =>
328
- convertToGoogleGenerativeAIMessages([
329
- {
330
- role: 'assistant',
331
- content: [
332
- {
333
- type: 'file',
334
- data: new URL('https://example.com/image.png'),
335
- mediaType: 'image/png',
336
- },
337
- ],
338
- },
339
- ]),
340
- ).toThrow('File data URLs in assistant messages are not supported');
341
- });
342
-
343
- it('should convert tool result messages with content type (multipart with images)', async () => {
344
- const result = convertToGoogleGenerativeAIMessages([
345
- {
346
- role: 'tool',
347
- content: [
348
- {
349
- type: 'tool-result',
350
- toolName: 'imageGenerator',
351
- toolCallId: 'testCallId',
352
- output: {
353
- type: 'content',
354
- value: [
355
- {
356
- type: 'text',
357
- text: 'Here is the generated image:',
358
- },
359
- {
360
- type: 'image-data',
361
- data: 'base64encodedimagedata',
362
- mediaType: 'image/jpeg',
363
- },
364
- ],
365
- },
366
- },
367
- ],
368
- },
369
- ]);
370
-
371
- expect(result).toEqual({
372
- systemInstruction: undefined,
373
- contents: [
374
- {
375
- role: 'user',
376
- parts: [
377
- {
378
- functionResponse: {
379
- name: 'imageGenerator',
380
- response: {
381
- name: 'imageGenerator',
382
- content: 'Here is the generated image:',
383
- },
384
- },
385
- },
386
- {
387
- inlineData: {
388
- mimeType: 'image/jpeg',
389
- data: 'base64encodedimagedata',
390
- },
391
- },
392
- {
393
- text: 'Tool executed successfully and returned this image as a response',
394
- },
395
- ],
396
- },
397
- ],
398
- });
399
- });
400
- });
401
-
402
- describe('parallel tool calls', () => {
403
- it('should include thought signature on functionCall when provided', async () => {
404
- const result = convertToGoogleGenerativeAIMessages([
405
- {
406
- role: 'assistant',
407
- content: [
408
- {
409
- type: 'tool-call',
410
- toolCallId: 'call1',
411
- toolName: 'checkweather',
412
- input: { city: 'paris' },
413
- providerOptions: { google: { thoughtSignature: 'sig_parallel' } },
414
- },
415
- {
416
- type: 'tool-call',
417
- toolCallId: 'call2',
418
- toolName: 'checkweather',
419
- input: { city: 'london' },
420
- },
421
- ],
422
- },
423
- ]);
424
-
425
- expect(result.contents[0].parts[0]).toEqual({
426
- functionCall: {
427
- args: { city: 'paris' },
428
- name: 'checkweather',
429
- },
430
- thoughtSignature: 'sig_parallel',
431
- });
432
-
433
- expect(result.contents[0].parts[1]).toEqual({
434
- functionCall: {
435
- args: { city: 'london' },
436
- name: 'checkweather',
437
- },
438
- thoughtSignature: undefined,
439
- });
440
- });
441
- });
442
-
443
- describe('tool results with thought signatures', () => {
444
- it('should include thought signature on functionCall but not on functionResponse', async () => {
445
- const result = convertToGoogleGenerativeAIMessages([
446
- {
447
- role: 'assistant',
448
- content: [
449
- {
450
- type: 'tool-call',
451
- toolCallId: 'call1',
452
- toolName: 'readdata',
453
- input: { userId: '123' },
454
- providerOptions: { google: { thoughtSignature: 'sig_original' } },
455
- },
456
- ],
457
- },
458
- {
459
- role: 'tool',
460
- content: [
461
- {
462
- type: 'tool-result',
463
- toolCallId: 'call1',
464
- toolName: 'readdata',
465
- output: {
466
- type: 'error-text',
467
- value: 'file not found',
468
- },
469
- providerOptions: { google: { thoughtSignature: 'sig_original' } },
470
- },
471
- ],
472
- },
473
- ]);
474
-
475
- expect(result.contents[0].parts[0]).toEqual({
476
- functionCall: {
477
- args: { userId: '123' },
478
- name: 'readdata',
479
- },
480
- thoughtSignature: 'sig_original',
481
- });
482
-
483
- expect(result.contents[1].parts[0]).toEqual({
484
- functionResponse: {
485
- name: 'readdata',
486
- response: {
487
- content: 'file not found',
488
- name: 'readdata',
489
- },
490
- },
491
- });
492
-
493
- expect(result.contents[1].parts[0]).not.toHaveProperty('thoughtSignature');
494
- });
495
- });
@@ -1,16 +0,0 @@
1
- import { getModelPath } from './get-model-path';
2
- import { it, expect } from 'vitest';
3
-
4
- it('should pass through model path for models/*', async () => {
5
- expect(getModelPath('models/some-model')).toEqual('models/some-model');
6
- });
7
-
8
- it('should pass through model path for tunedModels/*', async () => {
9
- expect(getModelPath('tunedModels/some-model')).toEqual(
10
- 'tunedModels/some-model',
11
- );
12
- });
13
-
14
- it('should add model path prefix to models without slash', async () => {
15
- expect(getModelPath('some-model')).toEqual('models/some-model');
16
- });
@@ -1,204 +0,0 @@
1
- import { EmbeddingModelV3Embedding } from '@ai-sdk/provider';
2
- import { createTestServer } from '@ai-sdk/test-server/with-vitest';
3
- import { GoogleGenerativeAIEmbeddingModel } from './google-generative-ai-embedding-model';
4
- import { createGoogleGenerativeAI } from './google-provider';
5
- import { describe, it, expect, vi } from 'vitest';
6
-
7
- vi.mock('./version', () => ({
8
- VERSION: '0.0.0-test',
9
- }));
10
-
11
- const dummyEmbeddings = [
12
- [0.1, 0.2, 0.3, 0.4, 0.5],
13
- [0.6, 0.7, 0.8, 0.9, 1.0],
14
- ];
15
- const testValues = ['sunny day at the beach', 'rainy day in the city'];
16
-
17
- const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key' });
18
- const model = provider.embeddingModel('gemini-embedding-001');
19
-
20
- const URL =
21
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:something';
22
-
23
- const server = createTestServer({
24
- [URL]: {},
25
- });
26
-
27
- describe('GoogleGenerativeAIEmbeddingModel', () => {
28
- function prepareBatchJsonResponse({
29
- embeddings = dummyEmbeddings,
30
- headers,
31
- }: {
32
- embeddings?: EmbeddingModelV3Embedding[];
33
- headers?: Record<string, string>;
34
- } = {}) {
35
- server.urls[URL].response = {
36
- type: 'json-value',
37
- headers,
38
- body: {
39
- embeddings: embeddings.map(embedding => ({ values: embedding })),
40
- },
41
- };
42
- }
43
-
44
- function prepareSingleJsonResponse({
45
- embeddings = dummyEmbeddings,
46
- headers,
47
- }: {
48
- embeddings?: EmbeddingModelV3Embedding[];
49
- headers?: Record<string, string>;
50
- } = {}) {
51
- server.urls[URL].response = {
52
- type: 'json-value',
53
- headers,
54
- body: {
55
- embedding: { values: embeddings[0] },
56
- },
57
- };
58
- }
59
-
60
- it('should extract embedding', async () => {
61
- prepareBatchJsonResponse();
62
-
63
- const { embeddings } = await model.doEmbed({ values: testValues });
64
-
65
- expect(embeddings).toStrictEqual(dummyEmbeddings);
66
- });
67
-
68
- it('should expose the raw response', async () => {
69
- prepareBatchJsonResponse({
70
- headers: {
71
- 'test-header': 'test-value',
72
- },
73
- });
74
-
75
- const { response } = await model.doEmbed({ values: testValues });
76
-
77
- expect(response?.headers).toStrictEqual({
78
- // default headers:
79
- 'content-length': '80',
80
- 'content-type': 'application/json',
81
-
82
- // custom header
83
- 'test-header': 'test-value',
84
- });
85
- expect(response).toMatchSnapshot();
86
- });
87
-
88
- it('should pass the model and the values', async () => {
89
- prepareBatchJsonResponse();
90
-
91
- await model.doEmbed({ values: testValues });
92
-
93
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
94
- requests: testValues.map(value => ({
95
- model: 'models/gemini-embedding-001',
96
- content: { role: 'user', parts: [{ text: value }] },
97
- })),
98
- });
99
- });
100
-
101
- it('should pass the outputDimensionality setting', async () => {
102
- prepareBatchJsonResponse();
103
-
104
- await provider.embedding('gemini-embedding-001').doEmbed({
105
- values: testValues,
106
- providerOptions: {
107
- google: { outputDimensionality: 64 },
108
- },
109
- });
110
-
111
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
112
- requests: testValues.map(value => ({
113
- model: 'models/gemini-embedding-001',
114
- content: { role: 'user', parts: [{ text: value }] },
115
- outputDimensionality: 64,
116
- })),
117
- });
118
- });
119
-
120
- it('should pass the taskType setting', async () => {
121
- prepareBatchJsonResponse();
122
-
123
- await provider.embedding('gemini-embedding-001').doEmbed({
124
- values: testValues,
125
- providerOptions: { google: { taskType: 'SEMANTIC_SIMILARITY' } },
126
- });
127
-
128
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
129
- requests: testValues.map(value => ({
130
- model: 'models/gemini-embedding-001',
131
- content: { role: 'user', parts: [{ text: value }] },
132
- taskType: 'SEMANTIC_SIMILARITY',
133
- })),
134
- });
135
- });
136
-
137
- it('should pass headers', async () => {
138
- prepareBatchJsonResponse();
139
-
140
- const provider = createGoogleGenerativeAI({
141
- apiKey: 'test-api-key',
142
- headers: {
143
- 'Custom-Provider-Header': 'provider-header-value',
144
- },
145
- });
146
-
147
- await provider.embedding('gemini-embedding-001').doEmbed({
148
- values: testValues,
149
- headers: {
150
- 'Custom-Request-Header': 'request-header-value',
151
- },
152
- });
153
-
154
- expect(server.calls[0].requestHeaders).toStrictEqual({
155
- 'x-goog-api-key': 'test-api-key',
156
- 'content-type': 'application/json',
157
- 'custom-provider-header': 'provider-header-value',
158
- 'custom-request-header': 'request-header-value',
159
- });
160
- expect(server.calls[0].requestUserAgent).toContain(
161
- `ai-sdk/google/0.0.0-test`,
162
- );
163
- });
164
-
165
- it('should throw an error if too many values are provided', async () => {
166
- const model = new GoogleGenerativeAIEmbeddingModel('gemini-embedding-001', {
167
- provider: 'google.generative-ai',
168
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
169
- headers: () => ({}),
170
- });
171
-
172
- const tooManyValues = Array(2049).fill('test');
173
-
174
- await expect(model.doEmbed({ values: tooManyValues })).rejects.toThrow(
175
- 'Too many values for a single embedding call. The google.generative-ai model "gemini-embedding-001" can only embed up to 2048 values per call, but 2049 values were provided.',
176
- );
177
- });
178
-
179
- it('should use the batch embeddings endpoint', async () => {
180
- prepareBatchJsonResponse();
181
- const model = provider.embeddingModel('gemini-embedding-001');
182
- await model.doEmbed({
183
- values: testValues,
184
- });
185
-
186
- expect(server.calls[0].requestUrl).toBe(
187
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:batchEmbedContents',
188
- );
189
- });
190
-
191
- it('should use the single embeddings endpoint', async () => {
192
- prepareSingleJsonResponse();
193
-
194
- const model = provider.embeddingModel('gemini-embedding-001');
195
-
196
- await model.doEmbed({
197
- values: [testValues[0]],
198
- });
199
-
200
- expect(server.calls[0].requestUrl).toBe(
201
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent',
202
- );
203
- });
204
- });