fix: handle provider factory failover

This commit is contained in:
pi
2026-04-12 03:24:04 +01:00
parent 02b46c24b6
commit 3eb2ef166c
3 changed files with 84 additions and 1 deletions

View File

@@ -59,6 +59,8 @@ export function validateFirecrawlSearchRequest(providerName: string, request: No
}
export function validateFirecrawlFetchRequest(providerName: string, request: NormalizedFetchRequest) {
// Keep this defensive check here even though runtime validation also rejects it,
// so direct provider callers still get the same provider-specific error.
if (request.highlights) {
throw createProviderValidationError(providerName, 'does not support generic fetch option "highlights".');
}

View File

@@ -398,3 +398,67 @@ test("search starts with the explicitly requested provider and still follows its
assert.equal(result.execution.requestedProviderName, "firecrawl-main");
assert.equal(result.execution.actualProviderName, "exa-fallback");
});
test("search records provider factory failures and follows fallbacks", async () => {
const calls: string[] = [];
const runtime = createWebSearchRuntime({
loadConfig: async () => ({
path: "test.json",
defaultProviderName: "firecrawl-main",
defaultProvider: {
name: "firecrawl-main",
type: "firecrawl",
apiKey: "fc",
fallbackProviders: ["exa-fallback"],
},
providers: [
{
name: "firecrawl-main",
type: "firecrawl",
apiKey: "fc",
fallbackProviders: ["exa-fallback"],
},
{ name: "exa-fallback", type: "exa", apiKey: "exa" },
],
providersByName: new Map([
[
"firecrawl-main",
{ name: "firecrawl-main", type: "firecrawl", apiKey: "fc", fallbackProviders: ["exa-fallback"] },
],
["exa-fallback", { name: "exa-fallback", type: "exa", apiKey: "exa" }],
]),
}),
createProvider(providerConfig) {
if (providerConfig.name === "firecrawl-main") {
throw new Error("factory boom:firecrawl-main");
}
return createProvider(providerConfig.name, providerConfig.type, {
search: async () => {
calls.push(providerConfig.name);
return {
providerName: providerConfig.name,
results: [{ title: "Exa hit", url: "https://exa.ai" }],
};
},
});
},
});
const result = await runtime.search({ query: "pi docs", provider: "firecrawl-main" });
assert.deepEqual(calls, ["exa-fallback"]);
assert.deepEqual(result.execution.attempts, [
{
providerName: "firecrawl-main",
status: "failed",
reason: "factory boom:firecrawl-main",
},
{
providerName: "exa-fallback",
status: "succeeded",
},
]);
assert.equal(result.execution.actualProviderName, "exa-fallback");
});

View File

@@ -118,7 +118,24 @@ export function createWebSearchRuntime(
validateFetchRequestForProvider(providerName, providerConfig, request as NormalizedFetchRequest);
}
const provider = createProvider(providerConfig);
let provider: WebProvider;
try {
provider = createProvider(providerConfig);
} catch (error) {
attempts.push({
providerName,
status: "failed",
reason: (error as Error).message,
});
lastError = error;
for (const fallbackProviderName of providerConfig.fallbackProviders ?? []) {
if (!visited.has(fallbackProviderName)) {
pendingProviderNames.push(fallbackProviderName);
}
}
continue;
}
try {
const response = await provider[operation]({