From 070ab04689e4312fb6430cd7e60857d7809d8582 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Wed, 2 Sep 2026 15:01:55 +0100 Subject: [PATCH 1/5] fix(firestore-vector-search): restore the extension's embedding defaults The kit swapped OpenAI embeddings to text-embedding-3-small pinned at 512 dimensions with a batch size of 1. The extension used text-embedding-ada-002 at its native 1536 dimensions with a batch size of 16, so vectors written by an installed instance and vectors written by the kit were not comparable. A newer model is not on its own a reason to diverge, so restore the extension's model, dimensionality and batch size. The Genkit client truncated each embedding to config.dimension, which the extension never did. dimensionFor() returns a fixed 768 for both gemini and vertex, the only providers that reach this client, and the embedder is asked for exactly that many dimensions, so the branch is unreachable. Drop it. Also adds the OpenAI client unit tests the kit was missing. --- kits/firestore-vector-search/README.md | 21 ++-- .../src/embeddings/client/genkit.ts | 10 +- .../src/embeddings/client/text/open_ai.ts | 5 +- .../tests/embeddings.test.ts | 108 +++++++++++++++--- 4 files changed, 107 insertions(+), 37 deletions(-) diff --git a/kits/firestore-vector-search/README.md b/kits/firestore-vector-search/README.md index 0552ca77b5..dc62039456 100644 --- a/kits/firestore-vector-search/README.md +++ b/kits/firestore-vector-search/README.md @@ -167,18 +167,6 @@ Selecting `multimodal` deploys, and then every embedding attempt throws multimodal image embedding, including reading images out of Cloud Storage, has no equivalent here. If you use it, stay on the extension. -### OpenAI embeddings are a different model and a different size - -`EMBEDDING_PROVIDER: openai` used `text-embedding-ada-002` and stored the full -1536-dimension vector, while the Firestore index it created was declared with 512 -dimensions. The kit uses `text-embedding-3-small` at 512 dimensions, which matches -the index. - -Vectors from the two models are not comparable, and the existing index is reused -as-is because the "does this index already exist" check only looks at the field -path, not the dimension. Re-embed the whole collection after you switch, and -delete the old vector index first if it was created with a different dimension. - ### You set `INSTANCE_ID` yourself, and it names the query collection The extension derived its instance id at install and used it for the query @@ -315,6 +303,15 @@ for; the Firebase CLI grants these for you. `EUCLIDEAN`, `DOT_PRODUCT`, default `COSINE`) behave as before. - Gemini and Vertex AI embeddings are still `gemini-embedding-001` at 768 dimensions. +- OpenAI embeddings are still `text-embedding-ada-002` at its native 1536 + dimensions, so vectors already written by an installed instance stay + comparable with the ones the kit writes. The vector index the kit creates for + `EMBEDDING_PROVIDER: openai` is still declared with 512 dimensions, as the + extension declared it, so it does not cover those 1536-dimension vectors and + `findNearest` fails against it. That mismatch is the extension's, and it is + tracked in [firebase/extensions#3029](https://github.com/firebase/extensions/issues/3029); + until it is resolved, create the 1536-dimension index yourself if you query an + OpenAI-embedded collection. - A custom endpoint still receives `{ batch: [...] }` and must return `{ embeddings: [[...]] }`, and still requires all three of `CUSTOM_EMBEDDINGS_ENDPOINT`, `CUSTOM_EMBEDDINGS_BATCH_SIZE` and diff --git a/kits/firestore-vector-search/src/embeddings/client/genkit.ts b/kits/firestore-vector-search/src/embeddings/client/genkit.ts index 399f208df2..af867eefb0 100644 --- a/kits/firestore-vector-search/src/embeddings/client/genkit.ts +++ b/kits/firestore-vector-search/src/embeddings/client/genkit.ts @@ -22,11 +22,9 @@ import { BaseEmbedClient } from "./base_class"; export class GenkitEmbedClient extends BaseEmbedClient { private readonly client: Genkit; private readonly embedder: EmbedderReference; - private readonly dimension: number; constructor(config: ResolvedVectorSearchConfig) { super(1); - this.dimension = config.dimension; const isVertex = config.embeddingProvider === "vertex"; this.embedder = isVertex ? vertexAI.embedder("gemini-embedding-001", { @@ -49,12 +47,6 @@ export class GenkitEmbedClient extends BaseEmbedClient { embedder: this.embedder, content: [...inputs], }); - return results.map((result) => { - const embedding = result.embedding; - if (embedding.length <= this.dimension) { - return embedding; - } - return embedding.slice(0, this.dimension); - }); + return results.map((result) => result.embedding); } } diff --git a/kits/firestore-vector-search/src/embeddings/client/text/open_ai.ts b/kits/firestore-vector-search/src/embeddings/client/text/open_ai.ts index 978c5df4ae..62a94b125f 100644 --- a/kits/firestore-vector-search/src/embeddings/client/text/open_ai.ts +++ b/kits/firestore-vector-search/src/embeddings/client/text/open_ai.ts @@ -22,7 +22,7 @@ export class OpenAiEmbedClient extends BaseEmbedClient { private readonly client: OpenAI; constructor(config: ResolvedVectorSearchConfig) { - super(1); + super(16); if (!config.openAiApiKey) { throw new Error("OpenAI embeddings require OPENAI_API_KEY"); } @@ -31,9 +31,8 @@ export class OpenAiEmbedClient extends BaseEmbedClient { async getEmbeddings(inputs: ReadonlyArray): Promise { const results = await this.client.embeddings.create({ - model: "text-embedding-3-small", + model: "text-embedding-ada-002", input: [...inputs], - dimensions: 512, }); return results.data.map((result) => result.embedding); } diff --git a/kits/firestore-vector-search/tests/embeddings.test.ts b/kits/firestore-vector-search/tests/embeddings.test.ts index e464abe6c5..a8853aa9c8 100644 --- a/kits/firestore-vector-search/tests/embeddings.test.ts +++ b/kits/firestore-vector-search/tests/embeddings.test.ts @@ -31,10 +31,25 @@ vi.mock("@genkit-ai/google-genai", () => ({ }), })); +const { embeddingsCreate, openAiConstructor } = vi.hoisted(() => ({ + embeddingsCreate: vi.fn(), + openAiConstructor: vi.fn(), +})); + +vi.mock("openai", () => ({ + default: class { + embeddings = { create: embeddingsCreate }; + constructor(options: unknown) { + openAiConstructor(options); + } + }, +})); + import { googleAI, vertexAI } from "@genkit-ai/google-genai"; import { genkit } from "genkit"; import { GenkitEmbedClient } from "../src/embeddings/client/genkit"; +import { OpenAiEmbedClient } from "../src/embeddings/client/text/open_ai"; import { type ResolvedVectorSearchConfig, resolveVectorSearchConfig, @@ -117,19 +132,7 @@ describe("GenkitEmbedClient", () => { ]); }); - test("truncates embeddings longer than the configured dimension", async () => { - const client = new GenkitEmbedClient( - config({ - embeddingProvider: "custom", - customEmbeddingsDimension: 2, - }) - ); - embedMany.mockResolvedValueOnce([{ embedding: [1, 2, 3, 4] }]); - - await expect(client.getEmbeddings(["input"])).resolves.toEqual([[1, 2]]); - }); - - test("leaves embeddings shorter than the dimension untouched", async () => { + test("returns the embeddings as the embedder produced them", async () => { const client = new GenkitEmbedClient(config()); embedMany.mockResolvedValueOnce([{ embedding: [1, 2, 3] }]); @@ -176,3 +179,82 @@ describe("GenkitEmbedClient", () => { }); }); }); + +describe("OpenAiEmbedClient", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + function openAiConfig(): ResolvedVectorSearchConfig { + return config({ + embeddingProvider: "openai", + openAiApiKey: "test-openai-key", + }); + } + + describe("constructor", () => { + test("builds the client with the configured API key", () => { + new OpenAiEmbedClient(openAiConfig()); + + expect(openAiConstructor).toHaveBeenCalledWith({ + apiKey: "test-openai-key", + }); + }); + + test("throws when no API key is configured", () => { + expect( + () => new OpenAiEmbedClient(config({ embeddingProvider: "openai" })) + ).toThrow("OpenAI embeddings require OPENAI_API_KEY"); + }); + + test("embeds sixteen inputs per batch", () => { + expect(new OpenAiEmbedClient(openAiConfig()).batchSize).toBe(16); + }); + }); + + describe("getEmbeddings", () => { + test("requests text-embedding-ada-002 at its native dimension", async () => { + const client = new OpenAiEmbedClient(openAiConfig()); + embeddingsCreate.mockResolvedValueOnce({ + data: [{ embedding: [1, 2, 3] }, { embedding: [4, 5, 6] }], + }); + + const inputs = ["input1", "input2"]; + + await expect(client.getEmbeddings(inputs)).resolves.toEqual([ + [1, 2, 3], + [4, 5, 6], + ]); + expect(embeddingsCreate).toHaveBeenCalledWith({ + model: "text-embedding-ada-002", + input: inputs, + }); + }); + + test("throws when embedding fails", async () => { + const client = new OpenAiEmbedClient(openAiConfig()); + embeddingsCreate.mockRejectedValueOnce(new Error("Embedding failed")); + + await expect(client.getEmbeddings(["input"])).rejects.toThrow( + "Embedding failed" + ); + }); + }); + + describe("getSingleEmbedding", () => { + test("returns a single embedding for an input", async () => { + const client = new OpenAiEmbedClient(openAiConfig()); + embeddingsCreate.mockResolvedValueOnce({ + data: [{ embedding: [7, 8, 9] }], + }); + + await expect(client.getSingleEmbedding("input1")).resolves.toEqual([ + 7, 8, 9, + ]); + expect(embeddingsCreate).toHaveBeenCalledWith({ + model: "text-embedding-ada-002", + input: ["input1"], + }); + }); + }); +}); From 5b6831ffdc639d8f5653a6fc0c30f1495ef50122 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Wed, 2 Sep 2026 15:04:37 +0100 Subject: [PATCH 2/5] docs(firestore-vector-search): correct the restored OpenAI parity notes The intro to the differences section still warned that the embedding providers changed, which after restoring text-embedding-ada-002 only holds for multimodal. The new Unchanged bullet pointed at #3029 for the extension's index/vector size mismatch, and this PR closes that issue. --- kits/firestore-vector-search/README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/kits/firestore-vector-search/README.md b/kits/firestore-vector-search/README.md index dc62039456..e652272a37 100644 --- a/kits/firestore-vector-search/README.md +++ b/kits/firestore-vector-search/README.md @@ -156,7 +156,7 @@ This kit is version 0.1.3 of the extension repackaged as an npm package, and it the least literal of the ports. The seven functions, the Firestore vector index, the query document collection and the callable all survive with their names and settings intact, so a `.env` copied from your installed instance needs no value -changes. The embedding providers, the backfill, and the shape of the status field +changes. Multimodal embedding, the backfill, and the shape of the status field written onto your documents all changed, so read this before you point the kit at a collection an installed instance has already embedded. @@ -306,12 +306,10 @@ for; the Firebase CLI grants these for you. - OpenAI embeddings are still `text-embedding-ada-002` at its native 1536 dimensions, so vectors already written by an installed instance stay comparable with the ones the kit writes. The vector index the kit creates for - `EMBEDDING_PROVIDER: openai` is still declared with 512 dimensions, as the - extension declared it, so it does not cover those 1536-dimension vectors and - `findNearest` fails against it. That mismatch is the extension's, and it is - tracked in [firebase/extensions#3029](https://github.com/firebase/extensions/issues/3029); - until it is resolved, create the 1536-dimension index yourself if you query an - OpenAI-embedded collection. + `EMBEDDING_PROVIDER: openai` is still declared with 512 dimensions, exactly as + the extension declared it, so it does not cover those 1536-dimension vectors + and `findNearest` fails against it. Create the 1536-dimension index yourself if + you query an OpenAI-embedded collection. - A custom endpoint still receives `{ batch: [...] }` and must return `{ embeddings: [[...]] }`, and still requires all three of `CUSTOM_EMBEDDINGS_ENDPOINT`, `CUSTOM_EMBEDDINGS_BATCH_SIZE` and From 8468929047853f4d6cd4d271d2f46eb200f69067 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Wed, 2 Sep 2026 16:13:08 +0100 Subject: [PATCH 3/5] fix(firestore-vector-search): keep the Genkit dimension truncation Removing the truncation was wrong. Live testing against corie-testing shows genkit does not apply outputDimensionality when it is set on the embedder reference, so gemini-embedding-001 returns its full 3072-dimension vector. Firestore refuses vectors above 2048 dimensions, so every gemini and vertex embed fails with "3 INVALID_ARGUMENT: Vectors must be at most 2048 dimensions" and the document is marked ERROR. The extension pins the same @genkit-ai/google-genai range and sets outputDimensionality the same way, so its gemini and vertex paths fail identically. Truncating to config.dimension is what makes the kit's default provider work, so it stays, and the README now explains why the kit diverges here. --- kits/firestore-vector-search/README.md | 14 ++++++++++++++ .../src/embeddings/client/genkit.ts | 10 +++++++++- .../tests/embeddings.test.ts | 16 +++++++++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/kits/firestore-vector-search/README.md b/kits/firestore-vector-search/README.md index e652272a37..0d4a9faf66 100644 --- a/kits/firestore-vector-search/README.md +++ b/kits/firestore-vector-search/README.md @@ -167,6 +167,20 @@ Selecting `multimodal` deploys, and then every embedding attempt throws multimodal image embedding, including reading images out of Cloud Storage, has no equivalent here. If you use it, stay on the extension. +### Gemini and Vertex AI embeddings are truncated to 768 dimensions + +Both the extension and the kit ask for `gemini-embedding-001` with +`outputDimensionality: 768`, set on the embedder reference. Genkit does not apply +it there, so the model returns its full 3072-dimension vector. Firestore refuses +any vector above 2048 dimensions, so on the extension every gemini and vertex +embed fails with `Vectors must be at most 2048 dimensions` and the document is +marked `ERROR`. + +The kit truncates each returned embedding to 768 before writing it, which is the +dimension both the extension and the kit declare their vector index with, so the +default provider works. This is the one place the kit deliberately does not match +the extension's behaviour, because matching it means writing nothing at all. + ### You set `INSTANCE_ID` yourself, and it names the query collection The extension derived its instance id at install and used it for the query diff --git a/kits/firestore-vector-search/src/embeddings/client/genkit.ts b/kits/firestore-vector-search/src/embeddings/client/genkit.ts index af867eefb0..399f208df2 100644 --- a/kits/firestore-vector-search/src/embeddings/client/genkit.ts +++ b/kits/firestore-vector-search/src/embeddings/client/genkit.ts @@ -22,9 +22,11 @@ import { BaseEmbedClient } from "./base_class"; export class GenkitEmbedClient extends BaseEmbedClient { private readonly client: Genkit; private readonly embedder: EmbedderReference; + private readonly dimension: number; constructor(config: ResolvedVectorSearchConfig) { super(1); + this.dimension = config.dimension; const isVertex = config.embeddingProvider === "vertex"; this.embedder = isVertex ? vertexAI.embedder("gemini-embedding-001", { @@ -47,6 +49,12 @@ export class GenkitEmbedClient extends BaseEmbedClient { embedder: this.embedder, content: [...inputs], }); - return results.map((result) => result.embedding); + return results.map((result) => { + const embedding = result.embedding; + if (embedding.length <= this.dimension) { + return embedding; + } + return embedding.slice(0, this.dimension); + }); } } diff --git a/kits/firestore-vector-search/tests/embeddings.test.ts b/kits/firestore-vector-search/tests/embeddings.test.ts index a8853aa9c8..d595d033cc 100644 --- a/kits/firestore-vector-search/tests/embeddings.test.ts +++ b/kits/firestore-vector-search/tests/embeddings.test.ts @@ -132,7 +132,21 @@ describe("GenkitEmbedClient", () => { ]); }); - test("returns the embeddings as the embedder produced them", async () => { + // gemini-embedding-001 ignores the embedder's outputDimensionality and + // returns its full 3072-dimension vector, which Firestore rejects outright. + test("truncates embeddings longer than the configured dimension", async () => { + const client = new GenkitEmbedClient( + config({ + embeddingProvider: "custom", + customEmbeddingsDimension: 2, + }) + ); + embedMany.mockResolvedValueOnce([{ embedding: [1, 2, 3, 4] }]); + + await expect(client.getEmbeddings(["input"])).resolves.toEqual([[1, 2]]); + }); + + test("leaves embeddings shorter than the dimension untouched", async () => { const client = new GenkitEmbedClient(config()); embedMany.mockResolvedValueOnce([{ embedding: [1, 2, 3] }]); From 927dd85941072b897983831d1d2c9474a46a1a56 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Wed, 2 Sep 2026 16:34:43 +0100 Subject: [PATCH 4/5] docs(firestore-vector-search): flag the Genkit truncation from Unchanged The Unchanged list claimed 768-dimension parity for gemini and vertex without noting that the kit only reaches 768 by truncating, which the difference entry above explains. --- kits/firestore-vector-search/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kits/firestore-vector-search/README.md b/kits/firestore-vector-search/README.md index 0d4a9faf66..b93deea357 100644 --- a/kits/firestore-vector-search/README.md +++ b/kits/firestore-vector-search/README.md @@ -316,7 +316,8 @@ for; the Firebase CLI grants these for you. - `DEFAULT_QUERY_LIMIT` (default 3) and `DISTANCE_MEASURE` (`COSINE`, `EUCLIDEAN`, `DOT_PRODUCT`, default `COSINE`) behave as before. - Gemini and Vertex AI embeddings are still `gemini-embedding-001` at 768 - dimensions. + dimensions, though the kit has to truncate the model's response to get there. + See *Gemini and Vertex AI embeddings are truncated to 768 dimensions* above. - OpenAI embeddings are still `text-embedding-ada-002` at its native 1536 dimensions, so vectors already written by an installed instance stay comparable with the ones the kit writes. The vector index the kit creates for From db9a76e77c7d2a6ceff01a3a4058b560194a67c9 Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Wed, 2 Sep 2026 17:22:55 +0100 Subject: [PATCH 5/5] docs(firestore-vector-search): changelog the OpenAI model revert The kit's changelog had no entry for a change that alters every OpenAI embedding it writes and leaves earlier kit-written vectors incomparable. --- kits/firestore-vector-search/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/kits/firestore-vector-search/CHANGELOG.md b/kits/firestore-vector-search/CHANGELOG.md index 711eb60d36..4efea603df 100644 --- a/kits/firestore-vector-search/CHANGELOG.md +++ b/kits/firestore-vector-search/CHANGELOG.md @@ -1 +1,2 @@ +- OpenAI embeddings are back on the extension's model and size: `EMBEDDING_PROVIDER: openai` requests `text-embedding-ada-002` at its native 1536 dimensions with a batch size of 16, replacing `text-embedding-3-small` pinned at 512 with a batch size of 1. Vectors written by an earlier version of the kit are not comparable with the ones it writes now, so re-embed the collection after upgrading. The vector index the kit creates for OpenAI is still declared with 512 dimensions, exactly as the extension declared it, so it does not cover the 1536-dimension vectors and `findNearest` fails against it; create the 1536-dimension index yourself if you query an OpenAI-embedded collection. - Initial release of kit, see README for differences between the legacy extension and this kit