Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

@ai-sdk/google

Package Overview
Dependencies
Maintainers
3
Versions
378
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ai-sdk/google - npm Package Compare versions

Comparing version
3.0.12
to
3.0.13
+9
-0
CHANGELOG.md
# @ai-sdk/google
## 3.0.13
### Patch Changes
- 4de5a1d: chore: excluded tests from src folder in npm package
- Updated dependencies [4de5a1d]
- @ai-sdk/provider@3.0.5
- @ai-sdk/provider-utils@4.0.9
## 3.0.12

@@ -4,0 +13,0 @@

+9
-5
{
"name": "@ai-sdk/google",
"version": "3.0.12",
"version": "3.0.13",
"license": "Apache-2.0",

@@ -13,2 +13,6 @@ "sideEffects": false,

"src",
"!src/**/*.test.ts",
"!src/**/*.test-d.ts",
"!src/**/__snapshots__",
"!src/**/__fixtures__",
"CHANGELOG.md",

@@ -36,4 +40,4 @@ "README.md",

"dependencies": {
"@ai-sdk/provider": "3.0.4",
"@ai-sdk/provider-utils": "4.0.8"
"@ai-sdk/provider": "3.0.5",
"@ai-sdk/provider-utils": "4.0.9"
},

@@ -45,4 +49,4 @@ "devDependencies": {

"zod": "3.25.76",
"@vercel/ai-tsconfig": "0.0.0",
"@ai-sdk/test-server": "1.0.2"
"@ai-sdk/test-server": "1.0.3",
"@vercel/ai-tsconfig": "0.0.0"
},

@@ -49,0 +53,0 @@ "peerDependencies": {

Sorry, the diff of this file is not supported yet

import { JSONSchema7 } from '@ai-sdk/provider';
import { convertJSONSchemaToOpenAPISchema } from './convert-json-schema-to-openapi-schema';
import { it, expect } from 'vitest';
it('should remove additionalProperties and $schema', () => {
const input: JSONSchema7 = {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
additionalProperties: false,
};
const expected = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should remove additionalProperties object from nested object schemas', function () {
const input: JSONSchema7 = {
type: 'object',
properties: {
keys: {
type: 'object',
additionalProperties: { type: 'string' },
description: 'Description for the key',
},
},
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
};
const expected = {
type: 'object',
properties: {
keys: {
type: 'object',
description: 'Description for the key',
},
},
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should handle nested objects and arrays', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
users: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
},
additionalProperties: false,
},
},
},
additionalProperties: false,
};
const expected = {
type: 'object',
properties: {
users: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
},
},
},
},
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should convert "const" to "enum" with a single value', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
status: { const: 'active' },
},
};
const expected = {
type: 'object',
properties: {
status: { enum: ['active'] },
},
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should handle allOf, anyOf, and oneOf', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
allOfProp: { allOf: [{ type: 'string' }, { minLength: 5 }] },
anyOfProp: { anyOf: [{ type: 'string' }, { type: 'number' }] },
oneOfProp: { oneOf: [{ type: 'boolean' }, { type: 'null' }] },
},
};
const expected = {
type: 'object',
properties: {
allOfProp: {
allOf: [{ type: 'string' }, { minLength: 5 }],
},
anyOfProp: {
anyOf: [{ type: 'string' }, { type: 'number' }],
},
oneOfProp: {
oneOf: [{ type: 'boolean' }, { type: 'null' }],
},
},
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should convert "format: date-time" to "format: date-time"', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
timestamp: { type: 'string', format: 'date-time' },
},
};
const expected = {
type: 'object',
properties: {
timestamp: { type: 'string', format: 'date-time' },
},
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should handle required properties', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
},
required: ['id'],
};
const expected = {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
},
required: ['id'],
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should convert deeply nested "const" to "enum"', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
nested: {
type: 'object',
properties: {
deeplyNested: {
anyOf: [
{
type: 'object',
properties: {
value: {
const: 'specific value',
},
},
},
{
type: 'string',
},
],
},
},
},
},
};
const expected = {
type: 'object',
properties: {
nested: {
type: 'object',
properties: {
deeplyNested: {
anyOf: [
{
type: 'object',
properties: {
value: {
enum: ['specific value'],
},
},
},
{
type: 'string',
},
],
},
},
},
},
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should correctly convert a complex schema with nested const and anyOf', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
name: {
type: 'string',
},
age: {
type: 'number',
},
contact: {
anyOf: [
{
type: 'object',
properties: {
type: {
type: 'string',
const: 'email',
},
value: {
type: 'string',
},
},
required: ['type', 'value'],
additionalProperties: false,
},
{
type: 'object',
properties: {
type: {
type: 'string',
const: 'phone',
},
value: {
type: 'string',
},
},
required: ['type', 'value'],
additionalProperties: false,
},
],
},
occupation: {
anyOf: [
{
type: 'object',
properties: {
type: {
type: 'string',
const: 'employed',
},
company: {
type: 'string',
},
position: {
type: 'string',
},
},
required: ['type', 'company', 'position'],
additionalProperties: false,
},
{
type: 'object',
properties: {
type: {
type: 'string',
const: 'student',
},
school: {
type: 'string',
},
grade: {
type: 'number',
},
},
required: ['type', 'school', 'grade'],
additionalProperties: false,
},
{
type: 'object',
properties: {
type: {
type: 'string',
const: 'unemployed',
},
},
required: ['type'],
additionalProperties: false,
},
],
},
},
required: ['name', 'age', 'contact', 'occupation'],
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
};
const expected = {
type: 'object',
properties: {
name: {
type: 'string',
},
age: {
type: 'number',
},
contact: {
anyOf: [
{
type: 'object',
properties: {
type: {
type: 'string',
enum: ['email'],
},
value: {
type: 'string',
},
},
required: ['type', 'value'],
},
{
type: 'object',
properties: {
type: {
type: 'string',
enum: ['phone'],
},
value: {
type: 'string',
},
},
required: ['type', 'value'],
},
],
},
occupation: {
anyOf: [
{
type: 'object',
properties: {
type: {
type: 'string',
enum: ['employed'],
},
company: {
type: 'string',
},
position: {
type: 'string',
},
},
required: ['type', 'company', 'position'],
},
{
type: 'object',
properties: {
type: {
type: 'string',
enum: ['student'],
},
school: {
type: 'string',
},
grade: {
type: 'number',
},
},
required: ['type', 'school', 'grade'],
},
{
type: 'object',
properties: {
type: {
type: 'string',
enum: ['unemployed'],
},
},
required: ['type'],
},
],
},
},
required: ['name', 'age', 'contact', 'occupation'],
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should handle null type correctly', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
nullableField: {
type: ['string', 'null'],
},
explicitNullField: {
type: 'null',
},
},
};
const expected = {
type: 'object',
properties: {
nullableField: {
anyOf: [{ type: 'string' }],
nullable: true,
},
explicitNullField: {
type: 'null',
},
},
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should handle descriptions', () => {
const input: JSONSchema7 = {
type: 'object',
description: 'A user object',
properties: {
id: {
type: 'number',
description: 'The user ID',
},
name: {
type: 'string',
description: "The user's full name",
},
email: {
type: 'string',
format: 'email',
description: "The user's email address",
},
},
required: ['id', 'name'],
};
const expected = {
type: 'object',
description: 'A user object',
properties: {
id: {
type: 'number',
description: 'The user ID',
},
name: {
type: 'string',
description: "The user's full name",
},
email: {
type: 'string',
format: 'email',
description: "The user's email address",
},
},
required: ['id', 'name'],
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should return undefined for empty object schemas at root level', () => {
const emptyObjectSchemas = [
{ type: 'object' },
{ type: 'object', properties: {} },
] as const;
emptyObjectSchemas.forEach(schema => {
expect(convertJSONSchemaToOpenAPISchema(schema)).toBeUndefined();
});
});
it('should preserve nested empty object schemas to avoid breaking required array validation', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
url: { type: 'string', description: 'URL to navigate to' },
launchOptions: {
type: 'object',
description: 'PuppeteerJS LaunchOptions',
},
allowDangerous: {
type: 'boolean',
description: 'Allow dangerous options',
},
},
required: ['url', 'launchOptions'],
};
const expected = {
type: 'object',
properties: {
url: { type: 'string', description: 'URL to navigate to' },
launchOptions: {
type: 'object',
description: 'PuppeteerJS LaunchOptions',
},
allowDangerous: {
type: 'boolean',
description: 'Allow dangerous options',
},
},
required: ['url', 'launchOptions'],
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should preserve nested empty object schemas without descriptions', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
options: { type: 'object' },
},
required: ['options'],
};
const expected = {
type: 'object',
properties: {
options: { type: 'object' },
},
required: ['options'],
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should handle non-empty object schemas', () => {
const nonEmptySchema: JSONSchema7 = {
type: 'object',
properties: {
name: { type: 'string' },
},
};
expect(convertJSONSchemaToOpenAPISchema(nonEmptySchema)).toEqual({
type: 'object',
properties: {
name: { type: 'string' },
},
});
});
it('should convert string enum properties', () => {
const schemaWithEnumProperty: JSONSchema7 = {
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['text', 'code', 'image'],
},
},
required: ['kind'],
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
};
expect(convertJSONSchemaToOpenAPISchema(schemaWithEnumProperty)).toEqual({
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['text', 'code', 'image'],
},
},
required: ['kind'],
});
});
it('should convert nullable string enum', () => {
const schemaWithEnumProperty: JSONSchema7 = {
type: 'object',
properties: {
fieldD: {
anyOf: [
{
type: 'string',
enum: ['a', 'b', 'c'],
},
{
type: 'null',
},
],
},
},
required: ['fieldD'],
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
};
expect(convertJSONSchemaToOpenAPISchema(schemaWithEnumProperty)).toEqual({
required: ['fieldD'],
type: 'object',
properties: {
fieldD: {
nullable: true,
type: 'string',
enum: ['a', 'b', 'c'],
},
},
});
});
it('should handle type arrays with multiple non-null types plus null', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
multiTypeField: {
type: ['string', 'number', 'null'],
},
},
};
const expected = {
type: 'object',
properties: {
multiTypeField: {
anyOf: [{ type: 'string' }, { type: 'number' }],
nullable: true,
},
},
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
it('should convert type arrays without null to anyOf', () => {
const input: JSONSchema7 = {
type: 'object',
properties: {
multiTypeField: {
type: ['string', 'number'],
},
},
};
const expected = {
type: 'object',
properties: {
multiTypeField: {
anyOf: [{ type: 'string' }, { type: 'number' }],
},
},
};
expect(convertJSONSchemaToOpenAPISchema(input)).toEqual(expected);
});
import { convertToGoogleGenerativeAIMessages } from './convert-to-google-generative-ai-messages';
import { describe, it, expect } from 'vitest';
describe('system messages', () => {
it('should store system message in system instruction', async () => {
const result = convertToGoogleGenerativeAIMessages([
{ role: 'system', content: 'Test' },
]);
expect(result).toEqual({
systemInstruction: { parts: [{ text: 'Test' }] },
contents: [],
});
});
it('should throw error when there was already a user message', async () => {
expect(() =>
convertToGoogleGenerativeAIMessages([
{ role: 'user', content: [{ type: 'text', text: 'Test' }] },
{ role: 'system', content: 'Test' },
]),
).toThrow(
'system messages are only supported at the beginning of the conversation',
);
});
});
describe('thought signatures', () => {
it('should preserve thought signatures in assistant messages', async () => {
const result = convertToGoogleGenerativeAIMessages([
{
role: 'assistant',
content: [
{
type: 'text',
text: 'Regular text',
providerOptions: { google: { thoughtSignature: 'sig1' } },
},
{
type: 'reasoning',
text: 'Reasoning text',
providerOptions: { google: { thoughtSignature: 'sig2' } },
},
{
type: 'tool-call',
toolCallId: 'call1',
toolName: 'test',
input: { value: 'test' },
providerOptions: { google: { thoughtSignature: 'sig3' } },
},
],
},
]);
expect(result).toMatchInlineSnapshot(`
{
"contents": [
{
"parts": [
{
"text": "Regular text",
"thoughtSignature": "sig1",
},
{
"text": "Reasoning text",
"thought": true,
"thoughtSignature": "sig2",
},
{
"functionCall": {
"args": {
"value": "test",
},
"name": "test",
},
"thoughtSignature": "sig3",
},
],
"role": "model",
},
],
"systemInstruction": undefined,
}
`);
});
});
describe('Gemma model system instructions', () => {
it('should prepend system instruction to first user message for Gemma models', async () => {
const result = convertToGoogleGenerativeAIMessages(
[
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: [{ type: 'text', text: 'Hello' }] },
],
{ isGemmaModel: true },
);
expect(result).toMatchInlineSnapshot(`
{
"contents": [
{
"parts": [
{
"text": "You are a helpful assistant.
",
},
{
"text": "Hello",
},
],
"role": "user",
},
],
"systemInstruction": undefined,
}
`);
});
it('should handle multiple system messages for Gemma models', async () => {
const result = convertToGoogleGenerativeAIMessages(
[
{ role: 'system', content: 'You are helpful.' },
{ role: 'system', content: 'Be concise.' },
{ role: 'user', content: [{ type: 'text', text: 'Hi' }] },
],
{ isGemmaModel: true },
);
expect(result).toMatchInlineSnapshot(`
{
"contents": [
{
"parts": [
{
"text": "You are helpful.
Be concise.
",
},
{
"text": "Hi",
},
],
"role": "user",
},
],
"systemInstruction": undefined,
}
`);
});
it('should not affect non-Gemma models', async () => {
const result = convertToGoogleGenerativeAIMessages(
[
{ role: 'system', content: 'You are helpful.' },
{ role: 'user', content: [{ type: 'text', text: 'Hello' }] },
],
{ isGemmaModel: false },
);
expect(result).toMatchInlineSnapshot(`
{
"contents": [
{
"parts": [
{
"text": "Hello",
},
],
"role": "user",
},
],
"systemInstruction": {
"parts": [
{
"text": "You are helpful.",
},
],
},
}
`);
});
it('should handle Gemma model with system instruction but no user messages', async () => {
const result = convertToGoogleGenerativeAIMessages(
[{ role: 'system', content: 'You are helpful.' }],
{ isGemmaModel: true },
);
expect(result).toMatchInlineSnapshot(`
{
"contents": [],
"systemInstruction": undefined,
}
`);
});
});
describe('user messages', () => {
it('should add image parts', async () => {
const result = convertToGoogleGenerativeAIMessages([
{
role: 'user',
content: [
{
type: 'file',
data: 'AAECAw==',
mediaType: 'image/png',
},
],
},
]);
expect(result).toEqual({
systemInstruction: undefined,
contents: [
{
role: 'user',
parts: [
{
inlineData: {
data: 'AAECAw==',
mimeType: 'image/png',
},
},
],
},
],
});
});
it('should add file parts for base64 encoded files', async () => {
const result = convertToGoogleGenerativeAIMessages([
{
role: 'user',
content: [{ type: 'file', data: 'AAECAw==', mediaType: 'image/png' }],
},
]);
expect(result).toEqual({
systemInstruction: undefined,
contents: [
{
role: 'user',
parts: [
{
inlineData: {
data: 'AAECAw==',
mimeType: 'image/png',
},
},
],
},
],
});
});
});
describe('tool messages', () => {
it('should convert tool result messages to function responses', async () => {
const result = convertToGoogleGenerativeAIMessages([
{
role: 'tool',
content: [
{
type: 'tool-result',
toolName: 'testFunction',
toolCallId: 'testCallId',
output: { type: 'json', value: { someData: 'test result' } },
},
],
},
]);
expect(result).toEqual({
systemInstruction: undefined,
contents: [
{
role: 'user',
parts: [
{
functionResponse: {
name: 'testFunction',
response: {
name: 'testFunction',
content: { someData: 'test result' },
},
},
},
],
},
],
});
});
});
describe('assistant messages', () => {
it('should add PNG image parts for base64 encoded files', async () => {
const result = convertToGoogleGenerativeAIMessages([
{
role: 'assistant',
content: [{ type: 'file', data: 'AAECAw==', mediaType: 'image/png' }],
},
]);
expect(result).toEqual({
systemInstruction: undefined,
contents: [
{
role: 'model',
parts: [
{
inlineData: {
data: 'AAECAw==',
mimeType: 'image/png',
},
},
],
},
],
});
});
it('should throw error for URL file data in assistant messages', async () => {
expect(() =>
convertToGoogleGenerativeAIMessages([
{
role: 'assistant',
content: [
{
type: 'file',
data: new URL('https://example.com/image.png'),
mediaType: 'image/png',
},
],
},
]),
).toThrow('File data URLs in assistant messages are not supported');
});
it('should convert tool result messages with content type (multipart with images)', async () => {
const result = convertToGoogleGenerativeAIMessages([
{
role: 'tool',
content: [
{
type: 'tool-result',
toolName: 'imageGenerator',
toolCallId: 'testCallId',
output: {
type: 'content',
value: [
{
type: 'text',
text: 'Here is the generated image:',
},
{
type: 'image-data',
data: 'base64encodedimagedata',
mediaType: 'image/jpeg',
},
],
},
},
],
},
]);
expect(result).toEqual({
systemInstruction: undefined,
contents: [
{
role: 'user',
parts: [
{
functionResponse: {
name: 'imageGenerator',
response: {
name: 'imageGenerator',
content: 'Here is the generated image:',
},
},
},
{
inlineData: {
mimeType: 'image/jpeg',
data: 'base64encodedimagedata',
},
},
{
text: 'Tool executed successfully and returned this image as a response',
},
],
},
],
});
});
});
describe('parallel tool calls', () => {
it('should include thought signature on functionCall when provided', async () => {
const result = convertToGoogleGenerativeAIMessages([
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'call1',
toolName: 'checkweather',
input: { city: 'paris' },
providerOptions: { google: { thoughtSignature: 'sig_parallel' } },
},
{
type: 'tool-call',
toolCallId: 'call2',
toolName: 'checkweather',
input: { city: 'london' },
},
],
},
]);
expect(result.contents[0].parts[0]).toEqual({
functionCall: {
args: { city: 'paris' },
name: 'checkweather',
},
thoughtSignature: 'sig_parallel',
});
expect(result.contents[0].parts[1]).toEqual({
functionCall: {
args: { city: 'london' },
name: 'checkweather',
},
thoughtSignature: undefined,
});
});
});
describe('tool results with thought signatures', () => {
it('should include thought signature on functionCall but not on functionResponse', async () => {
const result = convertToGoogleGenerativeAIMessages([
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'call1',
toolName: 'readdata',
input: { userId: '123' },
providerOptions: { google: { thoughtSignature: 'sig_original' } },
},
],
},
{
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: 'call1',
toolName: 'readdata',
output: {
type: 'error-text',
value: 'file not found',
},
providerOptions: { google: { thoughtSignature: 'sig_original' } },
},
],
},
]);
expect(result.contents[0].parts[0]).toEqual({
functionCall: {
args: { userId: '123' },
name: 'readdata',
},
thoughtSignature: 'sig_original',
});
expect(result.contents[1].parts[0]).toEqual({
functionResponse: {
name: 'readdata',
response: {
content: 'file not found',
name: 'readdata',
},
},
});
expect(result.contents[1].parts[0]).not.toHaveProperty('thoughtSignature');
});
});
import { getModelPath } from './get-model-path';
import { it, expect } from 'vitest';
it('should pass through model path for models/*', async () => {
expect(getModelPath('models/some-model')).toEqual('models/some-model');
});
it('should pass through model path for tunedModels/*', async () => {
expect(getModelPath('tunedModels/some-model')).toEqual(
'tunedModels/some-model',
);
});
it('should add model path prefix to models without slash', async () => {
expect(getModelPath('some-model')).toEqual('models/some-model');
});
import { EmbeddingModelV3Embedding } from '@ai-sdk/provider';
import { createTestServer } from '@ai-sdk/test-server/with-vitest';
import { GoogleGenerativeAIEmbeddingModel } from './google-generative-ai-embedding-model';
import { createGoogleGenerativeAI } from './google-provider';
import { describe, it, expect, vi } from 'vitest';
vi.mock('./version', () => ({
VERSION: '0.0.0-test',
}));
const dummyEmbeddings = [
[0.1, 0.2, 0.3, 0.4, 0.5],
[0.6, 0.7, 0.8, 0.9, 1.0],
];
const testValues = ['sunny day at the beach', 'rainy day in the city'];
const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key' });
const model = provider.embeddingModel('gemini-embedding-001');
const URL =
'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:something';
const server = createTestServer({
[URL]: {},
});
describe('GoogleGenerativeAIEmbeddingModel', () => {
function prepareBatchJsonResponse({
embeddings = dummyEmbeddings,
headers,
}: {
embeddings?: EmbeddingModelV3Embedding[];
headers?: Record<string, string>;
} = {}) {
server.urls[URL].response = {
type: 'json-value',
headers,
body: {
embeddings: embeddings.map(embedding => ({ values: embedding })),
},
};
}
function prepareSingleJsonResponse({
embeddings = dummyEmbeddings,
headers,
}: {
embeddings?: EmbeddingModelV3Embedding[];
headers?: Record<string, string>;
} = {}) {
server.urls[URL].response = {
type: 'json-value',
headers,
body: {
embedding: { values: embeddings[0] },
},
};
}
it('should extract embedding', async () => {
prepareBatchJsonResponse();
const { embeddings } = await model.doEmbed({ values: testValues });
expect(embeddings).toStrictEqual(dummyEmbeddings);
});
it('should expose the raw response', async () => {
prepareBatchJsonResponse({
headers: {
'test-header': 'test-value',
},
});
const { response } = await model.doEmbed({ values: testValues });
expect(response?.headers).toStrictEqual({
// default headers:
'content-length': '80',
'content-type': 'application/json',
// custom header
'test-header': 'test-value',
});
expect(response).toMatchSnapshot();
});
it('should pass the model and the values', async () => {
prepareBatchJsonResponse();
await model.doEmbed({ values: testValues });
expect(await server.calls[0].requestBodyJson).toStrictEqual({
requests: testValues.map(value => ({
model: 'models/gemini-embedding-001',
content: { role: 'user', parts: [{ text: value }] },
})),
});
});
it('should pass the outputDimensionality setting', async () => {
prepareBatchJsonResponse();
await provider.embedding('gemini-embedding-001').doEmbed({
values: testValues,
providerOptions: {
google: { outputDimensionality: 64 },
},
});
expect(await server.calls[0].requestBodyJson).toStrictEqual({
requests: testValues.map(value => ({
model: 'models/gemini-embedding-001',
content: { role: 'user', parts: [{ text: value }] },
outputDimensionality: 64,
})),
});
});
it('should pass the taskType setting', async () => {
prepareBatchJsonResponse();
await provider.embedding('gemini-embedding-001').doEmbed({
values: testValues,
providerOptions: { google: { taskType: 'SEMANTIC_SIMILARITY' } },
});
expect(await server.calls[0].requestBodyJson).toStrictEqual({
requests: testValues.map(value => ({
model: 'models/gemini-embedding-001',
content: { role: 'user', parts: [{ text: value }] },
taskType: 'SEMANTIC_SIMILARITY',
})),
});
});
it('should pass headers', async () => {
prepareBatchJsonResponse();
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
headers: {
'Custom-Provider-Header': 'provider-header-value',
},
});
await provider.embedding('gemini-embedding-001').doEmbed({
values: testValues,
headers: {
'Custom-Request-Header': 'request-header-value',
},
});
expect(server.calls[0].requestHeaders).toStrictEqual({
'x-goog-api-key': 'test-api-key',
'content-type': 'application/json',
'custom-provider-header': 'provider-header-value',
'custom-request-header': 'request-header-value',
});
expect(server.calls[0].requestUserAgent).toContain(
`ai-sdk/google/0.0.0-test`,
);
});
it('should throw an error if too many values are provided', async () => {
const model = new GoogleGenerativeAIEmbeddingModel('gemini-embedding-001', {
provider: 'google.generative-ai',
baseURL: 'https://generativelanguage.googleapis.com/v1beta',
headers: () => ({}),
});
const tooManyValues = Array(2049).fill('test');
await expect(model.doEmbed({ values: tooManyValues })).rejects.toThrow(
'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.',
);
});
it('should use the batch embeddings endpoint', async () => {
prepareBatchJsonResponse();
const model = provider.embeddingModel('gemini-embedding-001');
await model.doEmbed({
values: testValues,
});
expect(server.calls[0].requestUrl).toBe(
'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:batchEmbedContents',
);
});
it('should use the single embeddings endpoint', async () => {
prepareSingleJsonResponse();
const model = provider.embeddingModel('gemini-embedding-001');
await model.doEmbed({
values: [testValues[0]],
});
expect(server.calls[0].requestUrl).toBe(
'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent',
);
});
});
import { createTestServer } from '@ai-sdk/test-server/with-vitest';
import { GoogleGenerativeAIImageModel } from './google-generative-ai-image-model';
import { describe, it, expect } from 'vitest';
const prompt = 'A cute baby sea otter';
const model = new GoogleGenerativeAIImageModel(
'imagen-3.0-generate-002',
{},
{
provider: 'google.generative-ai',
baseURL: 'https://api.example.com/v1beta',
headers: () => ({ 'api-key': 'test-api-key' }),
},
);
const server = createTestServer({
'https://api.example.com/v1beta/models/imagen-3.0-generate-002:predict': {
response: {
type: 'json-value',
body: {
predictions: [
{ bytesBase64Encoded: 'base64-image-1' },
{ bytesBase64Encoded: 'base64-image-2' },
],
},
},
},
});
describe('GoogleGenerativeAIImageModel', () => {
describe('doGenerate', () => {
function prepareJsonResponse({
headers,
}: {
headers?: Record<string, string>;
} = {}) {
const url =
'https://api.example.com/v1beta/models/imagen-3.0-generate-002:predict';
server.urls[url].response = {
type: 'json-value',
headers,
body: {
predictions: [
{ bytesBase64Encoded: 'base64-image-1' },
{ bytesBase64Encoded: 'base64-image-2' },
],
},
};
}
it('should pass headers', async () => {
prepareJsonResponse();
const modelWithHeaders = new GoogleGenerativeAIImageModel(
'imagen-3.0-generate-002',
{},
{
provider: 'google.generative-ai',
baseURL: 'https://api.example.com/v1beta',
headers: () => ({
'Custom-Provider-Header': 'provider-header-value',
}),
},
);
await modelWithHeaders.doGenerate({
prompt,
files: undefined,
mask: undefined,
n: 2,
size: undefined,
aspectRatio: undefined,
seed: undefined,
providerOptions: {},
headers: {
'Custom-Request-Header': 'request-header-value',
},
});
expect(server.calls[0].requestHeaders).toStrictEqual({
'content-type': 'application/json',
'custom-provider-header': 'provider-header-value',
'custom-request-header': 'request-header-value',
});
});
it('should respect maxImagesPerCall setting', () => {
const customModel = new GoogleGenerativeAIImageModel(
'imagen-3.0-generate-002',
{ maxImagesPerCall: 2 },
{
provider: 'google.generative-ai',
baseURL: 'https://api.example.com/v1beta',
headers: () => ({ 'api-key': 'test-api-key' }),
},
);
expect(customModel.maxImagesPerCall).toBe(2);
});
it('should use default maxImagesPerCall when not specified', () => {
const defaultModel = new GoogleGenerativeAIImageModel(
'imagen-3.0-generate-002',
{},
{
provider: 'google.generative-ai',
baseURL: 'https://api.example.com/v1beta',
headers: () => ({ 'api-key': 'test-api-key' }),
},
);
expect(defaultModel.maxImagesPerCall).toBe(4);
});
it('should extract the generated images', async () => {
prepareJsonResponse();
const result = await model.doGenerate({
prompt,
files: undefined,
mask: undefined,
n: 2,
size: undefined,
aspectRatio: undefined,
seed: undefined,
providerOptions: {},
});
expect(result.images).toStrictEqual(['base64-image-1', 'base64-image-2']);
});
it('sends aspect ratio in the request', async () => {
prepareJsonResponse();
await model.doGenerate({
prompt: 'test prompt',
files: undefined,
mask: undefined,
n: 1,
size: undefined,
aspectRatio: '16:9',
seed: undefined,
providerOptions: {},
});
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
{
"instances": [
{
"prompt": "test prompt",
},
],
"parameters": {
"aspectRatio": "16:9",
"sampleCount": 1,
},
}
`);
});
it('should pass aspect ratio directly when specified', async () => {
prepareJsonResponse();
await model.doGenerate({
prompt: 'test prompt',
files: undefined,
mask: undefined,
n: 1,
size: undefined,
aspectRatio: '16:9',
seed: undefined,
providerOptions: {},
});
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
{
"instances": [
{
"prompt": "test prompt",
},
],
"parameters": {
"aspectRatio": "16:9",
"sampleCount": 1,
},
}
`);
});
it('should combine aspectRatio and provider options', async () => {
prepareJsonResponse();
await model.doGenerate({
prompt: 'test prompt',
files: undefined,
mask: undefined,
n: 1,
size: undefined,
aspectRatio: '1:1',
seed: undefined,
providerOptions: {
google: {
personGeneration: 'dont_allow',
},
},
});
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
{
"instances": [
{
"prompt": "test prompt",
},
],
"parameters": {
"aspectRatio": "1:1",
"personGeneration": "dont_allow",
"sampleCount": 1,
},
}
`);
});
it('should return warnings for unsupported settings', async () => {
prepareJsonResponse();
const result = await model.doGenerate({
prompt,
files: undefined,
mask: undefined,
n: 1,
size: '1024x1024',
aspectRatio: '1:1',
seed: 123,
providerOptions: {},
});
expect(result.warnings).toMatchInlineSnapshot(`
[
{
"details": "This model does not support the \`size\` option. Use \`aspectRatio\` instead.",
"feature": "size",
"type": "unsupported",
},
{
"details": "This model does not support the \`seed\` option through this provider.",
"feature": "seed",
"type": "unsupported",
},
]
`);
});
it('should include response data with timestamp, modelId and headers', async () => {
prepareJsonResponse({
headers: {
'request-id': 'test-request-id',
'x-goog-quota-remaining': '123',
},
});
const testDate = new Date('2024-03-15T12:00:00Z');
const customModel = new GoogleGenerativeAIImageModel(
'imagen-3.0-generate-002',
{},
{
provider: 'google.generative-ai',
baseURL: 'https://api.example.com/v1beta',
headers: () => ({ 'api-key': 'test-api-key' }),
_internal: {
currentDate: () => testDate,
},
},
);
const result = await customModel.doGenerate({
prompt,
files: undefined,
mask: undefined,
n: 1,
size: undefined,
aspectRatio: undefined,
seed: undefined,
providerOptions: {},
});
expect(result.response).toStrictEqual({
timestamp: testDate,
modelId: 'imagen-3.0-generate-002',
headers: {
'content-length': '97',
'content-type': 'application/json',
'request-id': 'test-request-id',
'x-goog-quota-remaining': '123',
},
});
});
it('should use real date when no custom date provider is specified', async () => {
prepareJsonResponse();
const beforeDate = new Date();
const result = await model.doGenerate({
prompt,
files: undefined,
mask: undefined,
n: 2,
size: undefined,
aspectRatio: undefined,
seed: undefined,
providerOptions: {},
});
const afterDate = new Date();
expect(result.response.timestamp.getTime()).toBeGreaterThanOrEqual(
beforeDate.getTime(),
);
expect(result.response.timestamp.getTime()).toBeLessThanOrEqual(
afterDate.getTime(),
);
expect(result.response.modelId).toBe('imagen-3.0-generate-002');
});
it('should only pass valid provider options', async () => {
prepareJsonResponse();
await model.doGenerate({
prompt,
files: undefined,
mask: undefined,
n: 2,
size: undefined,
aspectRatio: '16:9',
seed: undefined,
providerOptions: {
google: {
addWatermark: false,
personGeneration: 'allow_all',
foo: 'bar',
negativePrompt: 'negative prompt',
},
},
});
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
{
"instances": [
{
"prompt": "A cute baby sea otter",
},
],
"parameters": {
"aspectRatio": "16:9",
"personGeneration": "allow_all",
"sampleCount": 2,
},
}
`);
});
});
describe('Image Editing (Not Supported)', () => {
it('should throw error when files are provided', async () => {
await expect(
model.doGenerate({
prompt: 'Edit this image',
files: [
{
type: 'file',
data: 'base64-source-image',
mediaType: 'image/png',
},
],
mask: undefined,
n: 1,
size: undefined,
aspectRatio: undefined,
seed: undefined,
providerOptions: {},
}),
).rejects.toThrow(
'Google Generative AI does not support image editing. ' +
'Use Google Vertex AI (@ai-sdk/google-vertex) for image editing capabilities.',
);
});
it('should throw error when mask is provided', async () => {
await expect(
model.doGenerate({
prompt: 'Edit this image',
files: undefined,
mask: {
type: 'file',
data: 'base64-mask-image',
mediaType: 'image/png',
},
n: 1,
size: undefined,
aspectRatio: undefined,
seed: undefined,
providerOptions: {},
}),
).rejects.toThrow(
'Google Generative AI does not support image editing with masks. ' +
'Use Google Vertex AI (@ai-sdk/google-vertex) for image editing capabilities.',
);
});
});
});

