diff --git a/docs/reference/memory-config.md b/docs/reference/memory-config.md index 36a1cbfbdcc6..faa3421a3935 100644 --- a/docs/reference/memory-config.md +++ b/docs/reference/memory-config.md @@ -212,7 +212,7 @@ Use `provider: "openai-compatible"` for a generic OpenAI-compatible | Key | Type | Default | Description | | ---------------------- | -------- | ---------------------- | ------------------------------------------- | | `model` | `string` | `gemini-embedding-001` | Also supports `gemini-embedding-2` | - | `outputDimensionality` | `number` | `3072` | For Embedding 2: 768, 1536, or 3072 | + | `outputDimensionality` | `number` | `3072` | 128-3072; recommended: 768, 1536, or 3072 | The legacy `gemini-embedding-2-preview` identifier remains accepted during migration to the stable model. @@ -226,8 +226,10 @@ Use `provider: "openai-compatible"` for a generic OpenAI-compatible configuration. Before this release, the stable model's dimension was omitted from index identity whether `outputDimensionality` was absent or explicitly set. After upgrade, an absent setting resolves to 3072, while an - explicit 768, 1536, or 3072 setting becomes part of the identity. For either - path, check the affected agent with + explicit setting between 128 and 3072 becomes part of the identity. The + default `gemini-embedding-001` keeps its existing identity when this setting + is absent; an explicitly configured value that was previously ignored now + also changes the identity. For either path, check the affected agent with `openclaw memory status --deep --agent `, then rebuild when ready with `openclaw memory index --force --agent `. diff --git a/extensions/google/embedding-provider.test.ts b/extensions/google/embedding-provider.test.ts index f08cb8c40b55..c83d326c859c 100644 --- a/extensions/google/embedding-provider.test.ts +++ b/extensions/google/embedding-provider.test.ts @@ -168,19 +168,71 @@ describe("Gemini embedding provider", () => { }, ); - it("rejects unsupported Gemini 2 output dimensions through provider creation", async () => { + it.each( + ["gemini-embedding-001", "gemini-embedding-2", "gemini-embedding-2-preview"].flatMap((model) => + [128, 512, 1024, 3072].map((dimensions) => [model, dimensions] as const), + ), + )("supports %s with %i output dimensions", async (model, dimensions) => { + const fetchMock = installFetchMock((input) => { + const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url; + return url.endsWith(":batchEmbedContents") + ? { embeddings: [{ values: axisVector(dimensions) }] } + : { embedding: { values: axisVector(dimensions) } }; + }); + const { provider, client } = await createGeminiEmbeddingProvider({ + config: {} as never, + provider: "gemini", + remote: { apiKey: "placeholder" }, + model, + outputDimensionality: dimensions, + fallback: "none", + }); + + expect(client.outputDimensionality).toBe(dimensions); + await expect(provider.embedQuery("query")).resolves.toHaveLength(dimensions); + await expect(provider.embedBatch(["document"])).resolves.toEqual([axisVector(dimensions)]); + expect(fetchJsonBody(fetchMock, 0)).toMatchObject({ outputDimensionality: dimensions }); + expect(fetchJsonBody(fetchMock, 1)).toMatchObject({ + requests: [{ outputDimensionality: dimensions }], + }); + }); + + it.each( + ["gemini-embedding-001", "gemini-embedding-2", "gemini-embedding-2-preview"].flatMap((model) => + [127, 512.5, 3073].map((dimensions) => [model, dimensions] as const), + ), + )("rejects unsupported %s dimension %i before making a request", async (model, dimensions) => { await expect( createGeminiEmbeddingProvider({ config: {} as never, provider: "gemini", remote: { apiKey: "placeholder" }, - model: "gemini-embedding-2", - outputDimensionality: 1024, + model, + outputDimensionality: dimensions, fallback: "none", }), - ).rejects.toThrow(/Valid values: 768, 1536, 3072/); + ).rejects.toThrow(/integer between 128 and 3072/); }); + it.each([ + ["gemini-embedding-001", undefined], + ["gemini-embedding-2", 3072], + ["gemini-embedding-2-preview", 3072], + ] as const)( + "preserves the existing default dimension identity for %s", + async (model, dimensions) => { + const { client } = await createGeminiEmbeddingProvider({ + config: {} as never, + provider: "gemini", + remote: { apiKey: "placeholder" }, + model, + fallback: "none", + }); + + expect(client.outputDimensionality).toBe(dimensions); + }, + ); + it("handles legacy and v2 request/response behavior", async () => { const fetchMock = installFetchMock((input) => { const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url; diff --git a/extensions/google/embedding-provider.ts b/extensions/google/embedding-provider.ts index 51dce5f4ed92..c0165e2a4d91 100644 --- a/extensions/google/embedding-provider.ts +++ b/extensions/google/embedding-provider.ts @@ -55,7 +55,6 @@ type GeminiTaskType = NonNullable = { RETRIEVAL_QUERY: "task: search result | query:", RETRIEVAL_DOCUMENT: "title: none | text:", @@ -165,29 +164,22 @@ export function buildGeminiEmbeddingRequest(params: { return request; } -/** - * Returns true if the given model name is a gemini-embedding-2 variant that - * supports `outputDimensionality` and extended task types. - */ +/** Returns true for Gemini Embedding 2 variants with multimodal and extended task support. */ export function isGeminiEmbedding2Model(model: string): boolean { return GEMINI_EMBEDDING_2_MODELS.has(normalizeGeminiModel(model)); } -/** - * Validate and return the `outputDimensionality` for gemini-embedding-2 models. - * Returns `undefined` for older models (they don't support the param). - */ function resolveGeminiOutputDimensionality(model: string, requested?: number): number | undefined { - if (!isGeminiEmbedding2Model(model)) { + const isEmbedding2 = isGeminiEmbedding2Model(model); + if (!isEmbedding2 && model !== DEFAULT_GEMINI_EMBEDDING_MODEL) { return undefined; } if (requested == null) { - return GEMINI_EMBEDDING_2_DEFAULT_DIMENSIONS; + return isEmbedding2 ? GEMINI_EMBEDDING_2_DEFAULT_DIMENSIONS : undefined; } - const valid: readonly number[] = GEMINI_EMBEDDING_2_VALID_DIMENSIONS; - if (!valid.includes(requested)) { + if (!Number.isInteger(requested) || requested < 128 || requested > 3072) { throw new Error( - `Invalid outputDimensionality ${requested} for ${model}. Valid values: ${valid.join(", ")}`, + `Invalid outputDimensionality ${requested} for ${model}. Use an integer between 128 and 3072.`, ); } return requested; @@ -302,7 +294,6 @@ export async function createGeminiEmbeddingProvider( client.baseUrl, `${client.modelPath}:batchEmbedContents`, ); - const isV2 = isGeminiEmbedding2Model(client.model); const outputDimensionality = client.outputDimensionality; const embedQuery = async ( @@ -320,14 +311,11 @@ export async function createGeminiEmbeddingProvider( model: client.model, role: "query", taskType: options.taskType ?? "RETRIEVAL_QUERY", - outputDimensionality: isV2 ? outputDimensionality : undefined, + outputDimensionality, }), signal: callOptions?.signal, }); - return sanitizeGeminiEmbedding( - readGeminiSingleEmbedding(payload), - isV2 ? outputDimensionality : undefined, - ); + return sanitizeGeminiEmbedding(readGeminiSingleEmbedding(payload), outputDimensionality); }; const embedBatchInputs = async ( @@ -348,16 +336,14 @@ export async function createGeminiEmbeddingProvider( role: "document", modelPath: client.modelPath, taskType: options.taskType ?? "RETRIEVAL_DOCUMENT", - outputDimensionality: isV2 ? outputDimensionality : undefined, + outputDimensionality, }), ), }, signal: callOptions?.signal, }); const embeddings = readGeminiBatchEmbeddings(payload, inputs.length); - return embeddings.map((values) => - sanitizeGeminiEmbedding(values, isV2 ? outputDimensionality : undefined), - ); + return embeddings.map((values) => sanitizeGeminiEmbedding(values, outputDimensionality)); }; const embedBatch = async ( diff --git a/src/config/schema.help.models.ts b/src/config/schema.help.models.ts index 72ad6aab33c6..02ebc148aa01 100644 --- a/src/config/schema.help.models.ts +++ b/src/config/schema.help.models.ts @@ -225,7 +225,7 @@ export const MODEL_FIELD_HELP: Record = { "memory.search.documentInputType": "Optional provider-specific `input_type` value for document and indexing memory embeddings. Use this with OpenAI-compatible asymmetric embedding endpoints that require a passage or document label.", "memory.search.outputDimensionality": - "Provider-specific output vector size override for memory embeddings. Gemini embedding-2 supports 768, 1536, or 3072; Bedrock families such as Titan V2, Cohere V4, and Nova expose their own allowed sizes. Expect a full reindex when you change it because stored vector dimensions must stay consistent.", + "Provider-specific output vector size override for memory embeddings. Gemini models support 128-3072 dimensions and recommend 768, 1536, or 3072; Bedrock families such as Titan V2, Cohere V4, and Nova expose their own allowed sizes. Expect a full reindex when you change it because stored vector dimensions must stay consistent.", "memory.search.remote.baseUrl": "Overrides the embedding API endpoint, such as an OpenAI-compatible proxy or custom Gemini base URL. Use this only when routing through your own gateway or vendor endpoint; keep provider defaults otherwise.", "memory.search.remote.apiKey": diff --git a/src/config/types.memory.ts b/src/config/types.memory.ts index 1dfb5de801c8..979853120356 100644 --- a/src/config/types.memory.ts +++ b/src/config/types.memory.ts @@ -61,8 +61,8 @@ export type MemorySearchConfig = { /** Optional provider-specific embedding input_type for document/index embeddings. */ documentInputType?: string; /** - * Gemini embedding-2 models only: output vector dimensions. - * Supported values today are 768, 1536, and 3072. + * Provider-specific output vector dimensions. Gemini supports 128 to 3072. + * Google recommends 768, 1536, or 3072 dimensions. */ outputDimensionality?: number; /** Local embedding settings for the managed llama.cpp server. */