diff --git a/lerna.json b/lerna.json index d096f720..5aaba2ae 100644 --- a/lerna.json +++ b/lerna.json @@ -1,9 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "version": "0.25.0", - "packages": [ - "plugins/*", - "docs", - "examples/*" - ] -} \ No newline at end of file + "packages": ["plugins/*", "docs", "examples/*"] +} diff --git a/package-lock.json b/package-lock.json index 5e9da07c..44d15fbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5605,19 +5605,19 @@ } }, "node_modules/@genkit-ai/firebase": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@genkit-ai/firebase/-/firebase-1.18.0.tgz", - "integrity": "sha512-UJVR2Z8839ptPqO+I7WICALlOZdDl4RbYDEaPjK8FpQmyJBjdpToevi6c2MoAXg8wTEd68i1b0cGPKXk9HPTyg==", + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@genkit-ai/firebase/-/firebase-1.19.3.tgz", + "integrity": "sha512-qDEjnl9qAgPJO9uQvH+ZHuiedgfmtJMtKPpvjn0swYpGpXfhG623uikwfvkHgbwotX8GrnHO0V3vFgopb2dxqg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@genkit-ai/google-cloud": "^1.18.0" + "@genkit-ai/google-cloud": "^1.19.3" }, "peerDependencies": { "@google-cloud/firestore": "^7.11.0", "firebase": ">=11.5.0", "firebase-admin": ">=12.2", - "genkit": "^1.18.0" + "genkit": "^1.19.3" }, "peerDependenciesMeta": { "firebase": { @@ -5658,9 +5658,9 @@ } }, "node_modules/@genkit-ai/google-cloud": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@genkit-ai/google-cloud/-/google-cloud-1.18.0.tgz", - "integrity": "sha512-gVKYmtDl/QiWnOq2qWnzY4SsaP1h1rS2HmHibjM1DHc9qNfBOk1dkGL4shmX7N4R/fcFWFLt7nXQiPJKu6NngQ==", + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@genkit-ai/google-cloud/-/google-cloud-1.19.3.tgz", + "integrity": "sha512-Vwe9sfm1EuGKZA8CMFezqmOwxXve8pd5NaXXHl1OOUXr3KEAgQJOu/3nR/ZNRbQsC96GPaOF8dejQnWZCRAPZQ==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -5683,7 +5683,7 @@ "winston": "^3.12.0" }, "peerDependencies": { - "genkit": "^1.18.0" + "genkit": "^1.19.3" } }, "node_modules/@genkit-ai/google-cloud/node_modules/@opentelemetry/core": { @@ -18487,13 +18487,13 @@ } }, "node_modules/genkit": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/genkit/-/genkit-1.18.0.tgz", - "integrity": "sha512-vu7i25e6f8YV3j4UfxPER2QXWVqY/kpYlw3sexuGOS60a0md/pwlQlIhg++cPAPlvvAb9eQ7ZxlFlUtrHJaiRQ==", + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/genkit/-/genkit-1.19.3.tgz", + "integrity": "sha512-v82Nm3Ba5AwsU5qqXjvDY5C7uzCLiRoaeC80q9jl/Y7SFH/fQv6jHkyuoWRVkz3cLtFu7x7oumHH2DVfk7gNLQ==", "license": "Apache-2.0", "dependencies": { - "@genkit-ai/ai": "1.18.0", - "@genkit-ai/core": "1.18.0", + "@genkit-ai/ai": "1.19.3", + "@genkit-ai/core": "1.19.3", "uuid": "^10.0.0" } }, @@ -18506,12 +18506,12 @@ "link": true }, "node_modules/genkit/node_modules/@genkit-ai/ai": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@genkit-ai/ai/-/ai-1.18.0.tgz", - "integrity": "sha512-/rkMAtn3voQ3MhvGult863mrDRf0p86koyYk5Rfh8DSPffy/ir2QVzT1UCmNP/VaXEZig9+vfKh//8COsygUVg==", + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@genkit-ai/ai/-/ai-1.19.3.tgz", + "integrity": "sha512-a7ggeKV+0CCY/G68Du4Oc36KOnAC3cZ9Eirxc+AGndiRigTOWmQk5UNT/3GItmmaqtTjfPf+YmkuCNWUJ1a1fA==", "license": "Apache-2.0", "dependencies": { - "@genkit-ai/core": "1.18.0", + "@genkit-ai/core": "1.19.3", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.11.19", "colorette": "^2.0.20", @@ -18524,9 +18524,9 @@ } }, "node_modules/genkit/node_modules/@genkit-ai/core": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@genkit-ai/core/-/core-1.18.0.tgz", - "integrity": "sha512-meuc5A4cJWI+vMXeaELLRKDx+vytyvVvBSjD6pz6tD66j5H6I25Y9iGmM/UIAHqbrarM5Kl4bKLI8MtD+QSXdA==", + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@genkit-ai/core/-/core-1.19.3.tgz", + "integrity": "sha512-nnMPMFftaSbFegyEn5RzbTRrJvvHhBO0nNhp3TJQtlmVkCcFMZxSEWOiJ0YsTQLFCaqga7dzjeHhDcA1VTqwJw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -36925,7 +36925,7 @@ "typescript": "^4.9.5" }, "peerDependencies": { - "genkit": "^0.9.0 || ^1.0.0" + "genkit": "^1.19.3" } }, "plugins/groq/node_modules/typescript": { diff --git a/plugins/groq/.gitignore b/plugins/groq/.gitignore index 46f10721..160d9303 100644 --- a/plugins/groq/.gitignore +++ b/plugins/groq/.gitignore @@ -1,2 +1,3 @@ lib/ -node_modules/ \ No newline at end of file +node_modules/ +.env \ No newline at end of file diff --git a/plugins/groq/jest.config.js b/plugins/groq/jest.config.js new file mode 100644 index 00000000..d0fe51d6 --- /dev/null +++ b/plugins/groq/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/node_modules/', '/lib/'], +}; diff --git a/plugins/groq/package.json b/plugins/groq/package.json index 0676ce51..c8b472af 100644 --- a/plugins/groq/package.json +++ b/plugins/groq/package.json @@ -27,14 +27,16 @@ "groq-sdk": "^0.19.0" }, "peerDependencies": { - "genkit": "^0.9.0 || ^1.0.0" + "genkit": "^1.19.3" }, "devDependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "@types/node": "^20.11.16", + "jest": "^30.2.0", "npm-run-all": "^4.1.5", "rimraf": "^6.0.1", + "ts-jest": "^29.1.0", "tsup": "^8.0.2", "tsx": "^4.7.0", "typescript": "^4.9.5" @@ -61,6 +63,6 @@ "build:clean": "rimraf ./lib", "build": "npm-run-all build:clean check compile", "build:watch": "tsup-node --watch", - "test": "tsx \"./tests/\"" + "test": "jest" } } diff --git a/plugins/groq/src/groq_models.ts b/plugins/groq/src/groq_models.ts index 7f7503de..02975d23 100644 --- a/plugins/groq/src/groq_models.ts +++ b/plugins/groq/src/groq_models.ts @@ -25,7 +25,6 @@ import { ChatCompletion } from 'groq-sdk/resources/chat/index.mjs'; import { GenerateRequest, GenerationCommonConfigSchema, - Genkit, Message, MessageData, Part, @@ -40,6 +39,7 @@ import { modelRef, ToolDefinition, } from 'genkit/model'; +import { model } from 'genkit/plugin'; export const GroqConfigSchema = GenerationCommonConfigSchema.extend({ stream: z.boolean().optional(), @@ -572,29 +572,29 @@ export function toGroqRequestBody( } /** - * Defines a Groq model. + * Creates a Groq model action. * * @param name - The name of the model. * @param client - The Groq client. - * @returns The model. + * @returns The model action. */ -export function groqModel(ai: Genkit, name: string, client: Groq) { - const model = SUPPORTED_GROQ_MODELS[name]; - if (!model) throw new Error(`Unsupported model: ${name}`); - const modelId = `groq/${name}`; +export function createGroqModel(name: string, client: Groq) { + const modelRef = SUPPORTED_GROQ_MODELS[name]; + if (!modelRef) throw new Error(`Unsupported model: ${name}`); + const modelId = `${name}`; - return ai.defineModel( + return model( { name: modelId, - ...model.info, - configSchema: model.configSchema, + ...modelRef.info, + configSchema: modelRef.configSchema, }, - async ( - request, - streamingCallback?: StreamingCallback - ) => { + async (request, options: any) => { let response: ChatCompletion; const body = toGroqRequestBody(name, request); + const streamingCallback: + | StreamingCallback + | undefined = options?.streamingCallback; if (streamingCallback) { if (request.output?.format === 'json') { throw new Error( diff --git a/plugins/groq/src/index.ts b/plugins/groq/src/index.ts index c57e2b38..d3b59c99 100644 --- a/plugins/groq/src/index.ts +++ b/plugins/groq/src/index.ts @@ -17,7 +17,7 @@ // Import necessary types and functions for Groq SDK integration. import Groq from 'groq-sdk'; import { - groqModel, + createGroqModel, llama3x70b, llama3x8b, llamaGuard3x8b, @@ -32,8 +32,7 @@ import { deepseekR1DistillLlamax70b, SUPPORTED_GROQ_MODELS, } from './groq_models'; -import { Genkit } from 'genkit'; -import { genkitPlugin } from 'genkit/plugin'; +import { genkitPluginV2 } from 'genkit/plugin'; // Export models for direct access export { @@ -92,28 +91,45 @@ export interface PluginOptions { * @param options - Optional configuration settings for the plugin. * @returns An object containing the models initialized with the Groq client. */ -export const groq = (options?: PluginOptions) => - genkitPlugin('groq', async (ai: Genkit) => { - const apiKey = options?.apiKey || process.env.GROQ_API_KEY; - if (!apiKey) { - throw new Error( - 'Please provide the API key or set the GROQ_API_KEY environment variable' - ); - } +export const groq = (options?: PluginOptions) => { + const apiKey = options?.apiKey || process.env.GROQ_API_KEY; + if (!apiKey) { + throw new Error( + 'Please provide the API key or set the GROQ_API_KEY environment variable' + ); + } - // Initialize Groq client - const client = new Groq({ - baseURL: options?.baseURL || process.env.GROQ_BASE_URL, // Optional base URL with environment variable fallback - apiKey, // API key retrieved from options or environment - timeout: options?.timeout, // Optional timeout - maxRetries: options?.maxRetries, // Optional max retries - }); + const client = new Groq({ + baseURL: options?.baseURL || process.env.GROQ_BASE_URL, + apiKey, + timeout: options?.timeout, + maxRetries: options?.maxRetries, + }); - // Register each model with the Genkit instance - for (const name of Object.keys(SUPPORTED_GROQ_MODELS)) { - groqModel(ai, name, client); - } + return genkitPluginV2({ + name: 'groq', + init: async () => { + const models = Object.keys(SUPPORTED_GROQ_MODELS).map((name) => + createGroqModel(name, client) + ); + + return models; + }, + resolve: async (actionType, actionName) => { + if (actionType === 'model') { + return createGroqModel(actionName, client); + } + return undefined; + }, + list: async () => { + return Object.keys(SUPPORTED_GROQ_MODELS).map((name) => ({ + name, + namespace: 'groq', + type: 'model' as const, + info: SUPPORTED_GROQ_MODELS[name].info, + })); + }, }); +}; -// Default export for plugin usage export default groq; diff --git a/plugins/groq/tests/groq_test.ts b/plugins/groq/tests/groq.test.ts similarity index 54% rename from plugins/groq/tests/groq_test.ts rename to plugins/groq/tests/groq.test.ts index 00f3eb04..5e0f2346 100644 --- a/plugins/groq/tests/groq_test.ts +++ b/plugins/groq/tests/groq.test.ts @@ -1,5 +1,3 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; import { GenerateRequest, MessageData, Role } from 'genkit'; import { toGroqRequestBody, @@ -7,28 +5,29 @@ import { toGroqMessages, } from '../src/groq_models'; import { ChatCompletionCreateParamsBase } from 'groq-sdk/resources/chat/completions.mjs'; +import { groq } from '../src/index'; describe('toGroqRole', () => { it('should convert user role correctly', () => { - assert.strictEqual(toGroqRole('user'), 'user'); + expect(toGroqRole('user')).toBe('user'); }); it('should convert model role to assistant', () => { - assert.strictEqual(toGroqRole('model'), 'assistant'); + expect(toGroqRole('model')).toBe('assistant'); }); it('should convert system role correctly', () => { - assert.strictEqual(toGroqRole('system'), 'system'); + expect(toGroqRole('system')).toBe('system'); }); it('should convert tool role correctly', () => { - assert.strictEqual(toGroqRole('tool'), 'assistant'); + expect(toGroqRole('tool')).toBe('assistant'); }); it('should throw error for unsupported roles', () => { - assert.throws(() => toGroqRole('unknown' as Role), { - message: "role unknown doesn't map to a Groq role.", - }); + expect(() => toGroqRole('unknown' as Role)).toThrow( + "role unknown doesn't map to a Groq role." + ); }); }); @@ -43,7 +42,7 @@ describe('toGroqMessages', () => { { role: 'user', content: 'Hello, world!' }, { role: 'assistant', content: 'How can I assist you today?' }, ]; - assert.deepStrictEqual(toGroqMessages(messages), expectedOutput); + expect(toGroqMessages(messages)).toEqual(expectedOutput); }); }); @@ -84,16 +83,56 @@ describe('toGroqRequestBody', () => { }; const actualOutput = toGroqRequestBody('llama-3-8b', request); - console.log(`actualOutput.stop: ${actualOutput.stop}`); - assert.deepStrictEqual( - JSON.parse(JSON.stringify(actualOutput)), // Remove undefined fields - JSON.parse(JSON.stringify(expectedOutput)) - ); + expect( + JSON.parse(JSON.stringify(actualOutput)) // Remove undefined fields + ).toEqual(JSON.parse(JSON.stringify(expectedOutput))); }); it('should handle unsupported models', () => { - assert.throws(() => toGroqRequestBody('unsupported-model', request), { - message: 'Unsupported model: unsupported-model', - }); + expect(() => toGroqRequestBody('unsupported-model', request)).toThrow( + 'Unsupported model: unsupported-model' + ); + }); +}); + +describe('Groq Plugin', () => { + it('should create plugin with v2 API', () => { + const plugin = groq({ apiKey: 'test-key' }); + + // Check that the plugin has the expected structure + expect(plugin.name).toBe('groq'); + expect(typeof plugin.init).toBe('function'); + expect(typeof plugin.list).toBe('function'); + }); + + it('should list available models', async () => { + const plugin = groq({ apiKey: 'test-key' }); + const models = await plugin.list?.(); + + expect(Array.isArray(models)).toBe(true); + expect(models?.length).toBeGreaterThan(0); + + if (models) { + for (const model of models) { + expect((model as any).type).toBe('model'); + expect(typeof model.name).toBe('string'); + expect((model as any).namespace).toBe('groq'); + expect(typeof (model as any).info).toBe('object'); + } + } + }); + + it('should initialize models', async () => { + const plugin = groq({ apiKey: 'test-key' }); + const models = await plugin.init?.(); + + expect(Array.isArray(models)).toBe(true); + expect(models?.length).toBeGreaterThan(0); + + if (models) { + for (const model of models) { + expect(typeof model).toBe('function'); + } + } }); }); diff --git a/plugins/groq/tests/index.ts b/plugins/groq/tests/index.ts deleted file mode 100644 index b3c0c658..00000000 --- a/plugins/groq/tests/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './groq_test.ts'; -// Add other test files here as needed diff --git a/plugins/groq/tests/integration.test.ts b/plugins/groq/tests/integration.test.ts new file mode 100644 index 00000000..a990c72a --- /dev/null +++ b/plugins/groq/tests/integration.test.ts @@ -0,0 +1,280 @@ +/** + * Copyright 2024 Bloom Labs Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { genkit, Genkit, z } from 'genkit'; +import { groq, PluginOptions } from '../src/index'; +import { llamaGuard3x8b } from '../src/groq_models'; +import { type ChatCompletion } from 'groq-sdk/resources/chat/index.mjs'; +import { ResolvableAction } from 'genkit/plugin'; + +const mockCreate = jest.fn(); + +jest.mock('groq-sdk', () => ({ + __esModule: true, + default: jest.fn().mockImplementation(() => ({ + chat: { + completions: { + create: mockCreate, + }, + }, + })), +})); + +describe('with a genkit instance', () => { + const MODEL_NAME = 'llama-guard-3-8b'; + const AI_MESSAGE = 'Hello from mock Groq!'; + const USER_PROMPT = 'Hello'; + + let ai: Genkit; + + beforeEach(() => { + mockCreate.mockClear(); + ai = genkit({ + plugins: [groq({ apiKey: 'test-api-key' })], + }); + }); + + const createMockChatCompletion = (content: string): ChatCompletion => ({ + id: 'chatcmpl-test-123', + object: 'chat.completion', + created: 1234567890, + model: MODEL_NAME, + choices: [ + { + index: 0, + message: { + role: 'assistant', + content, + }, + finish_reason: 'stop', + logprobs: null, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + system_fingerprint: 'mock-fingerprint', + }); + + it('should handle basic response request when passing in a model string', async () => { + mockCreate.mockResolvedValue(createMockChatCompletion(AI_MESSAGE)); + + const result = await ai.generate({ + model: `groq/${MODEL_NAME}`, + prompt: USER_PROMPT, + }); + + expect(mockCreate).toHaveBeenCalledTimes(1); + expect(mockCreate).toHaveBeenCalledWith({ + messages: [{ role: 'user', content: USER_PROMPT }], + model: MODEL_NAME, + response_format: { type: 'text' }, + }); + + expect(result.text).toBe(AI_MESSAGE); + }); + + it('should handle basic chat request when passing in modelRef e.g llamaGuard3x8b', async () => { + mockCreate.mockResolvedValue(createMockChatCompletion(AI_MESSAGE)); + const result = await ai.generate({ + model: llamaGuard3x8b, + prompt: USER_PROMPT, + }); + expect(mockCreate).toHaveBeenCalledTimes(1); + expect(mockCreate).toHaveBeenCalledWith({ + messages: [{ role: 'user', content: USER_PROMPT }], + model: MODEL_NAME, + response_format: { type: 'text' }, + }); + expect(result.text).toBe(AI_MESSAGE); + }); +}); + +describe('calling the standalone plugin', () => { + const groqPlugin = groq({ apiKey: 'test-api-key' }); + + beforeEach(() => { + mockCreate.mockClear(); + }); + + const createMockChatCompletion = (content: string): ChatCompletion => ({ + id: 'chatcmpl-test-123', + object: 'chat.completion', + created: 1234567890, + model: 'llama-3-8b', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content, + }, + finish_reason: 'stop', + logprobs: null, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + system_fingerprint: 'mock-fingerprint', + }); + + it('should be able to initialize groq plugin', async () => { + const actions = await groqPlugin.init?.(); + + expect(actions).toBeDefined(); + expect(Array.isArray(actions)).toBe(true); + expect(actions?.length).toBeGreaterThan(0); + + const llamaGuardModel = actions?.find( + (action) => + typeof action === 'function' && + (action as any).__action?.name === 'llama-guard-3-8b' + ); + + expect(llamaGuardModel).toBeDefined(); + expect(typeof llamaGuardModel).toBe('function'); + }); + + it('should create actions with proper structure', async () => { + const actions = await groqPlugin.init?.(); + + expect(actions).toBeDefined(); + expect(Array.isArray(actions)).toBe(true); + expect(actions?.length).toBeGreaterThan(0); + + if (actions) { + for (const action of actions) { + expect(typeof action).toBe('function'); + expect((action as any).__action).toBeDefined(); + expect((action as any).__action.name).toBeDefined(); + expect(typeof (action as any).__action.name).toBe('string'); + } + } + }); + + it('should list available models', async () => { + const availableActions = await groqPlugin.list?.(); + + expect(availableActions).toBeDefined(); + expect(Array.isArray(availableActions)).toBe(true); + expect(availableActions?.length).toBeGreaterThan(0); + + const modelActions = availableActions?.filter( + (action) => (action as any).type === 'model' + ); + expect(modelActions?.length).toBeGreaterThan(0); + + const llamaGuardAction = modelActions?.find( + (action) => action.name === 'llama-guard-3-8b' + ); + expect(llamaGuardAction).toBeDefined(); + expect((llamaGuardAction as any)?.namespace).toBe('groq'); + }); + + it('should resolve models dynamically for direct usage', async () => { + const resolvedModel = await groqPlugin.resolve?.( + 'model', + 'llama-guard-3-8b' + ); + + expect(typeof resolvedModel).toBe('function'); + expect((resolvedModel as any).__action).toBeDefined(); + expect((resolvedModel as any).__action.name).toBe('llama-guard-3-8b'); + + // Mock the API response for direct model usage + mockCreate.mockResolvedValue({ + id: 'chatcmpl-test-123', + object: 'chat.completion', + created: 1234567890, + model: 'llama-guard-3-8b', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'Hello from resolved model!', + }, + finish_reason: 'stop', + logprobs: null, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + system_fingerprint: 'mock-fingerprint', + }); + + const response = await (resolvedModel as Function)({ + messages: [ + { + role: 'user', + content: [{ text: 'hi' }], + }, + ], + }); + + expect(mockCreate).toHaveBeenCalledTimes(1); + expect(response.candidates[0].message.content[0].text).toBe( + 'Hello from resolved model!' + ); + }); + + it('should handle resolve with unsupported action types', async () => { + const embedderResult = await groqPlugin.resolve?.( + 'embedder', + 'some-embedder' + ); + const toolResult = await groqPlugin.resolve?.('tool', 'some-tool'); + + expect(embedderResult).toBeUndefined(); + expect(toolResult).toBeUndefined(); + }); + + it('should create model action', async () => { + const actions = await groqPlugin.init?.(); + + expect(actions).toBeDefined(); + expect(actions?.length).toBeGreaterThan(0); + expect(actions?.[0].__action.name).toBe('llama-3-8b'); + }); + + it('should generate response', async () => { + const actions = await groqPlugin.init?.(); + + const model = actions?.[0]; + expect(model).toBeDefined(); + + mockCreate.mockResolvedValue(createMockChatCompletion('Hello!')); + + const response = await (model as Function)({ + messages: [ + { + role: 'user', + content: [{ text: 'Hi there!' }], + }, + ], + }); + + expect(mockCreate).toHaveBeenCalledTimes(1); + expect(response.candidates[0].message.content[0].text).toBe('Hello!'); + }); +});