diff --git a/packages/core/src/embedding.ts b/packages/core/src/embedding.ts index 467769f..6c4a580 100644 --- a/packages/core/src/embedding.ts +++ b/packages/core/src/embedding.ts @@ -609,6 +609,17 @@ export function _restoreProvider(token: unknown): void { * one-line warning from spamming on every fire-and-forget embed call. */ let remoteFallbackLogged = false; +/** + * Quick sanity check that a string looks like a real API key rather than + * a placeholder. Tools like Codex set `OPENAI_API_KEY=nokey` when routing + * through a custom base URL — using such a value for real API calls + * produces 401 errors. + */ +function looksLikeApiKey(key: string): boolean { + // Real API keys are at least 20 characters and don't look like + // common placeholders. + return key.length >= 20; +} /** * Build a remote `EmbeddingProvider` from whichever API key is in env. @@ -623,18 +634,23 @@ export function pickRemoteFallback(): { name: "voyage" | "openai"; provider: EmbeddingProvider; } | null { - if (process.env.VOYAGE_API_KEY) { + // Validate keys before using them — tools like Codex/OpenCode often set + // OPENAI_API_KEY to a placeholder (e.g. "nokey") when using a custom + // OPENAI_BASE_URL. Using such a key for real API calls produces 401 noise. + const voyageKey = process.env.VOYAGE_API_KEY; + if (voyageKey && looksLikeApiKey(voyageKey)) { const d = PROVIDER_DEFAULTS.voyage; return { name: "voyage", - provider: new VoyageProvider(process.env.VOYAGE_API_KEY, d.model, d.dimensions), + provider: new VoyageProvider(voyageKey, d.model, d.dimensions), }; } - if (process.env.OPENAI_API_KEY) { + const openaiKey = process.env.OPENAI_API_KEY; + if (openaiKey && looksLikeApiKey(openaiKey)) { const d = PROVIDER_DEFAULTS.openai; return { name: "openai", - provider: new OpenAIProvider(process.env.OPENAI_API_KEY, d.model, d.dimensions), + provider: new OpenAIProvider(openaiKey, d.model, d.dimensions), }; } return null; diff --git a/packages/core/test/embedding.test.ts b/packages/core/test/embedding.test.ts index a892aae..bad23be 100644 --- a/packages/core/test/embedding.test.ts +++ b/packages/core/test/embedding.test.ts @@ -211,7 +211,7 @@ describe("auto-fallback to remote provider when local provider is unavailable", test("auto-falls back to Voyage when VOYAGE_API_KEY is set", async () => { _markLocalProviderUnavailable(); - process.env.VOYAGE_API_KEY = "vk-test"; + process.env.VOYAGE_API_KEY = "vk-test-key-that-is-long-enough"; const { fetch, calls } = fakeFetch("voyage"); globalThis.fetch = fetch; @@ -224,7 +224,7 @@ describe("auto-fallback to remote provider when local provider is unavailable", test("auto-falls back to OpenAI when only OPENAI_API_KEY is set", async () => { _markLocalProviderUnavailable(); - process.env.OPENAI_API_KEY = "sk-test"; + process.env.OPENAI_API_KEY = "sk-test-key-that-is-long-enough"; const { fetch, calls } = fakeFetch("openai"); globalThis.fetch = fetch; @@ -237,8 +237,8 @@ describe("auto-fallback to remote provider when local provider is unavailable", test("Voyage wins when both keys are set", async () => { _markLocalProviderUnavailable(); - process.env.VOYAGE_API_KEY = "vk-test"; - process.env.OPENAI_API_KEY = "sk-test"; + process.env.VOYAGE_API_KEY = "vk-test-key-that-is-long-enough"; + process.env.OPENAI_API_KEY = "sk-test-key-that-is-long-enough"; const { fetch, calls } = fakeFetch("voyage"); globalThis.fetch = fetch; @@ -248,7 +248,7 @@ describe("auto-fallback to remote provider when local provider is unavailable", test("subsequent embed() calls go directly to the swapped provider (no double fail)", async () => { _markLocalProviderUnavailable(); - process.env.VOYAGE_API_KEY = "vk-test"; + process.env.VOYAGE_API_KEY = "vk-test-key-that-is-long-enough"; const { fetch, calls } = fakeFetch("voyage"); globalThis.fetch = fetch; @@ -290,23 +290,28 @@ describe("pickRemoteFallback", () => { }); test("returns Voyage when only VOYAGE_API_KEY is set", () => { - process.env.VOYAGE_API_KEY = "vk-test"; + process.env.VOYAGE_API_KEY = "vk-test-key-that-is-long-enough"; const result = pickRemoteFallback(); expect(result?.name).toBe("voyage"); }); test("returns OpenAI when only OPENAI_API_KEY is set", () => { - process.env.OPENAI_API_KEY = "sk-test"; + process.env.OPENAI_API_KEY = "sk-test-key-that-is-long-enough"; const result = pickRemoteFallback(); expect(result?.name).toBe("openai"); }); test("Voyage wins when both keys are set", () => { - process.env.VOYAGE_API_KEY = "vk-test"; - process.env.OPENAI_API_KEY = "sk-test"; + process.env.VOYAGE_API_KEY = "vk-test-key-that-is-long-enough"; + process.env.OPENAI_API_KEY = "sk-test-key-that-is-long-enough"; const result = pickRemoteFallback(); expect(result?.name).toBe("voyage"); }); + + test("rejects placeholder API keys (e.g. 'nokey')", () => { + process.env.OPENAI_API_KEY = "nokey"; + expect(pickRemoteFallback()).toBeNull(); + }); }); describe("vectorSearch", () => { diff --git a/packages/gateway/instrument.ts b/packages/gateway/instrument.ts index eca0445..9567ff4 100644 --- a/packages/gateway/instrument.ts +++ b/packages/gateway/instrument.ts @@ -87,6 +87,15 @@ if (sentryEnabled && !Sentry.isInitialized()) { /LocalProviderUnavailableError/, /ECONNRESET\b/, /ECONNREFUSED\b/, + // Remote embedding fallback with invalid/placeholder API key + /Incorrect API key/i, + /onnxruntime/i, + /Cannot find package 'onnxruntime-node'/, + /LoadLibrary failed/, + /Protobuf parsing failed/, + // Bun doesn't implement getSystemErrorMap from node:util — + // this crashes the Sentry SDK itself during error processing + /getSystemErrorMap/, ]; Sentry.init({