@ai-sdk/google
Advanced tools
+9
-0
| # @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
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
3
-80%950813
-17.05%45
-18.18%10391
-39.06%+ Added
+ Added
- Removed
- Removed
Updated
Updated