From dfa9569ca7afdadf15c8e0bb6a4686b949f20149 Mon Sep 17 00:00:00 2001 From: wuqxuan Date: Sun, 12 Jul 2026 08:06:21 +0800 Subject: [PATCH] fix(google): exclude versioned Gemini client header from memory identity (#104759) * fix(google): exclude versioned Gemini client header from memory identity * test(google): focus memory identity coverage --------- Co-authored-by: Peter Steinberger --- .../google/memory-embedding-adapter.test.ts | 98 +++++++++++++++++++ extensions/google/memory-embedding-adapter.ts | 4 + 2 files changed, 102 insertions(+) create mode 100644 extensions/google/memory-embedding-adapter.test.ts diff --git a/extensions/google/memory-embedding-adapter.test.ts b/extensions/google/memory-embedding-adapter.test.ts new file mode 100644 index 000000000000..aecfa219558c --- /dev/null +++ b/extensions/google/memory-embedding-adapter.test.ts @@ -0,0 +1,98 @@ +// Google tests cover memory embedding adapter plugin behavior. +import { + sanitizeEmbeddingCacheHeaders, + type MemoryEmbeddingProvider, +} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + createGeminiEmbeddingProvider: vi.fn(), + runGeminiEmbeddingBatches: vi.fn(async () => new Map([["0", [1, 0]]])), +})); + +vi.mock("./embedding-provider.js", () => ({ + DEFAULT_GEMINI_EMBEDDING_MODEL: "gemini-embedding-001", + createGeminiEmbeddingProvider: mocks.createGeminiEmbeddingProvider, + buildGeminiEmbeddingRequest: vi.fn(), +})); + +vi.mock("./embedding-batch.js", () => ({ + runGeminiEmbeddingBatches: mocks.runGeminiEmbeddingBatches, +})); + +import { geminiMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js"; + +const provider: MemoryEmbeddingProvider = { + id: "gemini", + model: "gemini-embedding-2-preview", + embedQuery: async () => [1, 0], + embedBatch: async (texts) => texts.map(() => [1, 0]), +}; + +const clientBase = { + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + model: "gemini-embedding-2-preview", + modelPath: "models/gemini-embedding-2-preview", + outputDimensionality: 768, +}; + +async function createAdapterWithHeaders(headers: Record) { + mocks.createGeminiEmbeddingProvider.mockResolvedValueOnce({ + provider, + client: { ...clientBase, headers }, + }); + return await geminiMemoryEmbeddingProviderAdapter.create({ + config: {} as never, + provider: "gemini", + model: "gemini-embedding-2-preview", + fallback: "none", + }); +} + +describe("Gemini memory embedding adapter", () => { + beforeEach(() => { + mocks.createGeminiEmbeddingProvider.mockReset(); + mocks.runGeminiEmbeddingBatches.mockClear(); + }); + + it("keeps durable identity stable across generated client-version changes", async () => { + const sharedHeaders = { + "Content-Type": "application/json", + "x-goog-api-key": "secret-key", + Authorization: "Bearer token", + "X-Custom-Region": "us-central1", + }; + const older = await createAdapterWithHeaders({ + ...sharedHeaders, + "x-goog-api-client": "openclaw/2026.6.11", + }); + const newer = await createAdapterWithHeaders({ + ...sharedHeaders, + "x-goog-api-client": "openclaw/2026.7.1-beta.5", + }); + + expect(older.runtime?.cacheKeyData).toEqual(newer.runtime?.cacheKeyData); + expect(older.runtime?.cacheKeyData?.headers).toEqual( + sanitizeEmbeddingCacheHeaders( + { + "Content-Type": "application/json", + "X-Custom-Region": "us-central1", + }, + [], + ), + ); + }); + + it("still invalidates identity when a semantic custom header changes", async () => { + const first = await createAdapterWithHeaders({ + "x-goog-api-client": "openclaw/2026.7.1-beta.5", + "x-custom-endpoint": "https://example.invalid/a", + }); + const second = await createAdapterWithHeaders({ + "x-goog-api-client": "openclaw/2026.7.1-beta.5", + "x-custom-endpoint": "https://example.invalid/b", + }); + + expect(first.runtime?.cacheKeyData).not.toEqual(second.runtime?.cacheKeyData); + }); +}); diff --git a/extensions/google/memory-embedding-adapter.ts b/extensions/google/memory-embedding-adapter.ts index b44340d58c72..94cae3ed3f00 100644 --- a/extensions/google/memory-embedding-adapter.ts +++ b/extensions/google/memory-embedding-adapter.ts @@ -45,9 +45,13 @@ export const geminiMemoryEmbeddingProviderAdapter: MemoryEmbeddingProviderAdapte baseUrl: client.baseUrl, model: client.model, outputDimensionality: client.outputDimensionality, + // x-goog-api-client is generated partner attribution (openclaw/). + // Keep it on outbound requests, but exclude it from durable memory identity so + // OpenClaw version bumps do not pause otherwise-compatible Gemini indexes. headers: sanitizeEmbeddingCacheHeaders(client.headers, [ "authorization", "x-goog-api-key", + "x-goog-api-client", ]), }, batchEmbed: async (batch) => {