fix(doctor): align memory provider guidance (#130756)

This commit is contained in:
Peter Steinberger
2026-08-26 23:54:41 -07:00
committed by GitHub
parent a732513745
commit f8ad31333b
3 changed files with 30 additions and 152 deletions
+1 -1
View File
@@ -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;
+9 -50
View File
@@ -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",
+20 -101
View File
@@ -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<boolean> {
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(