Skip to content
Merged
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
24 changes: 20 additions & 4 deletions packages/core/src/embedding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down
23 changes: 14 additions & 9 deletions packages/core/test/embedding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -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", () => {
Expand Down
9 changes: 9 additions & 0 deletions packages/gateway/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading