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
12 changes: 9 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@
# -----------------------------------------------------------------------------
#
# Without an embedding key, agentmemory runs in BM25-only mode for hybrid
# search. Detection order: EMBEDDING_PROVIDER override → GEMINI_API_KEY →
# OPENAI_API_KEY → VOYAGE_API_KEY → COHERE_API_KEY → OPENROUTER_API_KEY →
# search. Detection order: EMBEDDING_PROVIDER override →
# OPENAI_EMBEDDING_API_KEY → GEMINI_API_KEY → OPENAI_API_KEY →
# VOYAGE_API_KEY → COHERE_API_KEY → OPENROUTER_API_KEY →
# local (Xenova/all-MiniLM-L6-v2, 384-dim).

# EMBEDDING_PROVIDER=local # local | openai | voyage | cohere | gemini | openrouter
Expand All @@ -73,7 +74,12 @@

# COHERE_API_KEY=... # General-purpose embeddings

# Reuses OPENAI_API_KEY / OPENAI_BASE_URL above when EMBEDDING_PROVIDER=openai.
# When EMBEDDING_PROVIDER=openai, embeddings reuse OPENAI_API_KEY /
# OPENAI_BASE_URL above by default. Set the two vars below to point
# embeddings at a different endpoint / key than the chat LLM — e.g. chat
# on a self-hosted OpenAI-compatible proxy, embeddings on OpenAI proper.
# OPENAI_EMBEDDING_API_KEY=sk-... # Embedding-specific key; takes precedence over OPENAI_API_KEY
# OPENAI_EMBEDDING_BASE_URL=https://api.openai.com # Embedding-specific base URL; takes precedence over OPENAI_BASE_URL
# OPENAI_EMBEDDING_MODEL=text-embedding-3-small # Embedding model when EMBEDDING_PROVIDER=openai
# OPENAI_EMBEDDING_DIMENSIONS=1536 # Required when the model is not in the known-models table

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,8 @@ Create `~/.agentmemory/.env`:
# VOYAGE_API_KEY=...
# OPENAI_API_KEY=sk-...
# OPENAI_BASE_URL=https://api.openai.com # Override for Azure / vLLM / LM Studio / proxies
# OPENAI_EMBEDDING_API_KEY=sk-... # Optional: embedding-specific key; takes precedence over OPENAI_API_KEY
# OPENAI_EMBEDDING_BASE_URL=https://api.openai.com # Optional: embedding-specific base URL; takes precedence over OPENAI_BASE_URL
# OPENAI_EMBEDDING_MODEL=text-embedding-3-small
# OPENAI_EMBEDDING_DIMENSIONS=1536 # Required when the model is not in the known-models table

Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ export function detectEmbeddingProvider(
const forced = source["EMBEDDING_PROVIDER"];
if (forced) return forced;

// Embedding-specific key beats generic provider auto-detection: it's a
// stronger signal of intent than a chat-LLM key that merely shares the
// OpenAI wire shape. Without this, "chat on Gemini, embeddings on OpenAI"
// silently routes embeddings through the Gemini branch.
if (source["OPENAI_EMBEDDING_API_KEY"]) return "openai";

if (source["GEMINI_API_KEY"]) return "gemini";
if (source["OPENAI_API_KEY"]) return "openai";
if (source["VOYAGE_API_KEY"]) return "voyage";
Expand Down
2 changes: 1 addition & 1 deletion src/providers/embedding/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function createEmbeddingProvider(): EmbeddingProvider | null {
case "gemini":
return withDimensionGuard(new GeminiEmbeddingProvider(getEnvVar("GEMINI_API_KEY")!));
case "openai":
return withDimensionGuard(new OpenAIEmbeddingProvider(getEnvVar("OPENAI_API_KEY")!));
return withDimensionGuard(new OpenAIEmbeddingProvider());
case "voyage":
return withDimensionGuard(new VoyageEmbeddingProvider(getEnvVar("VOYAGE_API_KEY")!));
case "cohere":
Expand Down
2 changes: 1 addition & 1 deletion src/providers/embedding/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function resolveDimensions(model: string, override: string | undefined): number
* `api-key` header instead of `Authorization: Bearer`.
*
* Required env vars:
* OPENAI_API_KEY — API key (fallback for OPENAI_EMBEDDING_API_KEY)
* OPENAI_API_KEY — Fallback API key when OPENAI_EMBEDDING_API_KEY is unset
*
* Optional:
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
Expand Down
40 changes: 40 additions & 0 deletions test/embedding-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ describe("createEmbeddingProvider", () => {
delete process.env["COHERE_API_KEY"];
delete process.env["OPENROUTER_API_KEY"];
delete process.env["EMBEDDING_PROVIDER"];
delete process.env["OPENAI_BASE_URL"];
delete process.env["OPENAI_EMBEDDING_API_KEY"];
delete process.env["OPENAI_EMBEDDING_BASE_URL"];
delete process.env["OPENAI_EMBEDDING_MODEL"];
delete process.env["OPENAI_EMBEDDING_DIMENSIONS"];
});

afterEach(() => {
Expand Down Expand Up @@ -50,6 +55,41 @@ describe("createEmbeddingProvider", () => {
const provider = createEmbeddingProvider();
expect(provider).toBeInstanceOf(OpenAIEmbeddingProvider);
});

it("uses OPENAI_EMBEDDING_API_KEY in Authorization when both keys are set", async () => {
process.env["OPENAI_API_KEY"] = "llm-key";
process.env["OPENAI_EMBEDDING_API_KEY"] = "embedding-key";
const provider = createEmbeddingProvider();
expect(provider).toBeInstanceOf(OpenAIEmbeddingProvider);

const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
JSON.stringify({ data: [{ embedding: new Array(1536).fill(0.1) }] }),
{ status: 200 },
),
);

await provider!.embed("hi");
const headers = (fetchSpy.mock.calls[0]![1] as RequestInit).headers as Record<string, string>;
expect(headers["Authorization"]).toBe("Bearer embedding-key");

fetchSpy.mockRestore();
});

it("detects openai when only OPENAI_EMBEDDING_API_KEY is set", () => {
process.env["OPENAI_EMBEDDING_API_KEY"] = "embedding-only-key";
const provider = createEmbeddingProvider();
expect(provider).toBeInstanceOf(OpenAIEmbeddingProvider);
expect(provider!.name).toBe("openai");
});

it("OPENAI_EMBEDDING_API_KEY takes precedence over GEMINI_API_KEY for embedding detection", () => {
process.env["GEMINI_API_KEY"] = "gemini-chat-key";
process.env["OPENAI_EMBEDDING_API_KEY"] = "embedding-key";
const provider = createEmbeddingProvider();
expect(provider).toBeInstanceOf(OpenAIEmbeddingProvider);
expect(provider!.name).toBe("openai");
});
});

describe("OpenAIEmbeddingProvider", () => {
Expand Down