Skip to content

Commit 45f8e19

Browse files
committed
fix(embed): allow separate OPENAI_EMBEDDING_BASE_URL + OPENAI_EMBEDDING_API_KEY
The OpenAI-compat embedding provider previously read OPENAI_BASE_URL and OPENAI_API_KEY only, which the chat-LLM path (src/providers/openai.ts) also reads. That couples both calls to the same endpoint — so operators who want, say, chat completions on a fast hosted provider (Novita / DeepInfra) and embeddings on a self-hosted vLLM cluster (Qwen3-Embedding-8B on a dedicated GPU) had to either move both to the same endpoint or run agentmemory against a single provider with whatever embedding model it happens to expose. Add two embedding-scoped overrides with fallback to the existing vars: OPENAI_EMBEDDING_BASE_URL → falls back to OPENAI_BASE_URL → default OPENAI_EMBEDDING_API_KEY → falls back to OPENAI_API_KEY → required The fallback chain keeps existing setups working without any .env changes. New setups can mix and match — common patterns: # vLLM (self-hosted GPU, free, batchable) for embeddings + Novita (DeepSeek V4 Flash) for chat OPENAI_BASE_URL=https://api.novita.ai/v3/openai OPENAI_API_KEY=sk-novita-... OPENAI_EMBEDDING_BASE_URL=https://embed.your.lan OPENAI_EMBEDDING_API_KEY=local-no-auth # endpoints that ignore Authorization OPENAI_EMBEDDING_MODEL=Qwen3-Embedding-8B OPENAI_EMBEDDING_DIMENSIONS=4096 # Local Ollama for embeddings + remote for chat OPENAI_BASE_URL=https://api.openai.com OPENAI_API_KEY=sk-... OPENAI_EMBEDDING_BASE_URL=http://localhost:11434 OPENAI_EMBEDDING_API_KEY=ollama OPENAI_EMBEDDING_MODEL=nomic-embed-text The separate API key matters because most local endpoints (Ollama / LM Studio / llama.cpp / vLLM) ignore Authorization entirely but Node fetch still requires a non-empty Bearer token. Setting OPENAI_EMBEDDING_API_KEY=anything-truthy unblocks that case without revealing the real OPENAI_API_KEY to whatever's on localhost. No code-paths other than the embedding provider are touched. Reviewed against the 17 existing test cases in test/embedding-provider.test.ts; no regression (the 6 pre-existing failures on main are env-pollution when ~/.agentmemory/.env has API keys set, unrelated to this change).
1 parent 29e6ebd commit 45f8e19

2 files changed

Lines changed: 44 additions & 12 deletions

File tree

src/providers/embedding/openai.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,30 @@ function resolveDimensions(model: string, override: string | undefined): number
4848
* `api-key` header instead of `Authorization: Bearer`.
4949
*
5050
* Required env vars:
51-
* OPENAI_API_KEY — API key
51+
* OPENAI_API_KEY — API key (fallback for OPENAI_EMBEDDING_API_KEY)
5252
*
5353
* Optional:
54-
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
55-
* Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
56-
* OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
57-
* OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
58-
* OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
59-
* custom / self-hosted models not in the
60-
* MODEL_DIMENSIONS table above)
54+
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
55+
* Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
56+
* OPENAI_EMBEDDING_BASE_URL — embedding-specific base URL override (defaults
57+
* to OPENAI_BASE_URL). Lets operators run
58+
* embeddings on a separate endpoint from chat —
59+
* e.g. local Ollama / LM Studio / llama.cpp /
60+
* vLLM at http://localhost:1234 for unlimited
61+
* free embeddings, while keeping chat
62+
* completions on a rate-limited but high-quality
63+
* hosted provider. Azure detection runs on
64+
* whichever URL ends up selected.
65+
* OPENAI_EMBEDDING_API_KEY — separate API key for the embedding endpoint
66+
* (defaults to OPENAI_API_KEY). Useful when the
67+
* embedding endpoint requires a different key
68+
* or no key at all (set to e.g. "local" for
69+
* endpoints that ignore Authorization).
70+
* OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
71+
* OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
72+
* OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
73+
* custom / self-hosted models not in the
74+
* MODEL_DIMENSIONS table above)
6175
*/
6276
export class OpenAIEmbeddingProvider implements EmbeddingProvider {
6377
readonly name = "openai";
@@ -69,9 +83,26 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider {
6983
private azureApiVersion: string;
7084

7185
constructor(apiKey?: string) {
72-
this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
73-
if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
74-
this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_BASE_URL"));
86+
// Separate API key path: caller-passed wins, then OPENAI_EMBEDDING_API_KEY,
87+
// then fall back to OPENAI_API_KEY. Allows e.g. a placeholder key for
88+
// local endpoints that ignore Authorization (most do).
89+
this.apiKey =
90+
apiKey ||
91+
getEnvVar("OPENAI_EMBEDDING_API_KEY") ||
92+
getEnvVar("OPENAI_API_KEY") ||
93+
"";
94+
if (!this.apiKey) {
95+
throw new Error(
96+
"API key is required (via constructor, OPENAI_EMBEDDING_API_KEY, or OPENAI_API_KEY)",
97+
);
98+
}
99+
// Embedding-specific base URL override; falls back to OPENAI_BASE_URL,
100+
// then normalizeBaseUrl's default. The chat-LLM path (src/providers/openai.ts)
101+
// still reads only OPENAI_BASE_URL, so setting OPENAI_EMBEDDING_BASE_URL
102+
// alone moves embeddings to the new endpoint without affecting chat.
103+
this.baseUrl = normalizeBaseUrl(
104+
getEnvVar("OPENAI_EMBEDDING_BASE_URL") || getEnvVar("OPENAI_BASE_URL"),
105+
);
75106
this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL;
76107
this.dimensions = resolveDimensions(
77108
this.model,

test/embedding-provider.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ describe("OpenAIEmbeddingProvider", () => {
7474

7575
it("throws when no API key is provided", () => {
7676
delete process.env["OPENAI_API_KEY"];
77-
expect(() => new OpenAIEmbeddingProvider()).toThrow("OPENAI_API_KEY is required");
77+
delete process.env["OPENAI_EMBEDDING_API_KEY"];
78+
expect(() => new OpenAIEmbeddingProvider()).toThrow(/API key is required.*OPENAI_EMBEDDING_API_KEY.*OPENAI_API_KEY/);
7879
});
7980

8081
it("respects OPENAI_BASE_URL env var", async () => {

0 commit comments

Comments
 (0)