fix(google): honor supported Gemini embedding dimensions (#129038)

This commit is contained in:
Peter Steinberger
2026-08-24 23:39:35 -07:00
committed by GitHub
parent 28c9f9abd8
commit a556d5379b
5 changed files with 74 additions and 34 deletions
+5 -3
View File
@@ -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 <id>`, then rebuild when ready with
`openclaw memory index --force --agent <id>`.
</Warning>
+56 -4
View File
@@ -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;
+10 -24
View File
@@ -55,7 +55,6 @@ type GeminiTaskType = NonNullable<MemoryEmbeddingProviderCreateOptions["taskType
const GEMINI_EMBEDDING_2_MODELS = new Set(["gemini-embedding-2", "gemini-embedding-2-preview"]);
const GEMINI_EMBEDDING_2_DEFAULT_DIMENSIONS = 3072;
const GEMINI_EMBEDDING_2_VALID_DIMENSIONS = [768, 1536, 3072] as const;
const GEMINI_EMBEDDING_2_TASK_PREFIXES: Record<GeminiTaskType, string> = {
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 (
+1 -1
View File
@@ -225,7 +225,7 @@ export const MODEL_FIELD_HELP: Record<string, string> = {
"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":
+2 -2
View File
@@ -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. */