Merge ba76125985 into 4583f88626
This commit is contained in:
commit
cf2350293b
@ -10,7 +10,7 @@ export type ResolvedMemorySearchConfig = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
sources: Array<"memory" | "sessions">;
|
sources: Array<"memory" | "sessions">;
|
||||||
extraPaths: string[];
|
extraPaths: string[];
|
||||||
provider: "openai" | "local" | "gemini" | "auto";
|
provider: "openai" | "local" | "gemini" | "openrouter" | "auto";
|
||||||
remote?: {
|
remote?: {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
@ -26,7 +26,7 @@ export type ResolvedMemorySearchConfig = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
sessionMemory: boolean;
|
sessionMemory: boolean;
|
||||||
};
|
};
|
||||||
fallback: "openai" | "gemini" | "local" | "none";
|
fallback: "openai" | "gemini" | "openrouter" | "local" | "none";
|
||||||
model: string;
|
model: string;
|
||||||
local: {
|
local: {
|
||||||
modelPath?: string;
|
modelPath?: string;
|
||||||
@ -71,8 +71,10 @@ export type ResolvedMemorySearchConfig = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const DEFAULT_OPENAI_MODEL = "text-embedding-3-small";
|
const DEFAULT_OPENAI_MODEL = "text-embedding-3-small";
|
||||||
const DEFAULT_GEMINI_MODEL = "gemini-embedding-001";
|
const DEFAULT_GEMINI_MODEL = "gemini-embedding-001";
|
||||||
|
const DEFAULT_OPENROUTER_MODEL = "openai/text-embedding-3-small";
|
||||||
const DEFAULT_CHUNK_TOKENS = 400;
|
const DEFAULT_CHUNK_TOKENS = 400;
|
||||||
const DEFAULT_CHUNK_OVERLAP = 80;
|
const DEFAULT_CHUNK_OVERLAP = 80;
|
||||||
const DEFAULT_WATCH_DEBOUNCE_MS = 1500;
|
const DEFAULT_WATCH_DEBOUNCE_MS = 1500;
|
||||||
@ -129,7 +131,11 @@ function mergeConfig(
|
|||||||
defaultRemote?.headers,
|
defaultRemote?.headers,
|
||||||
);
|
);
|
||||||
const includeRemote =
|
const includeRemote =
|
||||||
hasRemoteConfig || provider === "openai" || provider === "gemini" || provider === "auto";
|
hasRemoteConfig ||
|
||||||
|
provider === "openai" ||
|
||||||
|
provider === "gemini" ||
|
||||||
|
provider === "openrouter" ||
|
||||||
|
provider === "auto";
|
||||||
const batch = {
|
const batch = {
|
||||||
enabled: overrideRemote?.batch?.enabled ?? defaultRemote?.batch?.enabled ?? true,
|
enabled: overrideRemote?.batch?.enabled ?? defaultRemote?.batch?.enabled ?? true,
|
||||||
wait: overrideRemote?.batch?.wait ?? defaultRemote?.batch?.wait ?? true,
|
wait: overrideRemote?.batch?.wait ?? defaultRemote?.batch?.wait ?? true,
|
||||||
@ -156,7 +162,9 @@ function mergeConfig(
|
|||||||
? DEFAULT_GEMINI_MODEL
|
? DEFAULT_GEMINI_MODEL
|
||||||
: provider === "openai"
|
: provider === "openai"
|
||||||
? DEFAULT_OPENAI_MODEL
|
? DEFAULT_OPENAI_MODEL
|
||||||
: undefined;
|
: provider === "openrouter"
|
||||||
|
? DEFAULT_OPENROUTER_MODEL
|
||||||
|
: undefined;
|
||||||
const model = overrides?.model ?? defaults?.model ?? modelDefault ?? "";
|
const model = overrides?.model ?? defaults?.model ?? modelDefault ?? "";
|
||||||
const local = {
|
const local = {
|
||||||
modelPath: overrides?.local?.modelPath ?? defaults?.local?.modelPath,
|
modelPath: overrides?.local?.modelPath ?? defaults?.local?.modelPath,
|
||||||
|
|||||||
@ -504,7 +504,8 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
"Extra paths to include in memory search (directories or .md files; relative paths resolved from workspace).",
|
"Extra paths to include in memory search (directories or .md files; relative paths resolved from workspace).",
|
||||||
"agents.defaults.memorySearch.experimental.sessionMemory":
|
"agents.defaults.memorySearch.experimental.sessionMemory":
|
||||||
"Enable experimental session transcript indexing for memory search (default: false).",
|
"Enable experimental session transcript indexing for memory search (default: false).",
|
||||||
"agents.defaults.memorySearch.provider": 'Embedding provider ("openai", "gemini", or "local").',
|
"agents.defaults.memorySearch.provider":
|
||||||
|
'Embedding provider ("openai", "gemini", "openrouter", or "local").',
|
||||||
"agents.defaults.memorySearch.remote.baseUrl":
|
"agents.defaults.memorySearch.remote.baseUrl":
|
||||||
"Custom base URL for remote embeddings (OpenAI-compatible proxies or Gemini overrides).",
|
"Custom base URL for remote embeddings (OpenAI-compatible proxies or Gemini overrides).",
|
||||||
"agents.defaults.memorySearch.remote.apiKey": "Custom API key for the remote embedding provider.",
|
"agents.defaults.memorySearch.remote.apiKey": "Custom API key for the remote embedding provider.",
|
||||||
@ -523,7 +524,7 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
"agents.defaults.memorySearch.local.modelPath":
|
"agents.defaults.memorySearch.local.modelPath":
|
||||||
"Local GGUF model path or hf: URI (node-llama-cpp).",
|
"Local GGUF model path or hf: URI (node-llama-cpp).",
|
||||||
"agents.defaults.memorySearch.fallback":
|
"agents.defaults.memorySearch.fallback":
|
||||||
'Fallback provider when embeddings fail ("openai", "gemini", "local", or "none").',
|
'Fallback provider when embeddings fail ("openai", "gemini", "openrouter", "local", or "none").',
|
||||||
"agents.defaults.memorySearch.store.path":
|
"agents.defaults.memorySearch.store.path":
|
||||||
"SQLite index path (default: ~/.clawdbot/memory/{agentId}.sqlite).",
|
"SQLite index path (default: ~/.clawdbot/memory/{agentId}.sqlite).",
|
||||||
"agents.defaults.memorySearch.store.vector.enabled":
|
"agents.defaults.memorySearch.store.vector.enabled":
|
||||||
|
|||||||
@ -234,7 +234,7 @@ export type MemorySearchConfig = {
|
|||||||
sessionMemory?: boolean;
|
sessionMemory?: boolean;
|
||||||
};
|
};
|
||||||
/** Embedding provider mode. */
|
/** Embedding provider mode. */
|
||||||
provider?: "openai" | "gemini" | "local";
|
provider?: "openai" | "gemini" | "openrouter" | "local";
|
||||||
remote?: {
|
remote?: {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
@ -253,7 +253,7 @@ export type MemorySearchConfig = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** Fallback behavior when embeddings fail. */
|
/** Fallback behavior when embeddings fail. */
|
||||||
fallback?: "openai" | "gemini" | "local" | "none";
|
fallback?: "openai" | "gemini" | "openrouter" | "local" | "none";
|
||||||
/** Embedding model id (remote) or alias (local). */
|
/** Embedding model id (remote) or alias (local). */
|
||||||
model?: string;
|
model?: string;
|
||||||
/** Local embedding settings (node-llama-cpp). */
|
/** Local embedding settings (node-llama-cpp). */
|
||||||
|
|||||||
86
src/memory/embeddings-openrouter.ts
Normal file
86
src/memory/embeddings-openrouter.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
|
||||||
|
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
|
||||||
|
|
||||||
|
export type OpenRouterEmbeddingClient = {
|
||||||
|
baseUrl: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_OPENROUTER_EMBEDDING_MODEL = "openai/text-embedding-3-small";
|
||||||
|
const DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
||||||
|
|
||||||
|
export function normalizeOpenRouterModel(model: string): string {
|
||||||
|
const trimmed = model.trim();
|
||||||
|
if (!trimmed) return DEFAULT_OPENROUTER_EMBEDDING_MODEL;
|
||||||
|
if (trimmed.startsWith("openrouter/")) return trimmed.slice("openrouter/".length);
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createOpenRouterEmbeddingProvider(
|
||||||
|
options: EmbeddingProviderOptions,
|
||||||
|
): Promise<{ provider: EmbeddingProvider; client: OpenRouterEmbeddingClient }> {
|
||||||
|
const client = await resolveOpenRouterEmbeddingClient(options);
|
||||||
|
const url = `${client.baseUrl.replace(/\/$/, "")}/embeddings`;
|
||||||
|
|
||||||
|
const embed = async (input: string[]): Promise<number[][]> => {
|
||||||
|
if (input.length === 0) return [];
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: client.headers,
|
||||||
|
body: JSON.stringify({ model: client.model, input }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`openrouter embeddings failed: ${res.status} ${text}`);
|
||||||
|
}
|
||||||
|
const payload = (await res.json()) as {
|
||||||
|
data?: Array<{ embedding?: number[] }>;
|
||||||
|
};
|
||||||
|
const data = payload.data ?? [];
|
||||||
|
return data.map((entry) => entry.embedding ?? []);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: {
|
||||||
|
id: "openrouter",
|
||||||
|
model: client.model,
|
||||||
|
embedQuery: async (text) => {
|
||||||
|
const [vec] = await embed([text]);
|
||||||
|
return vec ?? [];
|
||||||
|
},
|
||||||
|
embedBatch: embed,
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveOpenRouterEmbeddingClient(
|
||||||
|
options: EmbeddingProviderOptions,
|
||||||
|
): Promise<OpenRouterEmbeddingClient> {
|
||||||
|
const remote = options.remote;
|
||||||
|
const remoteApiKey = remote?.apiKey?.trim();
|
||||||
|
const remoteBaseUrl = remote?.baseUrl?.trim();
|
||||||
|
|
||||||
|
const apiKey = remoteApiKey
|
||||||
|
? remoteApiKey
|
||||||
|
: requireApiKey(
|
||||||
|
await resolveApiKeyForProvider({
|
||||||
|
provider: "openrouter",
|
||||||
|
cfg: options.config,
|
||||||
|
agentDir: options.agentDir,
|
||||||
|
}),
|
||||||
|
"openrouter",
|
||||||
|
);
|
||||||
|
|
||||||
|
const providerConfig = options.config.models?.providers?.openrouter;
|
||||||
|
const baseUrl = remoteBaseUrl || providerConfig?.baseUrl?.trim() || DEFAULT_OPENROUTER_BASE_URL;
|
||||||
|
const headerOverrides = Object.assign({}, providerConfig?.headers, remote?.headers);
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
...headerOverrides,
|
||||||
|
};
|
||||||
|
const model = normalizeOpenRouterModel(options.model);
|
||||||
|
return { baseUrl, headers, model };
|
||||||
|
}
|
||||||
@ -264,6 +264,131 @@ describe("embedding provider auto selection", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("embedding provider openrouter", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses openrouter with default base url", async () => {
|
||||||
|
const fetchMock = createFetchMock();
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
const { createEmbeddingProvider } = await import("./embeddings.js");
|
||||||
|
const authModule = await import("../agents/model-auth.js");
|
||||||
|
vi.mocked(authModule.resolveApiKeyForProvider).mockResolvedValue({
|
||||||
|
apiKey: "openrouter-key",
|
||||||
|
mode: "api-key",
|
||||||
|
source: "env: OPENROUTER_API_KEY",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await createEmbeddingProvider({
|
||||||
|
config: {} as never,
|
||||||
|
provider: "openrouter",
|
||||||
|
model: "openai/text-embedding-3-small",
|
||||||
|
fallback: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.provider.id).toBe("openrouter");
|
||||||
|
expect(result.provider.model).toBe("openai/text-embedding-3-small");
|
||||||
|
|
||||||
|
await result.provider.embedQuery("hello");
|
||||||
|
|
||||||
|
const [url, init] = fetchMock.mock.calls[0] ?? [];
|
||||||
|
expect(url).toBe("https://openrouter.ai/api/v1/embeddings");
|
||||||
|
const headers = (init?.headers ?? {}) as Record<string, string>;
|
||||||
|
expect(headers.Authorization).toBe("Bearer openrouter-key");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes model name by stripping openrouter/ prefix", async () => {
|
||||||
|
const fetchMock = createFetchMock();
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
const { createEmbeddingProvider } = await import("./embeddings.js");
|
||||||
|
const authModule = await import("../agents/model-auth.js");
|
||||||
|
vi.mocked(authModule.resolveApiKeyForProvider).mockResolvedValue({
|
||||||
|
apiKey: "openrouter-key",
|
||||||
|
mode: "api-key",
|
||||||
|
source: "env: OPENROUTER_API_KEY",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await createEmbeddingProvider({
|
||||||
|
config: {} as never,
|
||||||
|
provider: "openrouter",
|
||||||
|
model: "openrouter/openai/text-embedding-3-small",
|
||||||
|
fallback: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.provider.model).toBe("openai/text-embedding-3-small");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses openrouter in auto mode when openai/gemini are unavailable", async () => {
|
||||||
|
const fetchMock = createFetchMock();
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
const { createEmbeddingProvider } = await import("./embeddings.js");
|
||||||
|
const authModule = await import("../agents/model-auth.js");
|
||||||
|
vi.mocked(authModule.resolveApiKeyForProvider).mockImplementation(async ({ provider }) => {
|
||||||
|
if (provider === "openai") {
|
||||||
|
throw new Error('No API key found for provider "openai".');
|
||||||
|
}
|
||||||
|
if (provider === "google") {
|
||||||
|
throw new Error('No API key found for provider "google".');
|
||||||
|
}
|
||||||
|
if (provider === "openrouter") {
|
||||||
|
return { apiKey: "openrouter-key", source: "env: OPENROUTER_API_KEY", mode: "api-key" };
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected provider ${provider}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await createEmbeddingProvider({
|
||||||
|
config: {} as never,
|
||||||
|
provider: "auto",
|
||||||
|
model: "",
|
||||||
|
fallback: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.requestedProvider).toBe("auto");
|
||||||
|
expect(result.provider.id).toBe("openrouter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("batch embeds multiple texts in one request", async () => {
|
||||||
|
const fetchMock = vi.fn(async () => ({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
data: [{ embedding: [1, 2, 3] }, { embedding: [4, 5, 6] }, { embedding: [7, 8, 9] }],
|
||||||
|
}),
|
||||||
|
})) as unknown as typeof fetch;
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
const { createEmbeddingProvider } = await import("./embeddings.js");
|
||||||
|
const authModule = await import("../agents/model-auth.js");
|
||||||
|
vi.mocked(authModule.resolveApiKeyForProvider).mockResolvedValue({
|
||||||
|
apiKey: "openrouter-key",
|
||||||
|
mode: "api-key",
|
||||||
|
source: "env: OPENROUTER_API_KEY",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await createEmbeddingProvider({
|
||||||
|
config: {} as never,
|
||||||
|
provider: "openrouter",
|
||||||
|
model: "openai/text-embedding-3-small",
|
||||||
|
fallback: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
const embeddings = await result.provider.embedBatch(["text1", "text2", "text3"]);
|
||||||
|
|
||||||
|
expect(embeddings).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
const body = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body ?? "{}")) as {
|
||||||
|
input?: string[];
|
||||||
|
};
|
||||||
|
expect(body.input).toEqual(["text1", "text2", "text3"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("embedding provider local fallback", () => {
|
describe("embedding provider local fallback", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
|
|||||||
@ -5,10 +5,15 @@ import type { MoltbotConfig } from "../config/config.js";
|
|||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js";
|
import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js";
|
||||||
import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js";
|
import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js";
|
||||||
|
import {
|
||||||
|
createOpenRouterEmbeddingProvider,
|
||||||
|
type OpenRouterEmbeddingClient,
|
||||||
|
} from "./embeddings-openrouter.js";
|
||||||
import { importNodeLlamaCpp } from "./node-llama.js";
|
import { importNodeLlamaCpp } from "./node-llama.js";
|
||||||
|
|
||||||
export type { GeminiEmbeddingClient } from "./embeddings-gemini.js";
|
export type { GeminiEmbeddingClient } from "./embeddings-gemini.js";
|
||||||
export type { OpenAiEmbeddingClient } from "./embeddings-openai.js";
|
export type { OpenAiEmbeddingClient } from "./embeddings-openai.js";
|
||||||
|
export type { OpenRouterEmbeddingClient } from "./embeddings-openrouter.js";
|
||||||
|
|
||||||
export type EmbeddingProvider = {
|
export type EmbeddingProvider = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -19,24 +24,25 @@ export type EmbeddingProvider = {
|
|||||||
|
|
||||||
export type EmbeddingProviderResult = {
|
export type EmbeddingProviderResult = {
|
||||||
provider: EmbeddingProvider;
|
provider: EmbeddingProvider;
|
||||||
requestedProvider: "openai" | "local" | "gemini" | "auto";
|
requestedProvider: "openai" | "local" | "gemini" | "openrouter" | "auto";
|
||||||
fallbackFrom?: "openai" | "local" | "gemini";
|
fallbackFrom?: "openai" | "local" | "gemini" | "openrouter";
|
||||||
fallbackReason?: string;
|
fallbackReason?: string;
|
||||||
openAi?: OpenAiEmbeddingClient;
|
openAi?: OpenAiEmbeddingClient;
|
||||||
gemini?: GeminiEmbeddingClient;
|
gemini?: GeminiEmbeddingClient;
|
||||||
|
openRouter?: OpenRouterEmbeddingClient;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmbeddingProviderOptions = {
|
export type EmbeddingProviderOptions = {
|
||||||
config: MoltbotConfig;
|
config: MoltbotConfig;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
provider: "openai" | "local" | "gemini" | "auto";
|
provider: "openai" | "local" | "gemini" | "openrouter" | "auto";
|
||||||
remote?: {
|
remote?: {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
};
|
};
|
||||||
model: string;
|
model: string;
|
||||||
fallback: "openai" | "gemini" | "local" | "none";
|
fallback: "openai" | "gemini" | "openrouter" | "local" | "none";
|
||||||
local?: {
|
local?: {
|
||||||
modelPath?: string;
|
modelPath?: string;
|
||||||
modelCacheDir?: string;
|
modelCacheDir?: string;
|
||||||
@ -116,7 +122,7 @@ export async function createEmbeddingProvider(
|
|||||||
const requestedProvider = options.provider;
|
const requestedProvider = options.provider;
|
||||||
const fallback = options.fallback;
|
const fallback = options.fallback;
|
||||||
|
|
||||||
const createProvider = async (id: "openai" | "local" | "gemini") => {
|
const createProvider = async (id: "openai" | "local" | "gemini" | "openrouter") => {
|
||||||
if (id === "local") {
|
if (id === "local") {
|
||||||
const provider = await createLocalEmbeddingProvider(options);
|
const provider = await createLocalEmbeddingProvider(options);
|
||||||
return { provider };
|
return { provider };
|
||||||
@ -125,11 +131,15 @@ export async function createEmbeddingProvider(
|
|||||||
const { provider, client } = await createGeminiEmbeddingProvider(options);
|
const { provider, client } = await createGeminiEmbeddingProvider(options);
|
||||||
return { provider, gemini: client };
|
return { provider, gemini: client };
|
||||||
}
|
}
|
||||||
|
if (id === "openrouter") {
|
||||||
|
const { provider, client } = await createOpenRouterEmbeddingProvider(options);
|
||||||
|
return { provider, openRouter: client };
|
||||||
|
}
|
||||||
const { provider, client } = await createOpenAiEmbeddingProvider(options);
|
const { provider, client } = await createOpenAiEmbeddingProvider(options);
|
||||||
return { provider, openAi: client };
|
return { provider, openAi: client };
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatPrimaryError = (err: unknown, provider: "openai" | "local" | "gemini") =>
|
const formatPrimaryError = (err: unknown, provider: "openai" | "local" | "gemini" | "openrouter") =>
|
||||||
provider === "local" ? formatLocalSetupError(err) : formatError(err);
|
provider === "local" ? formatLocalSetupError(err) : formatError(err);
|
||||||
|
|
||||||
if (requestedProvider === "auto") {
|
if (requestedProvider === "auto") {
|
||||||
@ -145,7 +155,7 @@ export async function createEmbeddingProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const provider of ["openai", "gemini"] as const) {
|
for (const provider of ["openai", "gemini", "openrouter"] as const) {
|
||||||
try {
|
try {
|
||||||
const result = await createProvider(provider);
|
const result = await createProvider(provider);
|
||||||
return { ...result, requestedProvider };
|
return { ...result, requestedProvider };
|
||||||
|
|||||||
@ -20,9 +20,11 @@ import {
|
|||||||
type EmbeddingProviderResult,
|
type EmbeddingProviderResult,
|
||||||
type GeminiEmbeddingClient,
|
type GeminiEmbeddingClient,
|
||||||
type OpenAiEmbeddingClient,
|
type OpenAiEmbeddingClient,
|
||||||
|
type OpenRouterEmbeddingClient,
|
||||||
} from "./embeddings.js";
|
} from "./embeddings.js";
|
||||||
import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js";
|
import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js";
|
||||||
import { DEFAULT_OPENAI_EMBEDDING_MODEL } from "./embeddings-openai.js";
|
import { DEFAULT_OPENAI_EMBEDDING_MODEL } from "./embeddings-openai.js";
|
||||||
|
import { DEFAULT_OPENROUTER_EMBEDDING_MODEL } from "./embeddings-openrouter.js";
|
||||||
import {
|
import {
|
||||||
OPENAI_BATCH_ENDPOINT,
|
OPENAI_BATCH_ENDPOINT,
|
||||||
type OpenAiBatchRequest,
|
type OpenAiBatchRequest,
|
||||||
@ -123,11 +125,12 @@ export class MemoryIndexManager {
|
|||||||
private readonly workspaceDir: string;
|
private readonly workspaceDir: string;
|
||||||
private readonly settings: ResolvedMemorySearchConfig;
|
private readonly settings: ResolvedMemorySearchConfig;
|
||||||
private provider: EmbeddingProvider;
|
private provider: EmbeddingProvider;
|
||||||
private readonly requestedProvider: "openai" | "local" | "gemini" | "auto";
|
private readonly requestedProvider: "openai" | "local" | "gemini" | "openrouter" | "auto";
|
||||||
private fallbackFrom?: "openai" | "local" | "gemini";
|
private fallbackFrom?: "openai" | "local" | "gemini" | "openrouter";
|
||||||
private fallbackReason?: string;
|
private fallbackReason?: string;
|
||||||
private openAi?: OpenAiEmbeddingClient;
|
private openAi?: OpenAiEmbeddingClient;
|
||||||
private gemini?: GeminiEmbeddingClient;
|
private gemini?: GeminiEmbeddingClient;
|
||||||
|
private openRouter?: OpenRouterEmbeddingClient;
|
||||||
private batch: {
|
private batch: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
wait: boolean;
|
wait: boolean;
|
||||||
@ -224,6 +227,7 @@ export class MemoryIndexManager {
|
|||||||
this.fallbackReason = params.providerResult.fallbackReason;
|
this.fallbackReason = params.providerResult.fallbackReason;
|
||||||
this.openAi = params.providerResult.openAi;
|
this.openAi = params.providerResult.openAi;
|
||||||
this.gemini = params.providerResult.gemini;
|
this.gemini = params.providerResult.gemini;
|
||||||
|
this.openRouter = params.providerResult.openRouter;
|
||||||
this.sources = new Set(params.settings.sources);
|
this.sources = new Set(params.settings.sources);
|
||||||
this.db = this.openDatabase();
|
this.db = this.openDatabase();
|
||||||
this.providerKey = this.computeProviderKey();
|
this.providerKey = this.computeProviderKey();
|
||||||
@ -1318,14 +1322,16 @@ export class MemoryIndexManager {
|
|||||||
const fallback = this.settings.fallback;
|
const fallback = this.settings.fallback;
|
||||||
if (!fallback || fallback === "none" || fallback === this.provider.id) return false;
|
if (!fallback || fallback === "none" || fallback === this.provider.id) return false;
|
||||||
if (this.fallbackFrom) return false;
|
if (this.fallbackFrom) return false;
|
||||||
const fallbackFrom = this.provider.id as "openai" | "gemini" | "local";
|
const fallbackFrom = this.provider.id as "openai" | "gemini" | "openrouter" | "local";
|
||||||
|
|
||||||
const fallbackModel =
|
const fallbackModel =
|
||||||
fallback === "gemini"
|
fallback === "gemini"
|
||||||
? DEFAULT_GEMINI_EMBEDDING_MODEL
|
? DEFAULT_GEMINI_EMBEDDING_MODEL
|
||||||
: fallback === "openai"
|
: fallback === "openai"
|
||||||
? DEFAULT_OPENAI_EMBEDDING_MODEL
|
? DEFAULT_OPENAI_EMBEDDING_MODEL
|
||||||
: this.settings.model;
|
: fallback === "openrouter"
|
||||||
|
? DEFAULT_OPENROUTER_EMBEDDING_MODEL
|
||||||
|
: this.settings.model;
|
||||||
|
|
||||||
const fallbackResult = await createEmbeddingProvider({
|
const fallbackResult = await createEmbeddingProvider({
|
||||||
config: this.cfg,
|
config: this.cfg,
|
||||||
@ -1342,6 +1348,7 @@ export class MemoryIndexManager {
|
|||||||
this.provider = fallbackResult.provider;
|
this.provider = fallbackResult.provider;
|
||||||
this.openAi = fallbackResult.openAi;
|
this.openAi = fallbackResult.openAi;
|
||||||
this.gemini = fallbackResult.gemini;
|
this.gemini = fallbackResult.gemini;
|
||||||
|
this.openRouter = fallbackResult.openRouter;
|
||||||
this.providerKey = this.computeProviderKey();
|
this.providerKey = this.computeProviderKey();
|
||||||
this.batch = this.resolveBatchConfig();
|
this.batch = this.resolveBatchConfig();
|
||||||
log.warn(`memory embeddings: switched to fallback provider (${fallback})`, { reason });
|
log.warn(`memory embeddings: switched to fallback provider (${fallback})`, { reason });
|
||||||
@ -1758,6 +1765,20 @@ export class MemoryIndexManager {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (this.provider.id === "openrouter" && this.openRouter) {
|
||||||
|
const entries = Object.entries(this.openRouter.headers)
|
||||||
|
.filter(([key]) => key.toLowerCase() !== "authorization")
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
|
.map(([key, value]) => [key, value]);
|
||||||
|
return hashText(
|
||||||
|
JSON.stringify({
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: this.openRouter.baseUrl,
|
||||||
|
model: this.openRouter.model,
|
||||||
|
headers: entries,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
return hashText(JSON.stringify({ provider: this.provider.id, model: this.provider.model }));
|
return hashText(JSON.stringify({ provider: this.provider.id, model: this.provider.model }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export function computeEmbeddingProviderKey(params: {
|
|||||||
providerModel: string;
|
providerModel: string;
|
||||||
openAi?: { baseUrl: string; model: string; headers: Record<string, string> };
|
openAi?: { baseUrl: string; model: string; headers: Record<string, string> };
|
||||||
gemini?: { baseUrl: string; model: string; headers: Record<string, string> };
|
gemini?: { baseUrl: string; model: string; headers: Record<string, string> };
|
||||||
|
openRouter?: { baseUrl: string; model: string; headers: Record<string, string> };
|
||||||
}): string {
|
}): string {
|
||||||
if (params.openAi) {
|
if (params.openAi) {
|
||||||
const headerNames = fingerprintHeaderNames(params.openAi.headers);
|
const headerNames = fingerprintHeaderNames(params.openAi.headers);
|
||||||
@ -29,5 +30,16 @@ export function computeEmbeddingProviderKey(params: {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (params.openRouter) {
|
||||||
|
const headerNames = fingerprintHeaderNames(params.openRouter.headers);
|
||||||
|
return hashText(
|
||||||
|
JSON.stringify({
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: params.openRouter.baseUrl,
|
||||||
|
model: params.openRouter.model,
|
||||||
|
headerNames,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
return hashText(JSON.stringify({ provider: params.providerId, model: params.providerModel }));
|
return hashText(JSON.stringify({ provider: params.providerId, model: params.providerModel }));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user