Skip to content

Commit 9860db5

Browse files
authored
fix(memory): allow Gemini multimodal fallback before registry hydration (openclaw#61085)
* fix(memory): allow Gemini multimodal fallback * docs(memory): clarify multimodal fallback
1 parent dca2156 commit 9860db5

3 files changed

Lines changed: 80 additions & 36 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export {
22
isMemoryMultimodalEnabled,
33
normalizeMemoryMultimodalSettings,
4+
supportsMemoryMultimodalEmbeddings,
45
type MemoryMultimodalSettings,
56
} from "./host/multimodal.js";

src/agents/memory-search.test.ts

Lines changed: 62 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,20 @@ import { resolveMemorySearchConfig } from "./memory-search.js";
88

99
const asConfig = (cfg: OpenClawConfig): OpenClawConfig => cfg;
1010

11-
describe("memory search config", () => {
12-
beforeEach(() => {
13-
clearMemoryEmbeddingProviders();
14-
registerMemoryEmbeddingProvider({
15-
id: "openai",
16-
defaultModel: "text-embedding-3-small",
17-
transport: "remote",
18-
create: async () => ({ provider: null }),
19-
});
20-
registerMemoryEmbeddingProvider({
21-
id: "local",
22-
defaultModel: "local-default",
23-
transport: "local",
24-
create: async () => ({ provider: null }),
25-
});
11+
function registerBaseMemoryEmbeddingProviders(options?: { includeGemini?: boolean }): void {
12+
registerMemoryEmbeddingProvider({
13+
id: "openai",
14+
defaultModel: "text-embedding-3-small",
15+
transport: "remote",
16+
create: async () => ({ provider: null }),
17+
});
18+
registerMemoryEmbeddingProvider({
19+
id: "local",
20+
defaultModel: "local-default",
21+
transport: "local",
22+
create: async () => ({ provider: null }),
23+
});
24+
if (options?.includeGemini !== false) {
2625
registerMemoryEmbeddingProvider({
2726
id: "gemini",
2827
defaultModel: "gemini-embedding-001",
@@ -34,24 +33,31 @@ describe("memory search config", () => {
3433
.replace(/^(gemini|google)\//, "") === "gemini-embedding-2-preview",
3534
create: async () => ({ provider: null }),
3635
});
37-
registerMemoryEmbeddingProvider({
38-
id: "voyage",
39-
defaultModel: "voyage-4-large",
40-
transport: "remote",
41-
create: async () => ({ provider: null }),
42-
});
43-
registerMemoryEmbeddingProvider({
44-
id: "mistral",
45-
defaultModel: "mistral-embed",
46-
transport: "remote",
47-
create: async () => ({ provider: null }),
48-
});
49-
registerMemoryEmbeddingProvider({
50-
id: "ollama",
51-
defaultModel: "nomic-embed-text",
52-
transport: "remote",
53-
create: async () => ({ provider: null }),
54-
});
36+
}
37+
registerMemoryEmbeddingProvider({
38+
id: "voyage",
39+
defaultModel: "voyage-4-large",
40+
transport: "remote",
41+
create: async () => ({ provider: null }),
42+
});
43+
registerMemoryEmbeddingProvider({
44+
id: "mistral",
45+
defaultModel: "mistral-embed",
46+
transport: "remote",
47+
create: async () => ({ provider: null }),
48+
});
49+
registerMemoryEmbeddingProvider({
50+
id: "ollama",
51+
defaultModel: "nomic-embed-text",
52+
transport: "remote",
53+
create: async () => ({ provider: null }),
54+
});
55+
}
56+
57+
describe("memory search config", () => {
58+
beforeEach(() => {
59+
clearMemoryEmbeddingProviders();
60+
registerBaseMemoryEmbeddingProviders();
5561
});
5662

5763
afterEach(() => {
@@ -313,6 +319,29 @@ describe("memory search config", () => {
313319
);
314320
});
315321

322+
it("accepts Gemini multimodal memory even when the runtime registry has not registered Gemini yet", () => {
323+
clearMemoryEmbeddingProviders();
324+
registerBaseMemoryEmbeddingProviders({ includeGemini: false });
325+
const cfg = asConfig({
326+
agents: {
327+
defaults: {
328+
memorySearch: {
329+
provider: "gemini",
330+
model: "gemini-embedding-2-preview",
331+
multimodal: { enabled: true, modalities: ["image"] },
332+
},
333+
},
334+
},
335+
});
336+
const resolved = resolveMemorySearchConfig(cfg, "main");
337+
expect(resolved?.provider).toBe("gemini");
338+
expect(resolved?.multimodal).toEqual({
339+
enabled: true,
340+
modalities: ["image"],
341+
maxFileBytes: 10 * 1024 * 1024,
342+
});
343+
});
344+
316345
it("rejects multimodal memory when fallback is configured", () => {
317346
const cfg = asConfig({
318347
agents: {

src/agents/memory-search.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { SecretInput } from "../config/types.secrets.js";
66
import {
77
isMemoryMultimodalEnabled,
88
normalizeMemoryMultimodalSettings,
9+
supportsMemoryMultimodalEmbeddings,
910
type MemoryMultimodalSettings,
1011
} from "../memory-host-sdk/multimodal.js";
1112
import { getMemoryEmbeddingProvider } from "../plugins/memory-embedding-providers.js";
@@ -379,11 +380,24 @@ export function resolveMemorySearchConfig(
379380
const multimodalActive = isMemoryMultimodalEnabled(resolved.multimodal);
380381
const multimodalProvider =
381382
resolved.provider === "auto" ? undefined : getMemoryEmbeddingProvider(resolved.provider);
383+
const builtinMultimodalSupport =
384+
resolved.provider === "auto"
385+
? false
386+
: supportsMemoryMultimodalEmbeddings({
387+
provider: resolved.provider,
388+
model: resolved.model,
389+
});
382390
if (
383391
multimodalActive &&
384-
!multimodalProvider?.supportsMultimodalEmbeddings?.({
385-
model: resolved.model,
386-
})
392+
!(
393+
// Fall back to the built-in helper when the provider is not registered yet
394+
// or when a registered adapter does not implement multimodal capability checks.
395+
(
396+
multimodalProvider?.supportsMultimodalEmbeddings?.({
397+
model: resolved.model,
398+
}) ?? builtinMultimodalSupport
399+
)
400+
)
387401
) {
388402
throw new Error(
389403
"agents.*.memorySearch.multimodal requires a provider adapter that supports multimodal embeddings for the configured model.",

0 commit comments

Comments
 (0)