From 6e9f05b0e7a06aa77f8f3d7fea4ae6a00645d810 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Wed, 19 Aug 2026 13:19:14 +0100 Subject: [PATCH 1/7] fix(kits): add Gemini 3.x before 2.5 retirement Chatbot, translate, and the resize content filter still pin retiring Gemini 2.5 model ids. --- kits/firestore-genai-chatbot/README.md | 2 +- kits/firestore-genai-chatbot/src/config.ts | 2 +- .../src/export-config.ts | 2 +- .../src/generative-client/genkit.ts | 58 ++++++++----------- .../tests/generative-client.test.ts | 15 +++++ kits/firestore-translate-text/README.md | 2 +- kits/firestore-translate-text/src/config.ts | 5 +- .../src/export-config.ts | 2 +- .../src/content-filter.ts | 5 +- 9 files changed, 52 insertions(+), 41 deletions(-) diff --git a/kits/firestore-genai-chatbot/README.md b/kits/firestore-genai-chatbot/README.md index fd9922188..9f31fd48b 100644 --- a/kits/firestore-genai-chatbot/README.md +++ b/kits/firestore-genai-chatbot/README.md @@ -90,7 +90,7 @@ the CLI connects them to the function at deploy time. |---|---|---|---|---| | `provider` | `GENERATIVE_AI_PROVIDER` | no | `google-ai` | `google-ai` or `vertex-ai` | | `apiKey` | `API_KEY` | secret | — | Google AI API key | -| `model` | `MODEL` | no | `gemini-2.5-flash` | Model id | +| `model` | `MODEL` | no | `gemini-3.6-flash` | Model id | | `vertexModelLocation` | `VERTEX_AI_MODEL_LOCATION` | no | `null` | Vertex model region | | `collectionName` | `COLLECTION_NAME` | no | `generate` | Discussion collection | | `promptField` | `PROMPT_FIELD` | no | `prompt` | Prompt field name | diff --git a/kits/firestore-genai-chatbot/src/config.ts b/kits/firestore-genai-chatbot/src/config.ts index 52757a8c7..713fff00a 100644 --- a/kits/firestore-genai-chatbot/src/config.ts +++ b/kits/firestore-genai-chatbot/src/config.ts @@ -84,7 +84,7 @@ const params = { input: select([...GENERATIVE_AI_PROVIDER_OPTIONS]), }), apiKey: defineSecret("API_KEY"), - model: defineString("MODEL", { default: "gemini-2.5-flash" }), + model: defineString("MODEL", { default: "gemini-3.6-flash" }), vertexModelLocation: defineString("VERTEX_AI_MODEL_LOCATION", { default: "null", input: select([...VERTEX_MODEL_LOCATION_OPTIONS]), diff --git a/kits/firestore-genai-chatbot/src/export-config.ts b/kits/firestore-genai-chatbot/src/export-config.ts index 6c71012e0..5e85ed4e1 100644 --- a/kits/firestore-genai-chatbot/src/export-config.ts +++ b/kits/firestore-genai-chatbot/src/export-config.ts @@ -45,7 +45,7 @@ export interface GenaiChatbotConfig { provider?: GenerativeAIProvider | "google-ai" | "vertex-ai"; /** API key for the `google-ai` provider. */ apiKey?: string; - /** Model id, e.g. `gemini-2.5-flash`. */ + /** Model id, e.g. `gemini-3.6-flash`. */ model: string; /** Vertex AI model location. */ vertexModelLocation?: string; diff --git a/kits/firestore-genai-chatbot/src/generative-client/genkit.ts b/kits/firestore-genai-chatbot/src/generative-client/genkit.ts index cb68e1cfd..ecef43df5 100644 --- a/kits/firestore-genai-chatbot/src/generative-client/genkit.ts +++ b/kits/firestore-genai-chatbot/src/generative-client/genkit.ts @@ -103,41 +103,33 @@ export class GenkitDiscussionClient extends DiscussionClient< return genkit(genkitConfig); } - // TODO(migration): inherited verbatim from the legacy extension — this - // hardcoded model allowlist means new/custom/fine-tuned models need a package - // update. `googleAI.model()` / `vertexAI.model()` resolve any id dynamically; - // consider simplifying to that. Improvement, not a bug. Deferred from PR #431 review. + /** + * Resolves a Genkit model reference for the configured provider. + * + * Known ids are registered first so version aliases still match. Unknown + * ids fall through to `googleAI.model()` / `vertexAI.model()` so current + * Gemini releases work without a package update. + */ static createModelReference( model: string, provider: string ): ModelReference { - const modelReferences = - provider === "google-ai" - ? [ - googleAI.model("gemini-1.5-flash"), - googleAI.model("gemini-1.5-pro"), - googleAI.model("gemini-2.0-flash"), - googleAI.model("gemini-2.0-flash-lite"), - googleAI.model("gemini-2.5-flash-lite"), - googleAI.model("gemini-2.5-flash"), - googleAI.model("gemini-2.5-pro"), - googleAI.model("gemini-3-pro-preview"), - googleAI.model("gemini-3-pro-image-preview"), - ] - : [ - vertexAI.model("gemini-1.5-flash"), - vertexAI.model("gemini-1.5-pro"), - vertexAI.model("gemini-2.0-flash"), - vertexAI.model("gemini-2.0-flash-lite"), - vertexAI.model("gemini-2.0-flash-001"), - vertexAI.model("gemini-2.5-flash-lite"), - vertexAI.model("gemini-2.5-flash"), - vertexAI.model("gemini-2.5-pro"), - vertexAI.model("gemini-3-pro-preview"), - vertexAI.model("gemini-3-pro-image-preview"), - ]; - - const pluginName = provider === "google-ai" ? "googleai" : "vertexai"; + const isGoogleAi = provider === "google-ai"; + const pluginName = isGoogleAi ? "googleai" : "vertexai"; + const knownIds = [ + "gemini-3.6-flash", + "gemini-3.5-flash", + "gemini-3.5-flash-lite", + "gemini-3.1-flash-lite", + "gemini-3.1-pro-preview", + "gemini-2.5-flash-lite", + "gemini-2.5-flash", + "gemini-2.5-pro", + ] as const; + + const modelReferences = knownIds.map((id) => + isGoogleAi ? googleAI.model(id) : vertexAI.model(id) + ); for (const modelReference of modelReferences) { if (modelReference.name === `${pluginName}/${model}`) { @@ -147,7 +139,7 @@ export class GenkitDiscussionClient extends DiscussionClient< return modelReference.withVersion(model); } } - throw new Error("Model not found."); + return isGoogleAi ? googleAI.model(model) : vertexAI.model(model); } private createGenerateOptions( @@ -172,7 +164,7 @@ export class GenkitDiscussionClient extends DiscussionClient< }; } - /** Whether the Genkit client can serve this config (single candidate + known model). */ + /** Whether the Genkit client can serve this config (single candidate). */ static shouldUseGenkitClient(config: ResolvedGenaiChatbotConfig): boolean { const shouldReturnMultipleCandidates = config.candidateCount && config.candidateCount > 1; diff --git a/kits/firestore-genai-chatbot/tests/generative-client.test.ts b/kits/firestore-genai-chatbot/tests/generative-client.test.ts index 100a8977e..34c3eb5e5 100644 --- a/kits/firestore-genai-chatbot/tests/generative-client.test.ts +++ b/kits/firestore-genai-chatbot/tests/generative-client.test.ts @@ -64,6 +64,21 @@ describe("GenkitDiscussionClient.shouldUseGenkitClient", () => { const config = resolveConfig({ ...baseInput, candidateCount: 2 }); expect(GenkitDiscussionClient.shouldUseGenkitClient(config)).toBe(false); }); + + test("true for a current model id that is not in the legacy allowlist", () => { + const config = resolveConfig({ + ...baseInput, + model: "gemini-3.6-flash", + candidateCount: 1, + }); + expect(GenkitDiscussionClient.shouldUseGenkitClient(config)).toBe(true); + expect(() => + GenkitDiscussionClient.createModelReference( + "gemini-3.6-flash", + "google-ai" + ) + ).not.toThrow(); + }); }); describe("VertexDiscussionClient", () => { diff --git a/kits/firestore-translate-text/README.md b/kits/firestore-translate-text/README.md index 2d2281ca2..3aee2e8d9 100644 --- a/kits/firestore-translate-text/README.md +++ b/kits/firestore-translate-text/README.md @@ -90,7 +90,7 @@ the CLI connects them to the function at deploy time. | `languages` | `LANGUAGES` | no | `en,es,de,fr` | Target language codes | | `languagesFieldName` | `LANGUAGES_FIELD_NAME` | no | `languages` | Per-doc languages field | | `provider` | `TRANSLATION_PROVIDER` | yes | — | Translation provider | -| `geminiModel` | `GEMINI_MODEL` | no | `gemini-2.5-flash` | Gemini model when used | +| `geminiModel` | `GEMINI_MODEL` | no | `gemini-3.6-flash` | Gemini model when used | | `googleAiApiKey` | `GOOGLE_AI_API_KEY` | secret | — | Google AI API key (Gemini) | ## Multiple instances diff --git a/kits/firestore-translate-text/src/config.ts b/kits/firestore-translate-text/src/config.ts index e3d92d676..7cd54880c 100644 --- a/kits/firestore-translate-text/src/config.ts +++ b/kits/firestore-translate-text/src/config.ts @@ -37,6 +37,9 @@ const TRANSLATION_PROVIDER_OPTIONS = [ "gemini-vertexai", ] as const; const GEMINI_MODEL_OPTIONS = [ + "gemini-3.1-pro-preview", + "gemini-3.6-flash", + "gemini-3.1-flash-lite", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", @@ -54,7 +57,7 @@ const params = { input: select([...TRANSLATION_PROVIDER_OPTIONS]), }), geminiModel: defineString("GEMINI_MODEL", { - default: "gemini-2.5-flash", + default: "gemini-3.6-flash", input: select([...GEMINI_MODEL_OPTIONS]), }), }; diff --git a/kits/firestore-translate-text/src/export-config.ts b/kits/firestore-translate-text/src/export-config.ts index 802037df3..5fc920204 100644 --- a/kits/firestore-translate-text/src/export-config.ts +++ b/kits/firestore-translate-text/src/export-config.ts @@ -49,7 +49,7 @@ export interface ResolvedTranslateConfig { } const DEFAULT_PROVIDER: TranslationProvider = "translate"; -const DEFAULT_GEMINI_MODEL = "gemini-2.5-flash"; +const DEFAULT_GEMINI_MODEL = "gemini-3.6-flash"; function toUniqueArray( languages: ReadonlyArray | string diff --git a/kits/storage-resize-images/src/content-filter.ts b/kits/storage-resize-images/src/content-filter.ts index a2c46b32b..add0b44da 100644 --- a/kits/storage-resize-images/src/content-filter.ts +++ b/kits/storage-resize-images/src/content-filter.ts @@ -27,6 +27,7 @@ const HARM_CATEGORIES = [ "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_HARASSMENT", ] as const; +const CONTENT_FILTER_MODEL = "gemini-3.6-flash"; const RETRY_BASE_MS = 500; const RETRY_JITTER_MS = 200; const RETRY_MAX_MS = 5000; @@ -88,7 +89,7 @@ export async function checkImageContent( plugins: [ vertexAI({ location, - models: ["gemini-2.5-flash"], + models: [CONTENT_FILTER_MODEL], }), ], }); @@ -112,7 +113,7 @@ export async function checkImageContent( try { const result = await ai.generate({ - model: gemini("gemini-2.5-flash"), + model: gemini(CONTENT_FILTER_MODEL), messages: [ { role: "user", From b6533ec8aaf6b8581f582b54239875d77f3820a8 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Thu, 13 Aug 2026 19:55:34 +0100 Subject: [PATCH 2/7] fix(translate): add Gemini 3.5 Flash models Picker listed every 2.5 model; add 3.5 Flash and Flash Lite to match the current Gemini set. --- kits/firestore-translate-text/src/config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kits/firestore-translate-text/src/config.ts b/kits/firestore-translate-text/src/config.ts index 7cd54880c..211f017b3 100644 --- a/kits/firestore-translate-text/src/config.ts +++ b/kits/firestore-translate-text/src/config.ts @@ -39,6 +39,8 @@ const TRANSLATION_PROVIDER_OPTIONS = [ const GEMINI_MODEL_OPTIONS = [ "gemini-3.1-pro-preview", "gemini-3.6-flash", + "gemini-3.5-flash", + "gemini-3.5-flash-lite", "gemini-3.1-flash-lite", "gemini-2.5-pro", "gemini-2.5-flash", From 7540b0d0508b07563793ec47d397033eaaac1f88 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Thu, 13 Aug 2026 20:21:55 +0100 Subject: [PATCH 3/7] fix(resize): use gemini-3.1-flash-lite Similar price to previous gemini-2.5-flash and better quality. Avoid 3.5-flash-lite and higher cost. --- kits/storage-resize-images/src/content-filter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kits/storage-resize-images/src/content-filter.ts b/kits/storage-resize-images/src/content-filter.ts index add0b44da..ff7a957e6 100644 --- a/kits/storage-resize-images/src/content-filter.ts +++ b/kits/storage-resize-images/src/content-filter.ts @@ -27,7 +27,8 @@ const HARM_CATEGORIES = [ "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_HARASSMENT", ] as const; -const CONTENT_FILTER_MODEL = "gemini-3.6-flash"; +/** Similar price to previous `gemini-2.5-flash`, better quality than 2.5 Flash. Higher Flash/Lite tiers cost more. */ +const CONTENT_FILTER_MODEL = "gemini-3.1-flash-lite"; const RETRY_BASE_MS = 500; const RETRY_JITTER_MS = 200; const RETRY_MAX_MS = 5000; From 56936e6c9a4c7549024e0d9a6cf38b317f712193 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Mon, 17 Aug 2026 23:22:58 +0100 Subject: [PATCH 4/7] fix(resize): use global Vertex via google-genai gemini-3.1-flash-lite is not served on us-central1. Old @genkit-ai/vertexai rejects global; switch to @genkit-ai/google-genai. --- kits/storage-resize-images/package-lock.json | 555 +++--------------- kits/storage-resize-images/package.json | 4 +- .../src/content-filter.ts | 13 +- 3 files changed, 77 insertions(+), 495 deletions(-) diff --git a/kits/storage-resize-images/package-lock.json b/kits/storage-resize-images/package-lock.json index e39865549..77d77961e 100644 --- a/kits/storage-resize-images/package-lock.json +++ b/kits/storage-resize-images/package-lock.json @@ -9,11 +9,11 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { - "@genkit-ai/vertexai": "^1.2.0", + "@genkit-ai/google-genai": "^1.24.0", "@google-cloud/storage": "^7.21.0", "firebase-admin": "^13.2.0", "firebase-functions": "7.3.2", - "genkit": "^1.2.0", + "genkit": "^1.24.0", "mkdirp": "^3.0.1", "p-queue": "^6.6.2", "sharp": "^0.34.5", @@ -23,45 +23,6 @@ "node": ">=22" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.90.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.90.0.tgz", - "integrity": "sha512-MzZtPabJF1b0FTDl6Z6H5ljphPwACLGP13lu8MTiB8jXaW/YXlpOp+Po2cVou3MPM5+f5toyLnul9whKCy7fBg==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@anthropic-ai/vertex-sdk": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.16.1.tgz", - "integrity": "sha512-NQSJTmHFqJP32W4I+UyZ42ioUkd8avdye259Cs+P9yhi+XdI4wk7sDVnmVNNTiMtN08WXyELnAQPG2gcLQFXdQ==", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": ">=0.50.3 <1", - "google-auth-library": "^9.4.2" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@cfworker/json-schema": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", @@ -640,102 +601,19 @@ "genkit": "^1.40.1" } }, - "node_modules/@genkit-ai/vertexai": { + "node_modules/@genkit-ai/google-genai": { "version": "1.40.1", - "resolved": "https://registry.npmjs.org/@genkit-ai/vertexai/-/vertexai-1.40.1.tgz", - "integrity": "sha512-wDRHoT6WaTYiARz+UgzK1Mt000eZVjI1waQ2xBnbJYUmrl4keEjepd2WZvhfq5VZHe1wwqD0Md8c97xeeznjFw==", + "resolved": "https://registry.npmjs.org/@genkit-ai/google-genai/-/google-genai-1.40.1.tgz", + "integrity": "sha512-U+Cl8009vVBVVRIW7L4sq4XOWE7O9wfOVqGdyp38cZWnkrQrxdsoMzg3svt/Vqn2C0lIMFNsdKeBGTxNx5RwbA==", "license": "Apache-2.0", "dependencies": { - "@anthropic-ai/sdk": "^0.90.0", - "@anthropic-ai/vertex-sdk": "^0.16.0", - "@google-cloud/aiplatform": "^3.23.0", - "@google-cloud/vertexai": "^1.9.3", - "@mistralai/mistralai-gcp": "^1.3.5", "google-auth-library": "^9.14.2", - "googleapis": "^140.0.1", - "node-fetch": "^3.3.2", - "openai": "^4.52.7" - }, - "optionalDependencies": { - "@google-cloud/bigquery": "^7.8.0", - "firebase-admin": ">=12.2" + "jsonpath-plus": "^10.3.0" }, "peerDependencies": { "genkit": "^1.40.1" } }, - "node_modules/@google-cloud/aiplatform": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/@google-cloud/aiplatform/-/aiplatform-3.35.0.tgz", - "integrity": "sha512-Eo+ckr1KbTxAOew9P+MeeR0aQXeW5PeOzrSM1JyGny/SGKejwX/RcGWSFpeapnlegTfI9N9xJeUeo3M+XBOeFg==", - "license": "Apache-2.0", - "dependencies": { - "google-gax": "^4.0.3", - "protobuf.js": "^1.1.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/bigquery": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@google-cloud/bigquery/-/bigquery-7.9.4.tgz", - "integrity": "sha512-C7jeI+9lnCDYK3cRDujcBsPgiwshWKn/f0BiaJmClplfyosCLfWE83iGQ0eKH113UZzjR9c9q7aZQg0nU388sw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@google-cloud/common": "^5.0.0", - "@google-cloud/paginator": "^5.0.2", - "@google-cloud/precise-date": "^4.0.0", - "@google-cloud/promisify": "4.0.0", - "arrify": "^2.0.1", - "big.js": "^6.0.0", - "duplexify": "^4.0.0", - "extend": "^3.0.2", - "is": "^3.3.0", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/bigquery/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@google-cloud/common": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-5.0.2.tgz", - "integrity": "sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "^4.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "extend": "^3.0.2", - "google-auth-library": "^9.0.0", - "html-entities": "^2.5.2", - "retry-request": "^7.0.0", - "teeny-request": "^9.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@google-cloud/firestore": { "version": "7.11.6", "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz", @@ -1323,97 +1201,6 @@ "node": ">=14" } }, - "node_modules/@google-cloud/vertexai": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-1.12.0.tgz", - "integrity": "sha512-XMJIk7GIeavFLP5A3YEUlowKa5Y5PZRrnnuTJcqR0k+lFKkv7+IWpdRp+Xbqb8xNDrvQaE2hP2RYPUylyD5EdA==", - "license": "Apache-2.0", - "dependencies": { - "@google/genai": "^1.45.0", - "google-auth-library": "^9.1.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@google/genai": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", - "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/@google/genai/node_modules/gaxios": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.0.tgz", - "integrity": "sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@google/genai/node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@google/genai/node_modules/google-auth-library": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", - "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@google/genai/node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/@grpc/grpc-js": { "version": "1.14.4", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", @@ -1450,6 +1237,7 @@ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "license": "Apache-2.0", + "optional": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -2089,13 +1877,28 @@ "url": "https://opencollective.com/js-sdsl" } }, - "node_modules/@mistralai/mistralai-gcp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai-gcp/-/mistralai-gcp-1.7.0.tgz", - "integrity": "sha512-0RsClXspFiPUE4/9aBQXxNVjXDYFxkASXR/pxnX+2N689DF1d5Fx6Ax+tXH/n3K87iSevO2qBTprvUOwmF/ApQ==", - "dependencies": { - "google-auth-library": "^9.11.0", - "zod": "^3.25.0 || ^4.0.0" + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" } }, "node_modules/@nodable/entities": { @@ -3671,7 +3474,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@types/memcached": { "version": "2.2.10", @@ -3700,24 +3504,14 @@ } }, "node_modules/@types/node": { - "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "license": "MIT", "dependencies": { "undici-types": "~8.3.0" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" - } - }, "node_modules/@types/pg": { "version": "8.6.1", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", @@ -3802,12 +3596,6 @@ "node": ">= 0.6" } }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" - }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", @@ -3911,18 +3699,6 @@ "node": ">= 14" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -4083,20 +3859,6 @@ ], "license": "MIT" }, - "node_modules/big.js": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", - "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/bigjs" - } - }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -4928,71 +4690,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT" - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "license": "MIT", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, - "node_modules/formdata-node/node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -5230,6 +4927,7 @@ "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", "license": "Apache-2.0", + "optional": true, "dependencies": { "@grpc/grpc-js": "^1.10.9", "@grpc/proto-loader": "^0.7.13", @@ -5253,6 +4951,7 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", + "optional": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -5278,6 +4977,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "optional": true, "bin": { "uuid": "dist/bin/uuid" } @@ -5291,24 +4991,12 @@ "node": ">=14" } }, - "node_modules/googleapis": { - "version": "140.0.1", - "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-140.0.1.tgz", - "integrity": "sha512-ZGvBX4mQcFXO9ACnVNg6Aqy3KtBPB5zTuue43YVLxwn8HSv8jB7w+uDKoIPSoWuxGROgnj2kbng6acXncOQRNA==", - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^9.0.0", - "googleapis-common": "^7.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/googleapis-common": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", "license": "Apache-2.0", + "optional": true, "dependencies": { "extend": "^3.0.2", "gaxios": "^6.0.3", @@ -5331,6 +5019,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "optional": true, "bin": { "uuid": "dist/bin/uuid" } @@ -5501,15 +5190,6 @@ "node": ">= 14" } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, "node_modules/iconv-lite": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", @@ -5553,16 +5233,6 @@ "node": ">= 0.10" } }, - "node_modules/is": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/is/-/is-3.3.2.tgz", - "integrity": "sha512-a2xr4E3s1PjDS8ORcGgXpWx6V+liNs+O3JRD2mb9aeugD7rtkkZ0zgLdYgw0tWsKhsdiezGYptSiMlVazCBTuQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -5659,6 +5329,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -5674,19 +5353,6 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "license": "(AFL-2.1 OR BSD-3-Clause)" }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -5705,6 +5371,24 @@ "node": ">=6" } }, + "node_modules/jsonpath-plus": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", + "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -6098,6 +5782,7 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "license": "MIT", + "optional": true, "engines": { "node": ">= 6" } @@ -6145,71 +5830,6 @@ "fn.name": "1.x.x" } }, - "node_modules/openai": { - "version": "4.104.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", - "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - }, - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/openai/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/openai/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/openai/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -6250,19 +5870,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-timeout": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", @@ -6444,6 +6051,7 @@ "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", "license": "Apache-2.0", + "optional": true, "dependencies": { "protobufjs": "^7.2.5" }, @@ -6451,24 +6059,6 @@ "node": ">=14.0.0" } }, - "node_modules/protobuf.js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/protobuf.js/-/protobuf.js-1.1.2.tgz", - "integrity": "sha512-USO7Xus/pzPw549M1TguiyoOrKEhm9VMXv+CkDufcjMC8Rd7EPbxeRQPEjCV8ua1tm0k7z9xHkogcxovZogWdA==", - "license": "MIT", - "dependencies": { - "long": "~1.1.2" - } - }, - "node_modules/protobuf.js/node_modules/long": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/long/-/long-1.1.5.tgz", - "integrity": "sha512-TU6nAF5SdasnTr28c7e74P4Crbn9o3/zwo1pM22Wvg2i2vlZ4Eelxwu4QT7j21z0sDBlJDEnEZjXTZg2J8WJrg==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.6" - } - }, "node_modules/protobufjs": { "version": "7.6.5", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", @@ -7199,12 +6789,6 @@ "node": ">= 14.0.0" } }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT" - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7280,7 +6864,8 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", - "license": "BSD" + "license": "BSD", + "optional": true }, "node_modules/util-deprecate": { "version": "1.0.2", diff --git a/kits/storage-resize-images/package.json b/kits/storage-resize-images/package.json index 480bebafd..2b18e6cb7 100644 --- a/kits/storage-resize-images/package.json +++ b/kits/storage-resize-images/package.json @@ -25,11 +25,11 @@ "serve": "firebase emulators:start --only functions" }, "dependencies": { - "@genkit-ai/vertexai": "^1.2.0", + "@genkit-ai/google-genai": "^1.24.0", "@google-cloud/storage": "^7.21.0", "firebase-admin": "^13.2.0", "firebase-functions": "7.3.2", - "genkit": "^1.2.0", + "genkit": "^1.24.0", "mkdirp": "^3.0.1", "p-queue": "^6.6.2", "sharp": "^0.34.5", diff --git a/kits/storage-resize-images/src/content-filter.ts b/kits/storage-resize-images/src/content-filter.ts index ff7a957e6..19b4c72d7 100644 --- a/kits/storage-resize-images/src/content-filter.ts +++ b/kits/storage-resize-images/src/content-filter.ts @@ -15,7 +15,7 @@ */ import * as fs from "node:fs"; -import vertexAI, { gemini } from "@genkit-ai/vertexai"; +import { vertexAI } from "@genkit-ai/google-genai"; import { genkit, z } from "genkit"; import type { SafetyThreshold } from "./export-config"; import { GLOBAL_RETRY_QUEUE } from "./global"; @@ -29,6 +29,8 @@ const HARM_CATEGORIES = [ ] as const; /** Similar price to previous `gemini-2.5-flash`, better quality than 2.5 Flash. Higher Flash/Lite tiers cost more. */ const CONTENT_FILTER_MODEL = "gemini-3.1-flash-lite"; +/** Vertex serves this model on `global` / `us` / `eu`, not single-region endpoints like `us-central1`. Old `@genkit-ai/vertexai` rejects `global`; `@genkit-ai/google-genai` accepts it. */ +const VERTEX_CONTENT_FILTER_LOCATION = "global"; const RETRY_BASE_MS = 500; const RETRY_JITTER_MS = 200; const RETRY_MAX_MS = 5000; @@ -80,17 +82,12 @@ export async function checkImageContent( if (filterLevel === null && prompt === null) { return true; } - if (!location) { - throw new Error("FUNCTION_REGION is required for Vertex AI filtering."); - } - const imageBuffer = fs.readFileSync(localOriginalFile); const dataUrl = createImageDataUrl(imageBuffer, contentType); const ai = genkit({ plugins: [ vertexAI({ - location, - models: [CONTENT_FILTER_MODEL], + location: VERTEX_CONTENT_FILTER_LOCATION, }), ], }); @@ -114,7 +111,7 @@ export async function checkImageContent( try { const result = await ai.generate({ - model: gemini(CONTENT_FILTER_MODEL), + model: vertexAI.model(CONTENT_FILTER_MODEL), messages: [ { role: "user", From 662d7b7401f5a72818f510f2c2688cf6910a9ee7 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Tue, 18 Aug 2026 15:14:41 +0100 Subject: [PATCH 5/7] refactor(genai-chatbot): drop knownIds allowlist googleAI.model()/vertexAI.model() already resolve any id. --- .../src/generative-client/genkit.ts | 44 +++---------------- .../tests/generative-client.test.ts | 31 +++++++++---- 2 files changed, 28 insertions(+), 47 deletions(-) diff --git a/kits/firestore-genai-chatbot/src/generative-client/genkit.ts b/kits/firestore-genai-chatbot/src/generative-client/genkit.ts index ecef43df5..6a374487e 100644 --- a/kits/firestore-genai-chatbot/src/generative-client/genkit.ts +++ b/kits/firestore-genai-chatbot/src/generative-client/genkit.ts @@ -104,42 +104,16 @@ export class GenkitDiscussionClient extends DiscussionClient< } /** - * Resolves a Genkit model reference for the configured provider. - * - * Known ids are registered first so version aliases still match. Unknown - * ids fall through to `googleAI.model()` / `vertexAI.model()` so current - * Gemini releases work without a package update. + * Resolves a Genkit model reference via `googleAI.model()` / `vertexAI.model()`. + * Any id is passed through so current Gemini releases work without a package update. */ static createModelReference( model: string, provider: string ): ModelReference { - const isGoogleAi = provider === "google-ai"; - const pluginName = isGoogleAi ? "googleai" : "vertexai"; - const knownIds = [ - "gemini-3.6-flash", - "gemini-3.5-flash", - "gemini-3.5-flash-lite", - "gemini-3.1-flash-lite", - "gemini-3.1-pro-preview", - "gemini-2.5-flash-lite", - "gemini-2.5-flash", - "gemini-2.5-pro", - ] as const; - - const modelReferences = knownIds.map((id) => - isGoogleAi ? googleAI.model(id) : vertexAI.model(id) - ); - - for (const modelReference of modelReferences) { - if (modelReference.name === `${pluginName}/${model}`) { - return modelReference; - } - if (modelReference.info?.versions?.includes(model)) { - return modelReference.withVersion(model); - } - } - return isGoogleAi ? googleAI.model(model) : vertexAI.model(model); + return provider === "google-ai" + ? googleAI.model(model) + : vertexAI.model(model); } private createGenerateOptions( @@ -168,13 +142,7 @@ export class GenkitDiscussionClient extends DiscussionClient< static shouldUseGenkitClient(config: ResolvedGenaiChatbotConfig): boolean { const shouldReturnMultipleCandidates = config.candidateCount && config.candidateCount > 1; - return ( - !shouldReturnMultipleCandidates && - !!GenkitDiscussionClient.createModelReference( - config.model, - config.provider - ) - ); + return !shouldReturnMultipleCandidates; } async generateResponse( diff --git a/kits/firestore-genai-chatbot/tests/generative-client.test.ts b/kits/firestore-genai-chatbot/tests/generative-client.test.ts index 34c3eb5e5..e89ab680a 100644 --- a/kits/firestore-genai-chatbot/tests/generative-client.test.ts +++ b/kits/firestore-genai-chatbot/tests/generative-client.test.ts @@ -65,19 +65,32 @@ describe("GenkitDiscussionClient.shouldUseGenkitClient", () => { expect(GenkitDiscussionClient.shouldUseGenkitClient(config)).toBe(false); }); - test("true for a current model id that is not in the legacy allowlist", () => { - const config = resolveConfig({ - ...baseInput, - model: "gemini-3.6-flash", - candidateCount: 1, - }); + test("true when a single candidate is requested", () => { + const config = resolveConfig({ ...baseInput, candidateCount: 1 }); expect(GenkitDiscussionClient.shouldUseGenkitClient(config)).toBe(true); - expect(() => + }); +}); + +describe("GenkitDiscussionClient.createModelReference", () => { + test("passes any model id through to the plugin", () => { + expect( GenkitDiscussionClient.createModelReference( "gemini-3.6-flash", "google-ai" - ) - ).not.toThrow(); + ).name + ).toBe("googleai/gemini-3.6-flash"); + expect( + GenkitDiscussionClient.createModelReference( + "gemini-9-flash", + "google-ai" + ).name + ).toBe("googleai/gemini-9-flash"); + expect( + GenkitDiscussionClient.createModelReference( + "gemini-9-flash", + "vertex-ai" + ).name + ).toBe("vertexai/gemini-9-flash"); }); }); From 2cad2db51c14f42d7ec4aa1ca0dbde2b745018ef Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Wed, 19 Aug 2026 13:19:15 +0100 Subject: [PATCH 6/7] fix(kits): pin Vertex AI calls to the global endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemini 3.x is served on the Vertex `global`, `us` and `eu` endpoints only, so the function region is not a usable default for the new `gemini-3.6-flash` default. Translate now calls Vertex at `global`, and the chatbot's VERTEX_AI_MODEL_LOCATION defaults to `global` instead of the function region. Gemini 3.x also deprecates temperature/topP/topK — the Vertex model card for gemini-3.6-flash states custom values are ignored. The params stay for Gemini 2.5 configurations, with the limitation documented. --- kits/firestore-genai-chatbot/README.md | 8 ++++---- kits/firestore-genai-chatbot/src/config.ts | 17 ++++++++++++++++- kits/firestore-translate-text/README.md | 1 + .../src/translate/common.ts | 14 +++++++++++++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/kits/firestore-genai-chatbot/README.md b/kits/firestore-genai-chatbot/README.md index 9f31fd48b..6e3e6a61f 100644 --- a/kits/firestore-genai-chatbot/README.md +++ b/kits/firestore-genai-chatbot/README.md @@ -91,16 +91,16 @@ the CLI connects them to the function at deploy time. | `provider` | `GENERATIVE_AI_PROVIDER` | no | `google-ai` | `google-ai` or `vertex-ai` | | `apiKey` | `API_KEY` | secret | — | Google AI API key | | `model` | `MODEL` | no | `gemini-3.6-flash` | Model id | -| `vertexModelLocation` | `VERTEX_AI_MODEL_LOCATION` | no | `null` | Vertex model region | +| `vertexModelLocation` | `VERTEX_AI_MODEL_LOCATION` | no | `global` | Vertex model region. Gemini 3.x is only served on `global`, `us` and `eu` | | `collectionName` | `COLLECTION_NAME` | no | `generate` | Discussion collection | | `promptField` | `PROMPT_FIELD` | no | `prompt` | Prompt field name | | `responseField` | `RESPONSE_FIELD` | no | `response` | Response field name | | `orderField` | `ORDER_FIELD` | no | `createTime` | Ordering field | | `candidatesField` | `CANDIDATES_FIELD` | no | `candidates` | Candidates field name | | `context` | `CONTEXT` | no | (empty) | System context | -| `temperature` | `TEMPERATURE` | no | (empty) | Sampling temperature | -| `topP` | `TOP_P` | no | (empty) | Top-p | -| `topK` | `TOP_K` | no | (empty) | Top-k | +| `temperature` | `TEMPERATURE` | no | (empty) | Sampling temperature. Ignored by Gemini 3.x | +| `topP` | `TOP_P` | no | (empty) | Top-p. Ignored by Gemini 3.x | +| `topK` | `TOP_K` | no | (empty) | Top-k. Ignored by Gemini 3.x | | `candidateCount` | `CANDIDATE_COUNT` | no | `1` | Candidate count | | `maxOutputTokens` | `MAX_OUTPUT_TOKENS` | no | (empty) | Max output tokens | | `enableOverrides` | `ENABLE_DISCUSSION_OPTION_OVERRIDES` | no | `false` | Per-discussion option overrides | diff --git a/kits/firestore-genai-chatbot/src/config.ts b/kits/firestore-genai-chatbot/src/config.ts index 713fff00a..89b187c8e 100644 --- a/kits/firestore-genai-chatbot/src/config.ts +++ b/kits/firestore-genai-chatbot/src/config.ts @@ -85,8 +85,17 @@ const params = { }), apiKey: defineSecret("API_KEY"), model: defineString("MODEL", { default: "gemini-3.6-flash" }), + /** + * Vertex AI location for the model. Defaults to `global` rather than the + * function region: Gemini 3.x is served on the `global`, `us` and `eu` + * endpoints only, so a single region such as `us-central1` returns 404 for the + * default `gemini-3.6-flash`. Set a specific region only with a model that is + * served there (for example a Gemini 2.5 model). + * + * @see https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations + */ vertexModelLocation: defineString("VERTEX_AI_MODEL_LOCATION", { - default: "null", + default: "global", input: select([...VERTEX_MODEL_LOCATION_OPTIONS]), }), collectionName: defineString("COLLECTION_NAME", { default: "generate" }), @@ -97,6 +106,12 @@ const params = { default: "candidates", }), context: defineString("CONTEXT", { default: "" }), + /** + * Sampling controls. Gemini 3.x deprecates `temperature`, `topP` and `topK`; + * the Vertex AI model card for `gemini-3.6-flash` states custom values are + * ignored. They still apply to Gemini 2.5 models, which retire in October + * 2026, so the params are kept for existing configurations. + */ temperature: defineString("TEMPERATURE", { default: "" }), topP: defineString("TOP_P", { default: "" }), topK: defineString("TOP_K", { default: "" }), diff --git a/kits/firestore-translate-text/README.md b/kits/firestore-translate-text/README.md index 3aee2e8d9..060dbc02d 100644 --- a/kits/firestore-translate-text/README.md +++ b/kits/firestore-translate-text/README.md @@ -91,6 +91,7 @@ the CLI connects them to the function at deploy time. | `languagesFieldName` | `LANGUAGES_FIELD_NAME` | no | `languages` | Per-doc languages field | | `provider` | `TRANSLATION_PROVIDER` | yes | — | Translation provider | | `geminiModel` | `GEMINI_MODEL` | no | `gemini-3.6-flash` | Gemini model when used | +| `region` | `FUNCTION_REGION` | no | (platform default) | Function region. Vertex Gemini calls always use the `global` endpoint — Gemini 3.x is not served on single regions | | `googleAiApiKey` | `GOOGLE_AI_API_KEY` | secret | — | Google AI API key (Gemini) | ## Multiple instances diff --git a/kits/firestore-translate-text/src/translate/common.ts b/kits/firestore-translate-text/src/translate/common.ts index 3c188a83f..79376261a 100644 --- a/kits/firestore-translate-text/src/translate/common.ts +++ b/kits/firestore-translate-text/src/translate/common.ts @@ -54,6 +54,18 @@ export class GoogleTranslator implements Translator { } } +/** + * Vertex AI location for Gemini translations. + * + * Gemini 3.x is served on the `global`, `us` and `eu` endpoints only — a single + * region such as `us-central1` returns 404 for `gemini-3.6-flash`. The function + * region is therefore not a safe default. Change this to `us` or `eu` if you + * need to keep requests inside those multi-regions. + * + * @see https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations + */ +const VERTEX_LOCATION = "global"; + export class GenkitTranslator implements Translator { private client: Genkit; private model: ModelReference; @@ -72,7 +84,7 @@ export class GenkitTranslator implements Translator { const plugins = config.geminiProvider === "vertexai" - ? [vertexAI(config.region ? { location: config.region } : {})] + ? [vertexAI({ location: VERTEX_LOCATION })] : [googleAI({ apiKey: config.googleAiApiKey })]; this.client = genkit({ plugins }); From 79a19352f00033dd249787deae131825e7b29b30 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Wed, 19 Aug 2026 19:41:48 +0100 Subject: [PATCH 7/7] fix(kits): address chatbot review from firebase/extensions#2943 Parity with the same review on GoogleCloudPlatform/firebase-extensions#1133, limited to the issues that exist here. The `global` endpoint problem does not apply: the kits legacy Vertex client uses `@google/genai`, which handles `global` natively. - Both legacy clients read `parts[0].text`, which is wrong for thinking models: they can lead with a thought part or answer in a later one. Add a shared `answerText` helper that skips thought parts. - The multi-candidate rule was genuinely divergent here: the client gate read the deploy-time `candidateCount` while the write path read the per-discussion override. A discussion asking for two candidates therefore selected the single-candidate Genkit client and then failed the candidates write. Extract `wantsMultipleCandidates`, thread the effective count through `getGenerativeClient`, and use the one predicate in both places. - `MODEL` had no validation once the allowlist went. Add a shape check on the resolved param and a matching `validationRegex` on the prompt, not an allowlist. - Use a missing-config error message that does not say "Model not found.", which now means something else. --- kits/firestore-genai-chatbot/README.md | 2 +- .../firestore-genai-chatbot/src/candidates.ts | 35 +++++++++ kits/firestore-genai-chatbot/src/config.ts | 31 +++++++- .../src/generate-chat-response.ts | 15 ++-- .../src/generative-client/genkit.ts | 22 ++++-- .../src/generative-client/google_ai.ts | 8 +- .../src/generative-client/index.ts | 13 ++-- .../src/generative-client/parts.ts | 34 +++++++++ .../src/generative-client/vertex_ai.ts | 3 +- .../tests/candidates.test.ts | 74 +++++++++++++++++++ .../tests/generative-client.test.ts | 12 +-- 11 files changed, 218 insertions(+), 31 deletions(-) create mode 100644 kits/firestore-genai-chatbot/src/candidates.ts create mode 100644 kits/firestore-genai-chatbot/src/generative-client/parts.ts create mode 100644 kits/firestore-genai-chatbot/tests/candidates.test.ts diff --git a/kits/firestore-genai-chatbot/README.md b/kits/firestore-genai-chatbot/README.md index 6e3e6a61f..ecf4d8940 100644 --- a/kits/firestore-genai-chatbot/README.md +++ b/kits/firestore-genai-chatbot/README.md @@ -90,7 +90,7 @@ the CLI connects them to the function at deploy time. |---|---|---|---|---| | `provider` | `GENERATIVE_AI_PROVIDER` | no | `google-ai` | `google-ai` or `vertex-ai` | | `apiKey` | `API_KEY` | secret | — | Google AI API key | -| `model` | `MODEL` | no | `gemini-3.6-flash` | Model id | +| `model` | `MODEL` | no | `gemini-3.6-flash` | Model id. Shape-checked at deploy time; not verified against the provider | | `vertexModelLocation` | `VERTEX_AI_MODEL_LOCATION` | no | `global` | Vertex model region. Gemini 3.x is only served on `global`, `us` and `eu` | | `collectionName` | `COLLECTION_NAME` | no | `generate` | Discussion collection | | `promptField` | `PROMPT_FIELD` | no | `prompt` | Prompt field name | diff --git a/kits/firestore-genai-chatbot/src/candidates.ts b/kits/firestore-genai-chatbot/src/candidates.ts new file mode 100644 index 000000000..3575bd553 --- /dev/null +++ b/kits/firestore-genai-chatbot/src/candidates.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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. + */ + +/** + * Whether this configuration asks for multiple candidate responses. + * + * The client-selection gate and the Firestore write path must agree: the Genkit + * client only serves single-candidate configs, and writing the `candidates` + * field requires a field name to write it to. Keeping one predicate stops the + * two from drifting, which would either waste a multi-candidate request or send + * a single-candidate config down the legacy clients. + */ +export function wantsMultipleCandidates(config: { + candidateCount?: number; + candidatesField?: string; +}): boolean { + return ( + !!config.candidatesField && + !!config.candidateCount && + config.candidateCount > 1 + ); +} diff --git a/kits/firestore-genai-chatbot/src/config.ts b/kits/firestore-genai-chatbot/src/config.ts index 89b187c8e..5ab8ccda2 100644 --- a/kits/firestore-genai-chatbot/src/config.ts +++ b/kits/firestore-genai-chatbot/src/config.ts @@ -29,6 +29,13 @@ import { type SafetySetting, } from "./export-config"; +/** + * Shape check only — model ids are not validated against the provider, so an id + * that exists but is not served fails at request time. This catches typos like + * `gemini 3.6-flash` at deploy time instead of on every write. + */ +const MODEL_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9.\-_/]*$/; + const GENERATIVE_AI_PROVIDER_OPTIONS = ["google-ai", "vertex-ai"] as const; const VERTEX_MODEL_LOCATION_OPTIONS = [ "null", @@ -84,7 +91,17 @@ const params = { input: select([...GENERATIVE_AI_PROVIDER_OPTIONS]), }), apiKey: defineSecret("API_KEY"), - model: defineString("MODEL", { default: "gemini-3.6-flash" }), + model: defineString("MODEL", { + default: "gemini-3.6-flash", + input: { + text: { + example: "gemini-3.6-flash", + validationRegex: MODEL_ID_PATTERN.source, + validationErrorMessage: + "Model ids have no spaces, for example 'gemini-3.6-flash'.", + }, + }, + }), /** * Vertex AI location for the model. Defaults to `global` rather than the * function region: Gemini 3.x is served on the `global`, `us` and `eu` @@ -144,6 +161,16 @@ const params = { /** The secret bound on the function so its value is available at runtime. */ export const apiKeySecret = params.apiKey; +/** Rejects a model id that cannot be a model id at all. */ +function requireModelId(model: string): string { + if (!MODEL_ID_PATTERN.test(model)) { + throw new Error( + `MODEL must be a model id with no spaces, for example 'gemini-3.6-flash'. Received: '${model}'` + ); + } + return model; +} + /** Coerce an empty-string param value to `undefined`. */ function optional(value: string): string | undefined { return value.length > 0 ? value : undefined; @@ -179,7 +206,7 @@ export function configFromEnv(): GenaiChatbotConfig { (optional(params.provider.value()) as GenerativeAIProvider) ?? GenerativeAIProvider.GOOGLE_AI, apiKey: params.apiKey.value(), - model: params.model.value(), + model: requireModelId(params.model.value()), vertexModelLocation: vertexModelLocation === "null" ? undefined : vertexModelLocation, projectId: getProjectId(), diff --git a/kits/firestore-genai-chatbot/src/generate-chat-response.ts b/kits/firestore-genai-chatbot/src/generate-chat-response.ts index 3171e831e..c94d32fdc 100644 --- a/kits/firestore-genai-chatbot/src/generate-chat-response.ts +++ b/kits/firestore-genai-chatbot/src/generate-chat-response.ts @@ -15,6 +15,7 @@ */ import type { DocumentSnapshot } from "firebase-admin/firestore"; +import { wantsMultipleCandidates } from "./candidates"; import type { ResolvedGenaiChatbotConfig } from "./export-config"; import { fetchDiscussionOptions, fetchHistory } from "./firestore"; import { getGenerativeClient } from "./generative-client"; @@ -53,12 +54,16 @@ export function createGenerateChatResponse(config: ResolvedGenaiChatbotConfig) { requestOptions = { ...requestOptions, ...discussionOptions }; } - const shouldAddCandidatesField = - config.candidatesField && - requestOptions.candidateCount && - requestOptions.candidateCount > 1; + // Per-discussion overrides can raise the candidate count, so the client + // gate and this write decision must both use the effective value. + const candidateCount = + requestOptions.candidateCount ?? config.candidateCount; + const shouldAddCandidatesField = wantsMultipleCandidates({ + candidateCount, + candidatesField: config.candidatesField, + }); - const discussionClient = getGenerativeClient(config); + const discussionClient = getGenerativeClient(config, candidateCount); const result = await discussionClient.send(prompt, requestOptions); const response = result.response; diff --git a/kits/firestore-genai-chatbot/src/generative-client/genkit.ts b/kits/firestore-genai-chatbot/src/generative-client/genkit.ts index 6a374487e..5944aecb2 100644 --- a/kits/firestore-genai-chatbot/src/generative-client/genkit.ts +++ b/kits/firestore-genai-chatbot/src/generative-client/genkit.ts @@ -30,6 +30,7 @@ import { } from "genkit"; import { logger as genkitLogger } from "genkit/logging"; import type { GenkitPluginV2 } from "genkit/plugin"; +import { wantsMultipleCandidates } from "../candidates"; import type { ResolvedGenaiChatbotConfig } from "../export-config"; import { logger } from "../logger"; import { @@ -120,7 +121,7 @@ export class GenkitDiscussionClient extends DiscussionClient< config: ResolvedGenaiChatbotConfig ): GenerateOptions { if (!config.model) { - throw new Error("Model not found."); + throw new Error("Model must be specified in the configuration."); } return { @@ -138,11 +139,20 @@ export class GenkitDiscussionClient extends DiscussionClient< }; } - /** Whether the Genkit client can serve this config (single candidate). */ - static shouldUseGenkitClient(config: ResolvedGenaiChatbotConfig): boolean { - const shouldReturnMultipleCandidates = - config.candidateCount && config.candidateCount > 1; - return !shouldReturnMultipleCandidates; + /** + * Whether the Genkit client can serve this request (single candidate). + * + * `candidateCount` is passed separately because per-discussion overrides can + * raise it above the deploy-time value. + */ + static shouldUseGenkitClient( + config: ResolvedGenaiChatbotConfig, + candidateCount = config.candidateCount + ): boolean { + return !wantsMultipleCandidates({ + candidateCount, + candidatesField: config.candidatesField, + }); } async generateResponse( diff --git a/kits/firestore-genai-chatbot/src/generative-client/google_ai.ts b/kits/firestore-genai-chatbot/src/generative-client/google_ai.ts index 02c2aea3b..631593d13 100644 --- a/kits/firestore-genai-chatbot/src/generative-client/google_ai.ts +++ b/kits/firestore-genai-chatbot/src/generative-client/google_ai.ts @@ -17,6 +17,7 @@ import { GoogleGenerativeAI, type SafetySetting } from "@google/generative-ai"; import { logger } from "../logger"; import { DiscussionClient, type Message } from "./base_class"; +import { answerText } from "./parts"; interface GeminiChatOptions { history?: Message[]; @@ -106,7 +107,7 @@ export class GeminiDiscussionClient extends DiscussionClient< ); }); - const text = result.response.text(); + const text = answerText(result.response.candidates?.[0]?.content?.parts); if (!text) { throw new Error("No text returned candidate"); @@ -115,8 +116,9 @@ export class GeminiDiscussionClient extends DiscussionClient< return { response: text, candidates: - result.response.candidates?.map((c) => c.content.parts[0].text ?? "") ?? - [], + result.response.candidates?.map( + (c) => answerText(c.content.parts) ?? "" + ) ?? [], safetyMetadata: result.response.promptFeedback, history, }; diff --git a/kits/firestore-genai-chatbot/src/generative-client/index.ts b/kits/firestore-genai-chatbot/src/generative-client/index.ts index 996f9125e..bd01ffba8 100644 --- a/kits/firestore-genai-chatbot/src/generative-client/index.ts +++ b/kits/firestore-genai-chatbot/src/generative-client/index.ts @@ -29,17 +29,20 @@ import { VertexDiscussionClient } from "./vertex_ai"; type Client = Genkit | GoogleGenAI | GoogleGenerativeAI; /** - * Selects and constructs the generative client for a resolved config. Prefers - * the Genkit client when it can serve the request (single candidate, known - * model), falling back to the provider-specific SDK clients. + * Selects and constructs the generative client for a request. Prefers the + * Genkit client when it can serve the request (single candidate), falling back + * to the provider-specific SDK clients. * * @param config - The resolved chatbot configuration. + * @param candidateCount - Effective candidate count for this request, which + * per-discussion overrides can raise above the deploy-time value. * @returns A ready-to-use discussion client. */ export const getGenerativeClient = ( - config: ResolvedGenaiChatbotConfig + config: ResolvedGenaiChatbotConfig, + candidateCount = config.candidateCount ): DiscussionClient => { - if (GenkitDiscussionClient.shouldUseGenkitClient(config)) { + if (GenkitDiscussionClient.shouldUseGenkitClient(config, candidateCount)) { return new GenkitDiscussionClient(config); } diff --git a/kits/firestore-genai-chatbot/src/generative-client/parts.ts b/kits/firestore-genai-chatbot/src/generative-client/parts.ts new file mode 100644 index 000000000..a89cfd654 --- /dev/null +++ b/kits/firestore-genai-chatbot/src/generative-client/parts.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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. + */ + +/** A response part as far as candidate parsing is concerned. */ +export interface TextPart { + text?: string; + thought?: boolean; +} + +/** + * First non-thought text part of a candidate. + * + * Reading `parts[0].text` is not reliable for thinking models: they can lead + * with thought parts, or put the answer in a later part. Thought parts carry + * `thought: true`, which the pinned `@google/generative-ai` version does not + * type, so callers pass their own part shape in. + */ +export function answerText(parts?: TextPart[]): string | undefined { + return parts?.find((part) => !part.thought && typeof part.text === "string") + ?.text; +} diff --git a/kits/firestore-genai-chatbot/src/generative-client/vertex_ai.ts b/kits/firestore-genai-chatbot/src/generative-client/vertex_ai.ts index 4c6d23996..8213f91f4 100644 --- a/kits/firestore-genai-chatbot/src/generative-client/vertex_ai.ts +++ b/kits/firestore-genai-chatbot/src/generative-client/vertex_ai.ts @@ -22,6 +22,7 @@ import { } from "@google/genai"; import { logger } from "../logger"; import { DiscussionClient, type Message } from "./base_class"; +import { answerText } from "./parts"; interface GeminiChatOptions { history?: Message[]; @@ -128,7 +129,7 @@ export class VertexDiscussionClient extends DiscussionClient< } const candidates = result.candidates - .map((candidate) => candidate.content?.parts?.[0]?.text) + .map((candidate) => answerText(candidate.content?.parts)) .filter((text): text is string => typeof text === "string"); if (candidates.length === 0) { diff --git a/kits/firestore-genai-chatbot/tests/candidates.test.ts b/kits/firestore-genai-chatbot/tests/candidates.test.ts new file mode 100644 index 000000000..38f6d576a --- /dev/null +++ b/kits/firestore-genai-chatbot/tests/candidates.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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 { describe, expect, it } from "vitest"; +import { wantsMultipleCandidates } from "../src/candidates"; +import { answerText } from "../src/generative-client/parts"; + +describe("wantsMultipleCandidates", () => { + it("is true for a count above one with a field to write to", () => { + expect( + wantsMultipleCandidates({ + candidateCount: 2, + candidatesField: "candidates", + }) + ).toBe(true); + }); + + it("is false for a single candidate", () => { + expect( + wantsMultipleCandidates({ + candidateCount: 1, + candidatesField: "candidates", + }) + ).toBe(false); + }); + + it("is false without a candidates field, so the request is not wasted", () => { + expect(wantsMultipleCandidates({ candidateCount: 2 })).toBe(false); + }); + + it("is false for an unset count", () => { + expect(wantsMultipleCandidates({ candidatesField: "candidates" })).toBe( + false + ); + }); +}); + +describe("answerText", () => { + it("returns the only text part", () => { + expect(answerText([{ text: "answer" }])).toBe("answer"); + }); + + it("skips a leading thought part", () => { + expect( + answerText([ + { text: "thinking out loud", thought: true }, + { text: "answer" }, + ]) + ).toBe("answer"); + }); + + it("returns undefined when every part is a thought", () => { + expect(answerText([{ text: "thinking", thought: true }])).toBeUndefined(); + }); + + it("returns undefined for missing or empty parts", () => { + expect(answerText(undefined)).toBeUndefined(); + expect(answerText([])).toBeUndefined(); + expect(answerText([{}])).toBeUndefined(); + }); +}); diff --git a/kits/firestore-genai-chatbot/tests/generative-client.test.ts b/kits/firestore-genai-chatbot/tests/generative-client.test.ts index e89ab680a..c818ffef7 100644 --- a/kits/firestore-genai-chatbot/tests/generative-client.test.ts +++ b/kits/firestore-genai-chatbot/tests/generative-client.test.ts @@ -80,16 +80,12 @@ describe("GenkitDiscussionClient.createModelReference", () => { ).name ).toBe("googleai/gemini-3.6-flash"); expect( - GenkitDiscussionClient.createModelReference( - "gemini-9-flash", - "google-ai" - ).name + GenkitDiscussionClient.createModelReference("gemini-9-flash", "google-ai") + .name ).toBe("googleai/gemini-9-flash"); expect( - GenkitDiscussionClient.createModelReference( - "gemini-9-flash", - "vertex-ai" - ).name + GenkitDiscussionClient.createModelReference("gemini-9-flash", "vertex-ai") + .name ).toBe("vertexai/gemini-9-flash"); }); });