Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
68 changes: 68 additions & 0 deletions test/no-apikey-local-provider.test.mjs
Original file line number Diff line number Diff line change
@@ -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/);
});
});
});
Loading