feat(memory): native Voyage AI support (#7078)

* feat(memory): add native Voyage AI embedding support with batching

Cherry-picked from PR #2519, resolved conflict in memory-search.ts
(hasRemote -> hasRemoteConfig rename + added voyage provider)

* fix(memory): optimize voyage batch memory usage with streaming and deduplicate code

Cherry-picked from PR #2519. Fixed lint error: changed this.runWithConcurrency
to use imported runWithConcurrency function after extraction to internal.ts
This commit is contained in:
Jake
2026-02-07 10:09:32 +13:00
committed by GitHub
parent e3d3893d5d
commit 6965a2cc9d
11 changed files with 879 additions and 58 deletions

View File

@@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../config/config.js";
import { resolveUserPath } from "../utils.js";
import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js";
import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js";
import { createVoyageEmbeddingProvider, type VoyageEmbeddingClient } from "./embeddings-voyage.js";
import { importNodeLlamaCpp } from "./node-llama.js";
function sanitizeAndNormalizeEmbedding(vec: number[]): number[] {
@@ -17,6 +18,7 @@ function sanitizeAndNormalizeEmbedding(vec: number[]): number[] {
export type { GeminiEmbeddingClient } from "./embeddings-gemini.js";
export type { OpenAiEmbeddingClient } from "./embeddings-openai.js";
export type { VoyageEmbeddingClient } from "./embeddings-voyage.js";
export type EmbeddingProvider = {
id: string;
@@ -27,24 +29,25 @@ export type EmbeddingProvider = {
export type EmbeddingProviderResult = {
provider: EmbeddingProvider;
requestedProvider: "openai" | "local" | "gemini" | "auto";
fallbackFrom?: "openai" | "local" | "gemini";
requestedProvider: "openai" | "local" | "gemini" | "voyage" | "auto";
fallbackFrom?: "openai" | "local" | "gemini" | "voyage";
fallbackReason?: string;
openAi?: OpenAiEmbeddingClient;
gemini?: GeminiEmbeddingClient;
voyage?: VoyageEmbeddingClient;
};
export type EmbeddingProviderOptions = {
config: OpenClawConfig;
agentDir?: string;
provider: "openai" | "local" | "gemini" | "auto";
provider: "openai" | "local" | "gemini" | "voyage" | "auto";
remote?: {
baseUrl?: string;
apiKey?: string;
headers?: Record<string, string>;
};
model: string;
fallback: "openai" | "gemini" | "local" | "none";
fallback: "openai" | "gemini" | "local" | "voyage" | "none";
local?: {
modelPath?: string;
modelCacheDir?: string;
@@ -128,7 +131,7 @@ export async function createEmbeddingProvider(
const requestedProvider = options.provider;
const fallback = options.fallback;
const createProvider = async (id: "openai" | "local" | "gemini") => {
const createProvider = async (id: "openai" | "local" | "gemini" | "voyage") => {
if (id === "local") {
const provider = await createLocalEmbeddingProvider(options);
return { provider };
@@ -137,11 +140,15 @@ export async function createEmbeddingProvider(
const { provider, client } = await createGeminiEmbeddingProvider(options);
return { provider, gemini: client };
}
if (id === "voyage") {
const { provider, client } = await createVoyageEmbeddingProvider(options);
return { provider, voyage: client };
}
const { provider, client } = await createOpenAiEmbeddingProvider(options);
return { provider, openAi: client };
};
const formatPrimaryError = (err: unknown, provider: "openai" | "local" | "gemini") =>
const formatPrimaryError = (err: unknown, provider: "openai" | "local" | "gemini" | "voyage") =>
provider === "local" ? formatLocalSetupError(err) : formatError(err);
if (requestedProvider === "auto") {
@@ -157,7 +164,7 @@ export async function createEmbeddingProvider(
}
}
for (const provider of ["openai", "gemini"] as const) {
for (const provider of ["openai", "gemini", "voyage"] as const) {
try {
const result = await createProvider(provider);
return { ...result, requestedProvider };
@@ -240,6 +247,7 @@ function formatLocalSetupError(err: unknown): string {
: null,
"3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp",
'Or set agents.defaults.memorySearch.provider = "openai" (remote).',
'Or set agents.defaults.memorySearch.provider = "voyage" (remote).',
]
.filter(Boolean)
.join("\n");