Sorry, the diff of this file is too big to display

import { LanguageModelV3ProviderTool } from '@ai-sdk/provider';
import { expect, it } from 'vitest';
import { prepareTools } from './google-prepare-tools';
it('should return undefined tools and tool_choice when tools are null', () => {
const result = prepareTools({
tools: undefined,
modelId: 'gemini-2.5-flash',
});
expect(result).toEqual({
tools: undefined,
tool_choice: undefined,
toolWarnings: [],
});
});
it('should return undefined tools and tool_choice when tools are empty', () => {
const result = prepareTools({ tools: [], modelId: 'gemini-2.5-flash' });
expect(result).toEqual({
tools: undefined,
tool_choice: undefined,
toolWarnings: [],
});
});
it('should correctly prepare function tools', () => {
const result = prepareTools({
tools: [
{
type: 'function',
name: 'testFunction',
description: 'A test function',
inputSchema: { type: 'object', properties: {} },
},
],
modelId: 'gemini-2.5-flash',
});
expect(result.tools).toEqual([
{
functionDeclarations: [
{
name: 'testFunction',
description: 'A test function',
parameters: undefined,
},
],
},
]);
expect(result.toolConfig).toBeUndefined();
expect(result.toolWarnings).toEqual([]);
});
it('should correctly prepare provider-defined tools as array', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'google.google_search',
name: 'google_search',
args: {},
},
{
type: 'provider',
id: 'google.url_context',
name: 'url_context',
args: {},
},
{
type: 'provider',
id: 'google.file_search',
name: 'file_search',
args: { fileSearchStoreNames: ['projects/foo/fileSearchStores/bar'] },
},
],
modelId: 'gemini-2.5-flash',
});
expect(result.tools).toEqual([
{ googleSearch: {} },
{ urlContext: {} },
{
fileSearch: {
fileSearchStoreNames: ['projects/foo/fileSearchStores/bar'],
},
},
]);
expect(result.toolConfig).toBeUndefined();
expect(result.toolWarnings).toEqual([]);
});
it('should correctly prepare single provider-defined tool', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'google.google_search',
name: 'google_search',
args: {},
},
],
modelId: 'gemini-2.5-flash',
});
expect(result.tools).toEqual([{ googleSearch: {} }]);
expect(result.toolConfig).toBeUndefined();
expect(result.toolWarnings).toEqual([]);
});
it('should add warnings for unsupported tools', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'unsupported.tool',
name: 'unsupported_tool',
args: {},
},
],
modelId: 'gemini-2.5-flash',
});
expect(result.tools).toBeUndefined();
expect(result.toolConfig).toBeUndefined();
expect(result.toolWarnings).toMatchInlineSnapshot(`
[
{
"feature": "provider-defined tool unsupported.tool",
"type": "unsupported",
},
]
`);
});
it('should add warnings for file search on unsupported models', () => {
const tool: LanguageModelV3ProviderTool = {
type: 'provider' as const,
id: 'google.file_search',
name: 'file_search',
args: { fileSearchStoreNames: ['projects/foo/fileSearchStores/bar'] },
};
const result = prepareTools({
tools: [tool],
modelId: 'gemini-1.5-flash-8b',
});
expect(result.tools).toBeUndefined();
expect(result.toolWarnings).toMatchInlineSnapshot(`
[
{
"details": "The file search tool is only supported with Gemini 2.5 models and Gemini 3 models.",
"feature": "provider-defined tool google.file_search",
"type": "unsupported",
},
]
`);
});
it('should correctly prepare file search tool for gemini-2.5 models', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'google.file_search',
name: 'file_search',
args: {
fileSearchStoreNames: ['projects/foo/fileSearchStores/bar'],
metadataFilter: 'author=Robert Graves',
topK: 5,
},
},
],
modelId: 'gemini-2.5-pro',
});
expect(result.tools).toEqual([
{
fileSearch: {
fileSearchStoreNames: ['projects/foo/fileSearchStores/bar'],
metadataFilter: 'author=Robert Graves',
topK: 5,
},
},
]);
expect(result.toolWarnings).toEqual([]);
});
it('should correctly prepare file search tool for gemini-3 models', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'google.file_search',
name: 'file_search',
args: {
fileSearchStoreNames: ['projects/foo/fileSearchStores/bar'],
metadataFilter: 'author=Robert Graves',
topK: 5,
},
},
],
modelId: 'gemini-3-pro-preview',
});
expect(result.tools).toEqual([
{
fileSearch: {
fileSearchStoreNames: ['projects/foo/fileSearchStores/bar'],
metadataFilter: 'author=Robert Graves',
topK: 5,
},
},
]);
expect(result.toolWarnings).toEqual([]);
});
it('should handle tool choice "auto"', () => {
const result = prepareTools({
tools: [
{
type: 'function',
name: 'testFunction',
description: 'Test',
inputSchema: {},
},
],
toolChoice: { type: 'auto' },
modelId: 'gemini-2.5-flash',
});
expect(result.toolConfig).toEqual({
functionCallingConfig: { mode: 'AUTO' },
});
});
it('should handle tool choice "required"', () => {
const result = prepareTools({
tools: [
{
type: 'function',
name: 'testFunction',
description: 'Test',
inputSchema: {},
},
],
toolChoice: { type: 'required' },
modelId: 'gemini-2.5-flash',
});
expect(result.toolConfig).toEqual({
functionCallingConfig: { mode: 'ANY' },
});
});
it('should handle tool choice "none"', () => {
const result = prepareTools({
tools: [
{
type: 'function',
name: 'testFunction',
description: 'Test',
inputSchema: {},
},
],
toolChoice: { type: 'none' },
modelId: 'gemini-2.5-flash',
});
expect(result.tools).toEqual([
{
functionDeclarations: [
{
name: 'testFunction',
description: 'Test',
parameters: {},
},
],
},
]);
expect(result.toolConfig).toEqual({
functionCallingConfig: { mode: 'NONE' },
});
});
it('should handle tool choice "tool"', () => {
const result = prepareTools({
tools: [
{
type: 'function',
name: 'testFunction',
description: 'Test',
inputSchema: {},
},
],
toolChoice: { type: 'tool', toolName: 'testFunction' },
modelId: 'gemini-2.5-flash',
});
expect(result.toolConfig).toEqual({
functionCallingConfig: {
mode: 'ANY',
allowedFunctionNames: ['testFunction'],
},
});
});
it('should warn when mixing function and provider-defined tools', () => {
const result = prepareTools({
tools: [
{
type: 'function',
name: 'testFunction',
description: 'A test function',
inputSchema: { type: 'object', properties: {} },
},
{
type: 'provider',
id: 'google.google_search',
name: 'google_search',
args: {},
},
],
modelId: 'gemini-2.5-flash',
});
expect(result.tools).toEqual([{ googleSearch: {} }]);
expect(result.toolWarnings).toMatchInlineSnapshot(`
[
{
"feature": "combination of function and provider-defined tools",
"type": "unsupported",
},
]
`);
expect(result.toolConfig).toBeUndefined();
});
it('should handle tool choice with mixed tools (provider-defined tools only)', () => {
const result = prepareTools({
tools: [
{
type: 'function',
name: 'testFunction',
description: 'A test function',
inputSchema: { type: 'object', properties: {} },
},
{
type: 'provider',
id: 'google.google_search',
name: 'google_search',
args: {},
},
],
toolChoice: { type: 'auto' },
modelId: 'gemini-2.5-flash',
});
expect(result.tools).toEqual([{ googleSearch: {} }]);
expect(result.toolConfig).toEqual(undefined);
expect(result.toolWarnings).toMatchInlineSnapshot(`
[
{
"feature": "combination of function and provider-defined tools",
"type": "unsupported",
},
]
`);
});
it('should handle latest modelId for provider-defined tools correctly', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'google.google_search',
name: 'google_search',
args: {},
},
],
modelId: 'gemini-flash-latest',
});
expect(result.tools).toEqual([{ googleSearch: {} }]);
expect(result.toolConfig).toBeUndefined();
expect(result.toolWarnings).toEqual([]);
});
it('should handle gemini-3 modelId for provider-defined tools correctly', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'google.google_search',
name: 'google_search',
args: {},
},
],
modelId: 'gemini-3-pro-preview',
});
expect(result.tools).toEqual([{ googleSearch: {} }]);
expect(result.toolConfig).toBeUndefined();
expect(result.toolWarnings).toEqual([]);
});
it('should handle code execution tool', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'google.code_execution',
name: 'code_execution',
args: {},
},
],
modelId: 'gemini-2.5-flash',
});
expect(result.tools).toEqual([{ codeExecution: {} }]);
expect(result.toolConfig).toBeUndefined();
expect(result.toolWarnings).toEqual([]);
});
it('should handle url context tool alone', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'google.url_context',
name: 'url_context',
args: {},
},
],
modelId: 'gemini-2.5-flash',
});
expect(result.tools).toEqual([{ urlContext: {} }]);
expect(result.toolConfig).toBeUndefined();
expect(result.toolWarnings).toEqual([]);
});
it('should handle google maps tool', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'google.google_maps',
name: 'google_maps',
args: {},
},
],
modelId: 'gemini-2.5-flash',
});
expect(result.tools).toEqual([{ googleMaps: {} }]);
expect(result.toolConfig).toBeUndefined();
expect(result.toolWarnings).toEqual([]);
});
it('should add warnings for google maps on unsupported models', () => {
const result = prepareTools({
tools: [
{
type: 'provider',
id: 'google.google_maps',
name: 'google_maps',
args: {},
},
],
modelId: 'gemini-1.5-flash',
});
expect(result.tools).toBeUndefined();
expect(result.toolWarnings).toMatchInlineSnapshot(`
[
{
"details": "The Google Maps grounding tool is not supported with Gemini models other than Gemini 2 or newer.",
"feature": "provider-defined tool google.google_maps",
"type": "unsupported",
},
]
`);
});
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createGoogleGenerativeAI } from './google-provider';
import { GoogleGenerativeAILanguageModel } from './google-generative-ai-language-model';
import { GoogleGenerativeAIEmbeddingModel } from './google-generative-ai-embedding-model';
import { GoogleGenerativeAIImageModel } from './google-generative-ai-image-model';
// Mock the imported modules using a partial mock to preserve original exports
vi.mock('@ai-sdk/provider-utils', async importOriginal => {
const mod = await importOriginal<typeof import('@ai-sdk/provider-utils')>();
return {
...mod,
loadApiKey: vi.fn().mockImplementation(({ apiKey }) => apiKey),
generateId: vi.fn().mockReturnValue('mock-id'),
withoutTrailingSlash: vi.fn().mockImplementation(url => url),
};
});
vi.mock('./google-generative-ai-language-model', () => ({
GoogleGenerativeAILanguageModel: vi.fn(),
}));
vi.mock('./google-generative-ai-embedding-model', () => ({
GoogleGenerativeAIEmbeddingModel: vi.fn(),
}));
vi.mock('./google-generative-ai-image-model', () => ({
GoogleGenerativeAIImageModel: vi.fn(),
}));
vi.mock('./version', () => ({
VERSION: '0.0.0-test',
}));
describe('google-provider', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should create a language model with default settings', () => {
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
});
provider('gemini-pro');
expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith(
'gemini-pro',
expect.objectContaining({
provider: 'google.generative-ai',
baseURL: 'https://generativelanguage.googleapis.com/v1beta',
headers: expect.any(Function),
generateId: expect.any(Function),
supportedUrls: expect.any(Function),
}),
);
});
it('should throw an error when using new keyword', () => {
const provider = createGoogleGenerativeAI({ apiKey: 'test-api-key' });
expect(() => new (provider as any)('gemini-pro')).toThrow(
'The Google Generative AI model function cannot be called with the new keyword.',
);
});
it('should create an embedding model with correct settings', () => {
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
});
provider.embeddingModel('embedding-001');
expect(GoogleGenerativeAIEmbeddingModel).toHaveBeenCalledWith(
'embedding-001',
expect.objectContaining({
provider: 'google.generative-ai',
headers: expect.any(Function),
baseURL: 'https://generativelanguage.googleapis.com/v1beta',
}),
);
});
it('should pass custom headers to the model constructor', () => {
const customHeaders = { 'Custom-Header': 'custom-value' };
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
headers: customHeaders,
});
provider('gemini-pro');
expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
headers: expect.any(Function),
}),
);
const options = (GoogleGenerativeAILanguageModel as any).mock.calls[0][1];
const headers = options.headers();
expect(headers).toEqual({
'x-goog-api-key': 'test-api-key',
'custom-header': 'custom-value',
'user-agent': 'ai-sdk/google/0.0.0-test',
});
});
it('should pass custom generateId function to the model constructor', () => {
const customGenerateId = () => 'custom-id';
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
generateId: customGenerateId,
});
provider('gemini-pro');
expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
generateId: customGenerateId,
}),
);
});
it('should use chat method to create a model', () => {
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
});
provider.chat('gemini-pro');
expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith(
'gemini-pro',
expect.any(Object),
);
});
it('should use custom baseURL when provided', () => {
const customBaseURL = 'https://custom-endpoint.example.com';
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
baseURL: customBaseURL,
});
provider('gemini-pro');
expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith(
'gemini-pro',
expect.objectContaining({
baseURL: customBaseURL,
}),
);
});
it('should create an image model with default settings', () => {
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
});
provider.image('imagen-3.0-generate-002');
expect(GoogleGenerativeAIImageModel).toHaveBeenCalledWith(
'imagen-3.0-generate-002',
{},
expect.objectContaining({
provider: 'google.generative-ai',
headers: expect.any(Function),
baseURL: 'https://generativelanguage.googleapis.com/v1beta',
}),
);
});
it('should create an image model with custom maxImagesPerCall', () => {
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
});
const imageSettings = {
maxImagesPerCall: 3,
};
provider.image('imagen-3.0-generate-002', imageSettings);
expect(GoogleGenerativeAIImageModel).toHaveBeenCalledWith(
'imagen-3.0-generate-002',
imageSettings,
expect.objectContaining({
provider: 'google.generative-ai',
headers: expect.any(Function),
baseURL: 'https://generativelanguage.googleapis.com/v1beta',
}),
);
});
it('should support deprecated methods', () => {
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
});
provider.generativeAI('gemini-pro');
provider.embedding('embedding-001');
provider.embeddingModel('embedding-001');
expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledTimes(1);
expect(GoogleGenerativeAIEmbeddingModel).toHaveBeenCalledTimes(2);
});
it('should include YouTube URLs in supportedUrls', () => {
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
});
provider('gemini-pro');
const call = vi.mocked(GoogleGenerativeAILanguageModel).mock.calls[0];
const supportedUrlsFunction = call[1].supportedUrls;
expect(supportedUrlsFunction).toBeDefined();
const supportedUrls = supportedUrlsFunction!() as Record<string, RegExp[]>;
const patterns = supportedUrls['*'];
expect(patterns).toBeDefined();
expect(Array.isArray(patterns)).toBe(true);
const testResults = {
supportedUrls: [
'https://generativelanguage.googleapis.com/v1beta/files/test123',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://youtube.com/watch?v=dQw4w9WgXcQ',
'https://youtu.be/dQw4w9WgXcQ',
].map(url => ({
url,
isSupported: patterns.some((pattern: RegExp) => pattern.test(url)),
})),
unsupportedUrls: [
'https://example.com',
'https://vimeo.com/123456789',
'https://youtube.com/channel/UCdQw4w9WgXcQ',
].map(url => ({
url,
isSupported: patterns.some((pattern: RegExp) => pattern.test(url)),
})),
};
expect(testResults).toMatchInlineSnapshot(`
{
"supportedUrls": [
{
"isSupported": true,
"url": "https://generativelanguage.googleapis.com/v1beta/files/test123",
},
{
"isSupported": true,
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
},
{
"isSupported": true,
"url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
},
{
"isSupported": true,
"url": "https://youtu.be/dQw4w9WgXcQ",
},
],
"unsupportedUrls": [
{
"isSupported": false,
"url": "https://example.com",
},
{
"isSupported": false,
"url": "https://vimeo.com/123456789",
},
{
"isSupported": false,
"url": "https://youtube.com/channel/UCdQw4w9WgXcQ",
},
],
}
`);
});
});
describe('google provider - custom provider name', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should use custom provider name when specified', () => {
const provider = createGoogleGenerativeAI({
name: 'my-gemini-proxy',
apiKey: 'test-api-key',
});
provider('gemini-pro');
expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith(
'gemini-pro',
expect.objectContaining({
provider: 'my-gemini-proxy',
}),
);
});
it('should default to google.generative-ai when name not specified', () => {
const provider = createGoogleGenerativeAI({
apiKey: 'test-api-key',
});
provider('gemini-pro');
expect(GoogleGenerativeAILanguageModel).toHaveBeenCalledWith(
'gemini-pro',
expect.objectContaining({
provider: 'google.generative-ai',
}),
);
});
});
import { isSupportedFileUrl } from './google-supported-file-url';
import { it, expect } from 'vitest';
it('should return true for valid Google generative language file URLs', () => {
const validUrl = new URL(
'https://generativelanguage.googleapis.com/v1beta/files/00000000-00000000-00000000-00000000',
);
expect(isSupportedFileUrl(validUrl)).toBe(true);
const simpleValidUrl = new URL(
'https://generativelanguage.googleapis.com/v1beta/files/test123',
);
expect(isSupportedFileUrl(simpleValidUrl)).toBe(true);
});
it('should return true for valid YouTube URLs', () => {
const validYouTubeUrls = [
new URL('https://www.youtube.com/watch?v=dQw4w9WgXcQ'),
new URL('https://youtube.com/watch?v=dQw4w9WgXcQ'),
new URL('https://youtu.be/dQw4w9WgXcQ'),
new URL('https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be'),
new URL('https://youtu.be/dQw4w9WgXcQ?t=42'),
];
validYouTubeUrls.forEach(url => {
expect(isSupportedFileUrl(url)).toBe(true);
});
});
it('should return false for invalid YouTube URLs', () => {
const invalidYouTubeUrls = [
new URL('https://youtube.com/channel/UCdQw4w9WgXcQ'),
new URL('https://youtube.com/playlist?list=PLdQw4w9WgXcQ'),
new URL('https://m.youtube.com/watch?v=dQw4w9WgXcQ'),
new URL('http://youtube.com/watch?v=dQw4w9WgXcQ'),
new URL('https://vimeo.com/123456789'),
];
invalidYouTubeUrls.forEach(url => {
expect(isSupportedFileUrl(url)).toBe(false);
});
});
it('should return false for non-Google generative language file URLs', () => {
const testCases = [
new URL('https://example.com'),
new URL('https://example.com/foo/bar'),
new URL('https://generativelanguage.googleapis.com'),
new URL('https://generativelanguage.googleapis.com/v1/other'),
new URL('http://generativelanguage.googleapis.com/v1beta/files/test'),
new URL('https://api.googleapis.com/v1beta/files/test'),
];
testCases.forEach(url => {
expect(isSupportedFileUrl(url)).toBe(false);
});
});

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display