mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(doctor): align memory provider guidance (#130756)
This commit is contained in:
committed by
GitHub
parent
a732513745
commit
f8ad31333b
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user