From 88ec1da805ad72e35c9d9f61666a2da33aef3df6 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Tue, 23 Sep 2025 18:53:22 +0300 Subject: [PATCH 01/13] chore(groq): bump genkit version --- package-lock.json | 44 +++++++++++++++++++-------------------- plugins/groq/package.json | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) 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/package.json b/plugins/groq/package.json index 0676ce51..749c43b7 100644 --- a/plugins/groq/package.json +++ b/plugins/groq/package.json @@ -27,7 +27,7 @@ "groq-sdk": "^0.19.0" }, "peerDependencies": { - "genkit": "^0.9.0 || ^1.0.0" + "genkit": "^1.19.3" }, "devDependencies": { "@types/hast": "^3.0.4", From 288ffcdd10fbac4f1e9e2744d4b6ea59eb8d9edc Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Mon, 29 Sep 2025 18:32:55 +0300 Subject: [PATCH 02/13] feat: migrate to v2 plugins API --- plugins/groq/src/groq_models.ts | 26 ++++++++-------- plugins/groq/src/index.ts | 54 ++++++++++++++++++++------------- plugins/groq/tests/groq_test.ts | 43 ++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 34 deletions(-) diff --git a/plugins/groq/src/groq_models.ts b/plugins/groq/src/groq_models.ts index 7f7503de..c9f6311b 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}`); +export function createGroqModel(name: string, client: Groq) { + const modelRef = SUPPORTED_GROQ_MODELS[name]; + if (!modelRef) throw new Error(`Unsupported model: ${name}`); const modelId = `groq/${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..2b681264 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 { @@ -93,26 +92,39 @@ export interface PluginOptions { * @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' - ); - } + genkitPluginV2({ + name: 'groq', + init: async () => { + 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 - }); + // 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 + }); - // Register each model with the Genkit instance - for (const name of Object.keys(SUPPORTED_GROQ_MODELS)) { - groqModel(ai, name, client); - } + // Create model actions for each supported model + const models: any[] = []; + for (const name of Object.keys(SUPPORTED_GROQ_MODELS)) { + models.push(createGroqModel(name, client)); + } + + return models; + }, + list: async () => { + return Object.keys(SUPPORTED_GROQ_MODELS).map((name) => ({ + name: `groq/${name}`, + type: 'model' as const, + info: SUPPORTED_GROQ_MODELS[name].info, + })); + }, }); // Default export for plugin usage diff --git a/plugins/groq/tests/groq_test.ts b/plugins/groq/tests/groq_test.ts index 00f3eb04..2151bf89 100644 --- a/plugins/groq/tests/groq_test.ts +++ b/plugins/groq/tests/groq_test.ts @@ -7,6 +7,7 @@ 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', () => { @@ -97,3 +98,45 @@ describe('toGroqRequestBody', () => { }); }); }); + +describe('Groq Plugin', () => { + it('should create plugin with v2 API', () => { + const plugin = groq({ apiKey: 'test-key' }); + + // Check that the plugin has the expected structure + assert.strictEqual(plugin.name, 'groq'); + assert(typeof plugin.init === 'function'); + assert(typeof plugin.list === 'function'); + }); + + it('should list available models', async () => { + const plugin = groq({ apiKey: 'test-key' }); + const models = await plugin.list?.(); + + // Check that we get a list of models + assert(Array.isArray(models)); + assert(models.length > 0); + + // Check that each model has the expected structure + for (const model of models) { + assert.strictEqual((model as any).type, 'model'); + assert(typeof model.name === 'string'); + assert(model.name.startsWith('groq/')); + assert(typeof (model as any).info === 'object'); + } + }); + + it('should initialize models', async () => { + const plugin = groq({ apiKey: 'test-key' }); + const models = await plugin.init?.(); + + // Check that we get an array of model actions + assert(Array.isArray(models)); + assert(models.length > 0); + + // Check that each model is a function (action) + for (const model of models) { + assert(typeof model === 'function'); + } + }); +}); From 941fd63d1175afcfc767ae4a0d87fdbe275e0a6e Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Tue, 30 Sep 2025 13:28:02 +0300 Subject: [PATCH 03/13] chore(plugins/groq): remove /groq prefix --- plugins/groq/src/groq_models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/groq/src/groq_models.ts b/plugins/groq/src/groq_models.ts index c9f6311b..02975d23 100644 --- a/plugins/groq/src/groq_models.ts +++ b/plugins/groq/src/groq_models.ts @@ -581,7 +581,7 @@ export function toGroqRequestBody( export function createGroqModel(name: string, client: Groq) { const modelRef = SUPPORTED_GROQ_MODELS[name]; if (!modelRef) throw new Error(`Unsupported model: ${name}`); - const modelId = `groq/${name}`; + const modelId = `${name}`; return model( { From 1a8afdbc3846896462141769466354e4e283d7f0 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Mon, 6 Oct 2025 11:19:13 +0300 Subject: [PATCH 04/13] chore(plugins/groq): remove unnecessary comments --- plugins/groq/tests/groq_test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/groq/tests/groq_test.ts b/plugins/groq/tests/groq_test.ts index 2151bf89..9f1c7c2f 100644 --- a/plugins/groq/tests/groq_test.ts +++ b/plugins/groq/tests/groq_test.ts @@ -113,11 +113,9 @@ describe('Groq Plugin', () => { const plugin = groq({ apiKey: 'test-key' }); const models = await plugin.list?.(); - // Check that we get a list of models assert(Array.isArray(models)); assert(models.length > 0); - // Check that each model has the expected structure for (const model of models) { assert.strictEqual((model as any).type, 'model'); assert(typeof model.name === 'string'); @@ -130,11 +128,9 @@ describe('Groq Plugin', () => { const plugin = groq({ apiKey: 'test-key' }); const models = await plugin.init?.(); - // Check that we get an array of model actions assert(Array.isArray(models)); assert(models.length > 0); - // Check that each model is a function (action) for (const model of models) { assert(typeof model === 'function'); } From 533f1d2c8af08cd5dc1da843cccb42635687b78b Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Mon, 6 Oct 2025 17:35:36 +0300 Subject: [PATCH 05/13] chore(plugins/groq): add name and namespace --- lerna.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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/*"] +} From bab5c8c44cd19457851d9c626eed2b6d304b8202 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Mon, 6 Oct 2025 17:36:19 +0300 Subject: [PATCH 06/13] chore(plugins/groq): format --- plugins/groq/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/groq/src/index.ts b/plugins/groq/src/index.ts index 2b681264..51d1b01a 100644 --- a/plugins/groq/src/index.ts +++ b/plugins/groq/src/index.ts @@ -120,7 +120,8 @@ export const groq = (options?: PluginOptions) => }, list: async () => { return Object.keys(SUPPORTED_GROQ_MODELS).map((name) => ({ - name: `groq/${name}`, + name, + namespace: 'groq', type: 'model' as const, info: SUPPORTED_GROQ_MODELS[name].info, })); From 1087bad8472275fea9bcc31600b46f6787d4adb7 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Mon, 6 Oct 2025 21:52:53 +0300 Subject: [PATCH 07/13] tests(plugins/groq): test for namespace to be groq when listing available models --- plugins/groq/tests/groq_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/groq/tests/groq_test.ts b/plugins/groq/tests/groq_test.ts index 9f1c7c2f..254e2763 100644 --- a/plugins/groq/tests/groq_test.ts +++ b/plugins/groq/tests/groq_test.ts @@ -119,7 +119,7 @@ describe('Groq Plugin', () => { for (const model of models) { assert.strictEqual((model as any).type, 'model'); assert(typeof model.name === 'string'); - assert(model.name.startsWith('groq/')); + assert.strictEqual((model as any).namespace, 'groq'); assert(typeof (model as any).info === 'object'); } }); From 57b788659c27646b4cdb12b127ceaa364f13b958 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Wed, 15 Oct 2025 14:34:34 +0300 Subject: [PATCH 08/13] test(js/plugin/groq): setup jest tests --- plugins/groq/.gitignore | 3 +- plugins/groq/jest.config.js | 6 + plugins/groq/package.json | 4 +- plugins/groq/src/index.ts | 5 + .../groq/tests/{groq_test.ts => groq.test.ts} | 65 +++++----- plugins/groq/tests/index.ts | 2 - plugins/groq/tests/integration.test.ts | 111 ++++++++++++++++++ 7 files changed, 160 insertions(+), 36 deletions(-) create mode 100644 plugins/groq/jest.config.js rename plugins/groq/tests/{groq_test.ts => groq.test.ts} (66%) delete mode 100644 plugins/groq/tests/index.ts create mode 100644 plugins/groq/tests/integration.test.ts 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 749c43b7..c8b472af 100644 --- a/plugins/groq/package.json +++ b/plugins/groq/package.json @@ -33,8 +33,10 @@ "@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/index.ts b/plugins/groq/src/index.ts index 51d1b01a..135989b5 100644 --- a/plugins/groq/src/index.ts +++ b/plugins/groq/src/index.ts @@ -118,6 +118,11 @@ export const groq = (options?: PluginOptions) => return models; }, + // TODO: remove ts-ignore when properly implementing + //@ts-ignore + resolve: async () => { + // TODO: if actionType is "model", then we return createGroqModel(...) of the model + }, list: async () => { return Object.keys(SUPPORTED_GROQ_MODELS).map((name) => ({ name, diff --git a/plugins/groq/tests/groq_test.ts b/plugins/groq/tests/groq.test.ts similarity index 66% rename from plugins/groq/tests/groq_test.ts rename to plugins/groq/tests/groq.test.ts index 254e2763..14813ac2 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, @@ -11,25 +9,25 @@ 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." + ); }); }); @@ -44,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); }); }); @@ -85,17 +83,16 @@ 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)) - ); + // console.log(`actualOutput.stop: ${actualOutput.stop}`); + 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' + ); }); }); @@ -104,23 +101,25 @@ describe('Groq Plugin', () => { const plugin = groq({ apiKey: 'test-key' }); // Check that the plugin has the expected structure - assert.strictEqual(plugin.name, 'groq'); - assert(typeof plugin.init === 'function'); - assert(typeof plugin.list === 'function'); + 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?.(); - assert(Array.isArray(models)); - assert(models.length > 0); + expect(Array.isArray(models)).toBe(true); + expect(models?.length).toBeGreaterThan(0); - for (const model of models) { - assert.strictEqual((model as any).type, 'model'); - assert(typeof model.name === 'string'); - assert.strictEqual((model as any).namespace, 'groq'); - assert(typeof (model as any).info === 'object'); + 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'); + } } }); @@ -128,11 +127,13 @@ describe('Groq Plugin', () => { const plugin = groq({ apiKey: 'test-key' }); const models = await plugin.init?.(); - assert(Array.isArray(models)); - assert(models.length > 0); + expect(Array.isArray(models)).toBe(true); + expect(models?.length).toBeGreaterThan(0); - for (const model of models) { - assert(typeof model === 'function'); + 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..bd933544 --- /dev/null +++ b/plugins/groq/tests/integration.test.ts @@ -0,0 +1,111 @@ +/** + * 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 { type ChatCompletion } from 'groq-sdk/resources/chat/index.mjs'; + +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', () => { + // TODO: mocks might need changing up for this test + // mockCreate.mockResolvedValue(createMockChatCompletion(AI_MESSAGE)); + // TODO: import llamaGuard3x8b model reference and use in test + // 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', () => { + it('should be able to call groq().model("some model name")', () => { + // TODO: look at the migration doc for a reference for how this should work + }); +}); From 7785060ed1de1c2ca128ae5ea68843b24296d317 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Wed, 15 Oct 2025 19:29:39 +0300 Subject: [PATCH 09/13] test(js/plugin/groq): add plugin standalone tests --- plugins/groq/tests/integration.test.ts | 84 ++++++++++++++++++++------ 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/plugins/groq/tests/integration.test.ts b/plugins/groq/tests/integration.test.ts index bd933544..f80177eb 100644 --- a/plugins/groq/tests/integration.test.ts +++ b/plugins/groq/tests/integration.test.ts @@ -15,6 +15,7 @@ */ 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'; const mockCreate = jest.fn(); @@ -86,26 +87,75 @@ describe('with a genkit instance', () => { expect(result.text).toBe(AI_MESSAGE); }); - it('should handle basic chat request when passing in modelRef e.g llamaGuard3x8b', () => { - // TODO: mocks might need changing up for this test - // mockCreate.mockResolvedValue(createMockChatCompletion(AI_MESSAGE)); - // TODO: import llamaGuard3x8b model reference and use in test - // 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); + 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', () => { - it('should be able to call groq().model("some model name")', () => { - // TODO: look at the migration doc for a reference for how this should work + const groqPlugin = groq({ apiKey: 'test-api-key' }); + + 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'); }); }); From e4e689ef716ddaa240742588211fbade8d8a8011 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Fri, 17 Oct 2025 11:13:53 +0300 Subject: [PATCH 10/13] test(js/plugins/groq): implement resolve method and add standalone tests --- plugins/groq/src/index.ts | 47 +++++----- plugins/groq/tests/integration.test.ts | 115 +++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 23 deletions(-) diff --git a/plugins/groq/src/index.ts b/plugins/groq/src/index.ts index 135989b5..189ac649 100644 --- a/plugins/groq/src/index.ts +++ b/plugins/groq/src/index.ts @@ -91,26 +91,24 @@ 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) => - genkitPluginV2({ - name: 'groq', - init: async () => { - 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, + }); - // Create model actions for each supported model + return genkitPluginV2({ + name: 'groq', + init: async () => { const models: any[] = []; for (const name of Object.keys(SUPPORTED_GROQ_MODELS)) { models.push(createGroqModel(name, client)); @@ -118,10 +116,13 @@ export const groq = (options?: PluginOptions) => return models; }, - // TODO: remove ts-ignore when properly implementing - //@ts-ignore - resolve: async () => { - // TODO: if actionType is "model", then we return createGroqModel(...) of the model + resolve: async (actionType, actionName) => { + console.log('resolve', actionType, actionName); + + if (actionType === 'model') { + return createGroqModel(actionName, client); + } + return undefined; }, list: async () => { return Object.keys(SUPPORTED_GROQ_MODELS).map((name) => ({ @@ -132,6 +133,6 @@ export const groq = (options?: PluginOptions) => })); }, }); +}; -// Default export for plugin usage export default groq; diff --git a/plugins/groq/tests/integration.test.ts b/plugins/groq/tests/integration.test.ts index f80177eb..675f504f 100644 --- a/plugins/groq/tests/integration.test.ts +++ b/plugins/groq/tests/integration.test.ts @@ -17,6 +17,7 @@ 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(); @@ -106,6 +107,30 @@ describe('with a genkit instance', () => { describe('calling the standalone plugin', () => { const groqPlugin = groq({ apiKey: 'test-api-key' }); + 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?.(); @@ -158,4 +183,94 @@ describe('calling the standalone plugin', () => { 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!'); + }); }); From 666aef667dfad4f4dd8681378a393ce6c21272e4 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Tue, 21 Oct 2025 14:23:49 +0300 Subject: [PATCH 11/13] chore(js/plugins/groq): cleanup --- plugins/groq/src/index.ts | 2 -- plugins/groq/tests/groq.test.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/plugins/groq/src/index.ts b/plugins/groq/src/index.ts index 189ac649..9f8827cc 100644 --- a/plugins/groq/src/index.ts +++ b/plugins/groq/src/index.ts @@ -117,8 +117,6 @@ export const groq = (options?: PluginOptions) => { return models; }, resolve: async (actionType, actionName) => { - console.log('resolve', actionType, actionName); - if (actionType === 'model') { return createGroqModel(actionName, client); } diff --git a/plugins/groq/tests/groq.test.ts b/plugins/groq/tests/groq.test.ts index 14813ac2..5e0f2346 100644 --- a/plugins/groq/tests/groq.test.ts +++ b/plugins/groq/tests/groq.test.ts @@ -83,7 +83,6 @@ describe('toGroqRequestBody', () => { }; const actualOutput = toGroqRequestBody('llama-3-8b', request); - // console.log(`actualOutput.stop: ${actualOutput.stop}`); expect( JSON.parse(JSON.stringify(actualOutput)) // Remove undefined fields ).toEqual(JSON.parse(JSON.stringify(expectedOutput))); From cc536d0fde11a9fd91236a3ce5ec22d4d190e863 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Tue, 21 Oct 2025 14:36:46 +0300 Subject: [PATCH 12/13] refactor(js/plugins/groq): replace loop with push to a map --- plugins/groq/src/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/groq/src/index.ts b/plugins/groq/src/index.ts index 9f8827cc..d3b59c99 100644 --- a/plugins/groq/src/index.ts +++ b/plugins/groq/src/index.ts @@ -109,10 +109,9 @@ export const groq = (options?: PluginOptions) => { return genkitPluginV2({ name: 'groq', init: async () => { - const models: any[] = []; - for (const name of Object.keys(SUPPORTED_GROQ_MODELS)) { - models.push(createGroqModel(name, client)); - } + const models = Object.keys(SUPPORTED_GROQ_MODELS).map((name) => + createGroqModel(name, client) + ); return models; }, From d090f09f6bf174d01c18170ca5b1f0767995640f Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Tue, 21 Oct 2025 14:57:11 +0300 Subject: [PATCH 13/13] tests(js/plugins/groq): clear mocks before test suits --- plugins/groq/tests/integration.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/groq/tests/integration.test.ts b/plugins/groq/tests/integration.test.ts index 675f504f..a990c72a 100644 --- a/plugins/groq/tests/integration.test.ts +++ b/plugins/groq/tests/integration.test.ts @@ -107,6 +107,10 @@ describe('with a genkit instance', () => { 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',