diff --git a/src/agents/memory-search.ts b/src/agents/memory-search.ts index 34d594903e2a..c71ee4b23fb6 100644 --- a/src/agents/memory-search.ts +++ b/src/agents/memory-search.ts @@ -130,7 +130,7 @@ const DEFAULT_TEMPORAL_DECAY_HALF_LIFE_DAYS = 30; const DEFAULT_CACHE_ENABLED = true; const DEFAULT_CACHE_MAX_ENTRIES = undefined; const DEFAULT_SOURCES: Array<"memory" | "sessions"> = ["memory"]; -const DEFAULT_MEMORY_EMBEDDING_PROVIDER = "openai"; +export const DEFAULT_MEMORY_EMBEDDING_PROVIDER = "openai"; const DEFAULT_REMOTE_BATCH_POLL_INTERVAL_MS = 2_000; const DEFAULT_REMOTE_BATCH_TIMEOUT_MINUTES = 60; diff --git a/src/commands/doctor-memory-search.test.ts b/src/commands/doctor-memory-search.test.ts index ef7f122f5be0..2283339293cb 100644 --- a/src/commands/doctor-memory-search.test.ts +++ b/src/commands/doctor-memory-search.test.ts @@ -42,6 +42,7 @@ vi.mock("../agents/agent-scope.js", () => ({ })); vi.mock("../agents/memory-search.js", () => ({ + DEFAULT_MEMORY_EMBEDDING_PROVIDER: "openai", resolveMemorySearchConfig, })); @@ -67,27 +68,6 @@ vi.mock("../plugin-sdk/memory-core-bundled-runtime.js", () => ({ auditShortTermPromotionArtifacts, repairDreamingArtifacts, repairShortTermPromotionArtifacts, - getBuiltinMemoryEmbeddingProviderDoctorMetadata: vi.fn((provider: string) => { - if (provider === "gemini") { - return { authProviderId: "google", envVars: ["GEMINI_API_KEY"] }; - } - if (provider === "mistral") { - return { authProviderId: "mistral", envVars: ["MISTRAL_API_KEY"] }; - } - if (provider === "openai") { - return { authProviderId: "openai", envVars: ["OPENAI_API_KEY"] }; - } - return null; - }), - listBuiltinAutoSelectMemoryEmbeddingProviderDoctorMetadata: vi.fn(() => [ - { - providerId: "openai", - authProviderId: "openai", - envVars: ["OPENAI_API_KEY"], - transport: "remote", - }, - { providerId: "local", authProviderId: "local", envVars: [], transport: "local" }, - ]), })); vi.mock("./doctor-workspace.js", async (importOriginal) => { @@ -324,7 +304,9 @@ describe("noteMemorySearchHealth", () => { expectFirstNoteContains( 'Memory search provider is set to "local"', "openclaw plugins install @openclaw/llama-cpp-provider", + "openclaw config set memory.search.provider openai", ); + expectFirstNoteExcludes("github-copilot"); }); it("supports silent structured collection through an injected note sink", async () => { @@ -687,7 +669,7 @@ describe("noteMemorySearchHealth", () => { resolveMemorySearchConfig.mockImplementation((_cfg: OpenClawConfig, agentId: string) => agentId === "personal" ? undefined - : { provider: "auto", local: {}, remote: {}, sources: ["memory"] }, + : { provider: "openai", local: {}, remote: {}, sources: ["memory"] }, ); await noteMemorySearchHealth(memoryCfg); @@ -708,12 +690,6 @@ describe("noteMemorySearchHealth", () => { "openai", { source: "env", provider: "default", id: "OPENAI_API_KEY" }, ], - ["does not warn in auto mode when remote apiKey is configured", "auto", "from-config"], - [ - "treats SecretRef remote apiKey as configured in auto mode", - "auto", - { source: "env", provider: "default", id: "OPENAI_API_KEY" }, - ], ])("%s", async (_name, provider, apiKey) => { await runMemorySearchHealth(provider, {}, { remote: { apiKey } }); expect(note).not.toHaveBeenCalled(); @@ -1005,22 +981,11 @@ describe("noteMemorySearchHealth", () => { expectFirstNoteExcludes("openclaw auth add --provider"); }); - it("warns for legacy auto mode as OpenAI when no API key is configured", async () => { - await runMemorySearchHealth("auto"); - - expect(note).toHaveBeenCalledTimes(1); - expectFirstNoteContains( - 'provider is set to "openai"', - "OPENAI_API_KEY", - "openclaw configure --section model", - ); - }); - - it("does not probe unrelated embedding providers for legacy auto mode", async () => { + it("does not probe unrelated embedding providers for the resolved default", async () => { resolveApiKeyForProviderCore.mockImplementation(async () => { throw new Error("missing key"); }); - await runMemorySearchHealth("auto"); + await runMemorySearchHealth("openai"); expect(note).toHaveBeenCalledTimes(1); const providerCalls = resolveApiKeyForProviderCore.mock.calls as Array<[{ provider: string }]>; @@ -1028,9 +993,9 @@ describe("noteMemorySearchHealth", () => { expect(providersChecked).toEqual(["openai"]); }); - it("skips auth-profile probing for legacy auto mode when no auth store exists", async () => { + it("skips auth-profile probing for the resolved default when no auth store exists", async () => { hasAnyAuthProfileStoreSource.mockReturnValue(false); - await runMemorySearchHealth("auto"); + await runMemorySearchHealth("openai"); const providerCalls = resolveApiKeyForProviderCore.mock.calls as Array<[{ provider: string }]>; const providersChecked = providerCalls.map(([arg]) => arg.provider); @@ -1043,15 +1008,9 @@ describe("noteMemorySearchHealth", () => { expectFirstNoteContains("GEMINI_API_KEY", 'provider is set to "gemini"'); }); - it("uses OpenAI env var hints for legacy auto mode", async () => { - await runMemorySearchHealth("auto"); - - expectFirstNoteContains('provider is set to "openai"', "OPENAI_API_KEY"); - }); - it("does not warn when only lowercase memory.md exists", async () => { resolveAgentWorkspaceDir.mockReturnValue("/tmp/agent-default/workspace"); - await runMemorySearchHealth("auto"); + await runMemorySearchHealth("openai"); expect(noteWorkspaceMemoryHealth).toHaveBeenCalledWith(cfg, { agentId: "agent-default", diff --git a/src/commands/doctor-memory-search.ts b/src/commands/doctor-memory-search.ts index 470e2fec85c1..191e2eb0fded 100644 --- a/src/commands/doctor-memory-search.ts +++ b/src/commands/doctor-memory-search.ts @@ -16,7 +16,10 @@ import { hasAuthProfileStoreSourceForProvider, isConfiguredAwsSdkAuthProfileForProvider, } from "../agents/auth-profiles.js"; -import { resolveMemorySearchConfig } from "../agents/memory-search.js"; +import { + DEFAULT_MEMORY_EMBEDDING_PROVIDER, + resolveMemorySearchConfig, +} from "../agents/memory-search.js"; import { resolveApiKeyForProviderCore, resolveEnvApiKey, @@ -74,13 +77,6 @@ function formatAgentMessage(agentId: string, labelAgent: boolean, message: strin return `${labelAgent ? `Agent "${agentId}": ` : ""}${message}`; } -type MemoryEmbeddingProviderDoctorMetadata = { - providerId: string; - authProviderId: string; - transport: "local" | "remote"; - autoSelectPriority?: number; -}; - function formatLocalRuntimeDoctorNote(facts: DoctorMemoryEmbeddingRuntimePayload): string { const backend = facts.backend ?? "unknown"; const build = facts.buildInfo ? `, ${facts.buildInfo}` : ""; @@ -104,86 +100,17 @@ function formatLocalRuntimeDoctorNote(facts: DoctorMemoryEmbeddingRuntimePayload return `llama.cpp server: ${backend}${build}${state}${model}${capabilities}${endpoints}${loadError}`; } -const BUNDLED_MEMORY_EMBEDDING_PROVIDER_DOCTOR_METADATA: MemoryEmbeddingProviderDoctorMetadata[] = [ - { - providerId: "github-copilot", - authProviderId: "github-copilot", - transport: "remote", - autoSelectPriority: 15, - }, - { - providerId: "openai", - authProviderId: "openai", - transport: "remote", - autoSelectPriority: 20, - }, - { - providerId: "gemini", - authProviderId: "google", - transport: "remote", - autoSelectPriority: 30, - }, - { - providerId: "voyage", - authProviderId: "voyage", - transport: "remote", - autoSelectPriority: 40, - }, - { - providerId: "mistral", - authProviderId: "mistral", - transport: "remote", - autoSelectPriority: 50, - }, - { - providerId: "bedrock", - authProviderId: "amazon-bedrock", - transport: "remote", - autoSelectPriority: 60, - }, -]; -const DEFAULT_MEMORY_EMBEDDING_PROVIDER = "openai"; +const MEMORY_EMBEDDING_PROVIDER_AUTH_IDS = new Map([ + ["github-copilot", "github-copilot"], + ["openai", "openai"], + ["gemini", "google"], + ["voyage", "voyage"], + ["mistral", "mistral"], + ["bedrock", "amazon-bedrock"], +]); const OPENAI_COMPATIBLE_MEMORY_EMBEDDING_PROVIDER = "openai-compatible"; const OPENAI_COMPATIBLE_MODEL_APIS = new Set(["openai-completions", "openai-responses"]); -function resolveMemoryEmbeddingProviderDoctorMetadata( - providerId: string, -): (MemoryEmbeddingProviderDoctorMetadata & { envVars: string[] }) | null { - const metadata = - BUNDLED_MEMORY_EMBEDDING_PROVIDER_DOCTOR_METADATA.find( - (candidate) => candidate.providerId === providerId, - ) ?? null; - if (!metadata) { - return null; - } - return { - ...metadata, - envVars: getProviderEnvVars(metadata.authProviderId), - }; -} - -function listAutoSelectMemoryEmbeddingProviderDoctorMetadata(): Array< - MemoryEmbeddingProviderDoctorMetadata & { envVars: string[] } -> { - return BUNDLED_MEMORY_EMBEDDING_PROVIDER_DOCTOR_METADATA.filter( - (provider) => typeof provider.autoSelectPriority === "number", - ) - .toSorted((a, b) => (a.autoSelectPriority ?? 0) - (b.autoSelectPriority ?? 0)) - .map((provider) => ({ - providerId: provider.providerId, - authProviderId: provider.authProviderId, - transport: provider.transport, - autoSelectPriority: provider.autoSelectPriority, - envVars: getProviderEnvVars(provider.authProviderId), - })); -} - -function resolveSuggestedRemoteMemoryProvider(): string | undefined { - return listAutoSelectMemoryEmbeddingProviderDoctorMetadata().find( - (provider) => provider.transport === "remote", - )?.providerId; -} - function hasConfiguredAwsSdkAuthForProvider(provider: string, cfg: OpenClawConfig): boolean { const providerConfig = findNormalizedProviderValue(cfg.models?.providers, provider); if (providerConfig?.auth === "aws-sdk") { @@ -202,11 +129,7 @@ function isOpenAICompatibleMemoryProvider(providerId: string, cfg: OpenClawConfi if (normalizedProviderId === OPENAI_COMPATIBLE_MEMORY_EMBEDDING_PROVIDER) { return true; } - if ( - BUNDLED_MEMORY_EMBEDDING_PROVIDER_DOCTOR_METADATA.some( - (provider) => provider.providerId === normalizedProviderId, - ) - ) { + if (MEMORY_EMBEDDING_PROVIDER_AUTH_IDS.has(normalizedProviderId)) { return false; } const providerConfig = findNormalizedProviderValue(cfg.models?.providers, providerId); @@ -614,8 +537,7 @@ async function noteMemorySearchHealthForAgent( ); return; } - const provider = - resolved.provider === "auto" ? DEFAULT_MEMORY_EMBEDDING_PROVIDER : resolved.provider; + const provider = resolved.provider; const backendConfig = resolveActiveMemoryBackendConfig({ cfg, agentId }); if (!backendConfig) { @@ -633,7 +555,6 @@ async function noteMemorySearchHealthForAgent( } if (provider === "local") { - const suggestedRemoteProvider = resolveSuggestedRemoteMemoryProvider(); const runtimeFacts = opts?.gatewayMemoryProbe?.runtimeFacts; if (opts?.gatewayMemoryProbe?.checked && opts.gatewayMemoryProbe.ready) { if (runtimeFacts) { @@ -661,9 +582,7 @@ async function noteMemorySearchHealthForAgent( "Fix (pick one):", `- Install the llama.cpp provider plugin: ${formatCliCommand("openclaw plugins install @openclaw/llama-cpp-provider")}`, `- Set a local GGUF model path in config`, - suggestedRemoteProvider - ? `- Switch to a remote provider: ${formatCliCommand(`openclaw config set memory.search.provider ${suggestedRemoteProvider}`)}` - : `- Switch to a remote embedding provider in config`, + `- Switch to a remote provider: ${formatCliCommand(`openclaw config set memory.search.provider ${DEFAULT_MEMORY_EMBEDDING_PROVIDER}`)}`, "", `Verify: ${formatCliCommand("openclaw memory status --deep")}`, ] @@ -739,7 +658,7 @@ async function noteMemorySearchHealthForAgent( return; } - // Remote provider — check for API key. Legacy provider: "auto" resolves to OpenAI. + // Remote provider — check for API key. if ( hasRemoteApiKey || (await hasApiKeyForProvider(provider, cfg, agentDir, { @@ -809,8 +728,7 @@ async function hasApiKeyForProvider( agentDir: string, opts?: { skipProfileResolution?: boolean }, ): Promise { - const metadata = resolveMemoryEmbeddingProviderDoctorMetadata(provider); - const authProviderId = metadata?.authProviderId ?? provider; + const authProviderId = MEMORY_EMBEDDING_PROVIDER_AUTH_IDS.get(provider) ?? provider; if ( resolveEnvApiKey(authProviderId) || resolveUsableCustomProviderApiKey({ cfg, provider: authProviderId }) @@ -847,8 +765,9 @@ function resolvePrimaryMemoryProviderEnvVar(provider: string): string { if (provider === "openai") { return "OPENAI_API_KEY"; } - const metadata = resolveMemoryEmbeddingProviderDoctorMetadata(provider); - return metadata?.envVars[0] ?? `${provider.toUpperCase()}_API_KEY`; + const authProviderId = MEMORY_EMBEDDING_PROVIDER_AUTH_IDS.get(provider); + const envVar = authProviderId ? getProviderEnvVars(authProviderId)[0] : undefined; + return envVar ?? `${provider.toUpperCase()}_API_KEY`; } function buildGatewayProbeWarning(