diff --git a/index.ts b/index.ts index 48d7fb9..1ae6900 100644 --- a/index.ts +++ b/index.ts @@ -205,6 +205,11 @@ function parsePositiveInt(value: unknown): number | undefined { return undefined; } +function isLocalProvider(baseURL?: string): boolean { + if (!baseURL) return false; + return /localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0/i.test(baseURL); +} + function resolveHookAgentId( explicitAgentId: string | undefined, sessionKey: string | undefined, @@ -3160,8 +3165,12 @@ export function parsePluginConfig(value: unknown): PluginConfig { throw new Error("embedding config is required"); } + const resolvedBaseURL = + typeof embedding.baseURL === "string" ? resolveEnvVars(embedding.baseURL) : undefined; + const isLocal = isLocalProvider(resolvedBaseURL); + // Accept single key (string) or array of keys for round-robin rotation - let apiKey: string | string[]; + let apiKey: string | string[] | undefined; if (typeof embedding.apiKey === "string") { apiKey = embedding.apiKey; } else if (Array.isArray(embedding.apiKey) && embedding.apiKey.length > 0) { @@ -3183,7 +3192,16 @@ export function parsePluginConfig(value: unknown): PluginConfig { } if (!apiKey || (Array.isArray(apiKey) && apiKey.length === 0)) { - throw new Error("embedding.apiKey is required (set directly or via OPENAI_API_KEY env var)"); + if (isLocal) { + apiKey = "ollama"; + console.warn( + "[memory-lancedb-pro] No embedding.apiKey provided for local provider; using dummy key. This is expected for Ollama and similar local endpoints.", + ); + } else { + throw new Error( + "embedding.apiKey is required for cloud providers (set directly or via OPENAI_API_KEY env var)", + ); + } } const memoryReflectionRaw = typeof cfg.memoryReflection === "object" && cfg.memoryReflection !== null diff --git a/test/no-apikey-local-provider.test.mjs b/test/no-apikey-local-provider.test.mjs new file mode 100644 index 0000000..e8fa849 --- /dev/null +++ b/test/no-apikey-local-provider.test.mjs @@ -0,0 +1,68 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import jitiFactory from "jiti"; + +const jiti = jitiFactory(import.meta.url, { interopDefault: true }); + +const { parsePluginConfig } = jiti("../index.ts"); + +function baseConfig(overrides = {}) { + return { + embedding: { + provider: "openai-compatible", + ...overrides, + }, + }; +} + +function withEnv(value, fn) { + const prev = process.env.OPENAI_API_KEY; + if (value === undefined) { + delete process.env.OPENAI_API_KEY; + } else { + process.env.OPENAI_API_KEY = value; + } + try { + fn(); + } finally { + if (prev === undefined) { + delete process.env.OPENAI_API_KEY; + } else { + process.env.OPENAI_API_KEY = prev; + } + } +} + +describe("embedding apiKey handling for local providers", () => { + it("allows missing apiKey for localhost baseURL and uses dummy key", () => { + const warn = console.warn; + console.warn = () => {}; + try { + withEnv(undefined, () => { + const cfg = parsePluginConfig(baseConfig({ baseURL: "http://localhost:11434" })); + assert.equal(cfg.embedding.apiKey, "ollama"); + }); + } finally { + console.warn = warn; + } + }); + + it("throws for cloud provider when apiKey is missing", () => { + withEnv(undefined, () => { + assert.throws(() => parsePluginConfig(baseConfig({ baseURL: "https://api.jina.ai" })), /embedding\.apiKey is required/); + }); + }); + + it("preserves explicit apiKey for local provider", () => { + withEnv(undefined, () => { + const cfg = parsePluginConfig(baseConfig({ baseURL: "http://127.0.0.1:11434", apiKey: "local-key" })); + assert.equal(cfg.embedding.apiKey, "local-key"); + }); + }); + + it("throws when no baseURL and no apiKey", () => { + withEnv(undefined, () => { + assert.throws(() => parsePluginConfig(baseConfig({})), /embedding\.apiKey is required/); + }); + }); +});