fix(memory): allow Gemini multimodal fallback before registry hydration (#61085)

* fix(memory): allow Gemini multimodal fallback

* docs(memory): clarify multimodal fallback
This commit is contained in:
scoootscooob
2026-04-04 18:24:20 -07:00
committed by GitHub
parent dca21563c6
commit 9860db5cea
3 changed files with 80 additions and 36 deletions

View File

@@ -1,5 +1,6 @@
export {
isMemoryMultimodalEnabled,
normalizeMemoryMultimodalSettings,
supportsMemoryMultimodalEmbeddings,
type MemoryMultimodalSettings,
} from "./host/multimodal.js";

View File

@@ -8,21 +8,20 @@ import { resolveMemorySearchConfig } from "./memory-search.js";
const asConfig = (cfg: OpenClawConfig): OpenClawConfig => cfg;
describe("memory search config", () => {
beforeEach(() => {
clearMemoryEmbeddingProviders();
registerMemoryEmbeddingProvider({
id: "openai",
defaultModel: "text-embedding-3-small",
transport: "remote",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "local",
defaultModel: "local-default",
transport: "local",
create: async () => ({ provider: null }),
});
function registerBaseMemoryEmbeddingProviders(options?: { includeGemini?: boolean }): void {
registerMemoryEmbeddingProvider({
id: "openai",
defaultModel: "text-embedding-3-small",
transport: "remote",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "local",
defaultModel: "local-default",
transport: "local",
create: async () => ({ provider: null }),
});
if (options?.includeGemini !== false) {
registerMemoryEmbeddingProvider({
id: "gemini",
defaultModel: "gemini-embedding-001",
@@ -34,24 +33,31 @@ describe("memory search config", () => {
.replace(/^(gemini|google)\//, "") === "gemini-embedding-2-preview",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "voyage",
defaultModel: "voyage-4-large",
transport: "remote",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "mistral",
defaultModel: "mistral-embed",
transport: "remote",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "ollama",
defaultModel: "nomic-embed-text",
transport: "remote",
create: async () => ({ provider: null }),
});
}
registerMemoryEmbeddingProvider({
id: "voyage",
defaultModel: "voyage-4-large",
transport: "remote",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "mistral",
defaultModel: "mistral-embed",
transport: "remote",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "ollama",
defaultModel: "nomic-embed-text",
transport: "remote",
create: async () => ({ provider: null }),
});
}
describe("memory search config", () => {
beforeEach(() => {
clearMemoryEmbeddingProviders();
registerBaseMemoryEmbeddingProviders();
});
afterEach(() => {
@@ -313,6 +319,29 @@ describe("memory search config", () => {
);
});
it("accepts Gemini multimodal memory even when the runtime registry has not registered Gemini yet", () => {
clearMemoryEmbeddingProviders();
registerBaseMemoryEmbeddingProviders({ includeGemini: false });
const cfg = asConfig({
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-2-preview",
multimodal: { enabled: true, modalities: ["image"] },
},
},
},
});
const resolved = resolveMemorySearchConfig(cfg, "main");
expect(resolved?.provider).toBe("gemini");
expect(resolved?.multimodal).toEqual({
enabled: true,
modalities: ["image"],
maxFileBytes: 10 * 1024 * 1024,
});
});
it("rejects multimodal memory when fallback is configured", () => {
const cfg = asConfig({
agents: {

View File

@@ -6,6 +6,7 @@ import type { SecretInput } from "../config/types.secrets.js";
import {
isMemoryMultimodalEnabled,
normalizeMemoryMultimodalSettings,
supportsMemoryMultimodalEmbeddings,
type MemoryMultimodalSettings,
} from "../memory-host-sdk/multimodal.js";
import { getMemoryEmbeddingProvider } from "../plugins/memory-embedding-providers.js";
@@ -379,11 +380,24 @@ export function resolveMemorySearchConfig(
const multimodalActive = isMemoryMultimodalEnabled(resolved.multimodal);
const multimodalProvider =
resolved.provider === "auto" ? undefined : getMemoryEmbeddingProvider(resolved.provider);
const builtinMultimodalSupport =
resolved.provider === "auto"
? false
: supportsMemoryMultimodalEmbeddings({
provider: resolved.provider,
model: resolved.model,
});
if (
multimodalActive &&
!multimodalProvider?.supportsMultimodalEmbeddings?.({
model: resolved.model,
})
!(
// Fall back to the built-in helper when the provider is not registered yet
// or when a registered adapter does not implement multimodal capability checks.
(
multimodalProvider?.supportsMultimodalEmbeddings?.({
model: resolved.model,
}) ?? builtinMultimodalSupport
)
)
) {
throw new Error(
"agents.*.memorySearch.multimodal requires a provider adapter that supports multimodal embeddings for the configured model.",