fix(chat): guard search providers from OpenAI fallback (#10394)

Co-authored-by: DarkAngel <48388675+DarkEsteves@users.noreply.github.com>
This commit is contained in:
azzaouiomar19-sketch
2026-08-16 04:15:55 +01:00
committed by GitHub
parent 149049ca4a
commit 201c234b96
2 changed files with 74 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts";
import { AntigravityExecutor } from "./antigravity.ts";
import { GithubExecutor } from "./github.ts";
import { GheCopilotExecutor } from "./ghe-copilot.ts";
@@ -233,6 +234,17 @@ const defaultCache = new Map();
// follow-up once their own chat-routing behavior is confirmed.
const CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS = new Set(["jules"]);
// #10274 — providers that exist ONLY as /v1/search endpoint entries
// (SEARCH_PROVIDERS in open-sse/config/searchRegistry.ts) and have no chat-completions
// REGISTRY entry anywhere in open-sse/. Without this guard, getExecutor() silently falls
// through to DefaultExecutor's `PROVIDERS[provider] || PROVIDERS.openai` fallback, sending
// the user's real search API key (e.g. a Tavily `tvly-...` key) to OpenAI's endpoint and
// surfacing OpenAI's own "Incorrect API key provided" error for a provider the user believes
// is the search provider. The set is DERIVED from SEARCH_PROVIDERS so adding a new search
// provider without updating this guard fails the regression test automatically. Search
// providers must be executed through /v1/search, never the chat-completions path.
const CHAT_UNSUPPORTED_SEARCH_PROVIDERS = new Set(Object.keys(SEARCH_PROVIDERS));
export function getExecutor(provider) {
if (executors[provider]) return executors[provider];
if (CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS.has(provider)) {
@@ -242,6 +254,13 @@ export function getExecutor(provider) {
(err as Error & { status?: number }).status = 400;
throw err;
}
if (CHAT_UNSUPPORTED_SEARCH_PROVIDERS.has(provider)) {
const err = new Error(
`Provider "${provider}" is a search provider and does not support chat completions; use the /v1/search endpoint instead.`
);
(err as Error & { status?: number }).status = 400;
throw err;
}
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
return defaultCache.get(provider);
}

View File

@@ -0,0 +1,55 @@
// Probe for issue #10274 -- "Search providers (tavily/exa/firecrawl) leak API keys to
// api.openai.com when used as chat/combo targets".
//
// Search-only providers (tavily-search, exa-search, firecrawl, serper-search, ...) exist
// ONLY in SEARCH_PROVIDERS (open-sse/config/searchRegistry.ts) + the /v1/search catalog;
// they have no chat REGISTRY entry and no specialized executor. Routing one of them as a
// chat-completions target (e.g. a round-robin combo with targets "tavily-search/web")
// therefore fell through to DefaultExecutor's `PROVIDERS[provider] || PROVIDERS.openai`
// fallback, which forwarded the user's real search API key to https://api.openai.com
// (observed as `[401] Incorrect API key provided: tvly-...` from OpenAI, not from Tavily).
// This probe proves the executor-level root cause directly and pins the guard: getExecutor()
// must throw a clear, sanitized 400 for every search provider instead of silently inheriting
// OpenAI's base URL/config. The guard set is DERIVED from SEARCH_PROVIDERS so adding a new
// search provider without updating the guard fails this test.
import test from "node:test";
import assert from "node:assert/strict";
import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts";
import { SEARCH_PROVIDERS } from "../../open-sse/config/searchRegistry.ts";
const SEARCH_PROVIDER_IDS = Object.keys(SEARCH_PROVIDERS);
test("#10274: no search provider has a specialized chat executor", () => {
for (const id of SEARCH_PROVIDER_IDS) {
assert.equal(
hasSpecializedExecutor(id),
false,
`search provider '${id}' must not have a specialized chat executor`
);
}
});
test("#10274: a chat-completion request routed to a search provider must not silently hit OpenAI's endpoint", () => {
// Desired behavior: search providers (registered only in SEARCH_PROVIDERS, never in the
// chat REGISTRY) must not silently resolve to OpenAI's chat/completions endpoint when
// routed through the normal chat-completions executor path. getExecutor() must throw a
// clear, sanitized error for this set instead of falling through to DefaultExecutor's
// `PROVIDERS.openai` fallback (which produced the "Incorrect API key provided: tvly-..."
// OpenAI error the reporter saw for genuine Tavily/Exa/Firecrawl keys). Before the fix,
// getExecutor("tavily-search") returned a working executor whose buildUrl() resolved to
// OpenAI's endpoint -- this assertion FAILS on unfixed release/v3.8.50 code because no
// error is thrown at all.
for (const id of SEARCH_PROVIDER_IDS) {
assert.throws(
() => getExecutor(id),
(err) => {
assert.match(err.message, /search provider/i);
assert.match(err.message, /does not support chat completions/i);
assert.match(err.message, /\/v1\/search/i);
assert.equal(err.status, 400);
return true;
},
`search provider '${id}' must raise a clear error instead of inheriting OpenAI's base URL/config`
);
}
});