feat: add Firecrawl provider support
This commit is contained in:
@@ -15,61 +15,54 @@ function createProvider(name: string, type: string, handlers: Partial<any>) {
|
||||
};
|
||||
}
|
||||
|
||||
test("search retries Tavily failures once with Exa", async () => {
|
||||
test("search follows configured fallback chains and records every attempt", async () => {
|
||||
const runtime = createWebSearchRuntime({
|
||||
loadConfig: async () => ({
|
||||
path: "test.json",
|
||||
defaultProviderName: "tavily-main",
|
||||
defaultProvider: { name: "tavily-main", type: "tavily", apiKey: "tvly" },
|
||||
defaultProviderName: "firecrawl-main",
|
||||
defaultProvider: {
|
||||
name: "firecrawl-main",
|
||||
type: "firecrawl",
|
||||
apiKey: "fc",
|
||||
fallbackProviders: ["tavily-backup"],
|
||||
},
|
||||
providers: [
|
||||
{ name: "tavily-main", type: "tavily", apiKey: "tvly" },
|
||||
{
|
||||
name: "firecrawl-main",
|
||||
type: "firecrawl",
|
||||
apiKey: "fc",
|
||||
fallbackProviders: ["tavily-backup"],
|
||||
},
|
||||
{
|
||||
name: "tavily-backup",
|
||||
type: "tavily",
|
||||
apiKey: "tvly",
|
||||
fallbackProviders: ["exa-fallback"],
|
||||
},
|
||||
{ name: "exa-fallback", type: "exa", apiKey: "exa" },
|
||||
],
|
||||
providersByName: new Map([
|
||||
["tavily-main", { name: "tavily-main", type: "tavily", apiKey: "tvly" }],
|
||||
[
|
||||
"firecrawl-main",
|
||||
{ name: "firecrawl-main", type: "firecrawl", apiKey: "fc", fallbackProviders: ["tavily-backup"] },
|
||||
],
|
||||
[
|
||||
"tavily-backup",
|
||||
{ name: "tavily-backup", type: "tavily", apiKey: "tvly", fallbackProviders: ["exa-fallback"] },
|
||||
],
|
||||
["exa-fallback", { name: "exa-fallback", type: "exa", apiKey: "exa" }],
|
||||
]),
|
||||
}),
|
||||
createProvider(providerConfig) {
|
||||
if (providerConfig.type === "tavily") {
|
||||
if (providerConfig.name === "exa-fallback") {
|
||||
return createProvider(providerConfig.name, providerConfig.type, {
|
||||
search: async () => {
|
||||
throw new Error("503 upstream unavailable");
|
||||
},
|
||||
search: async () => ({
|
||||
providerName: providerConfig.name,
|
||||
results: [{ title: "Exa hit", url: "https://exa.ai" }],
|
||||
}),
|
||||
});
|
||||
}
|
||||
return createProvider(providerConfig.name, providerConfig.type, {
|
||||
search: async () => ({
|
||||
providerName: providerConfig.name,
|
||||
results: [{ title: "Exa hit", url: "https://exa.ai" }],
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runtime.search({ query: "pi docs" });
|
||||
|
||||
assert.equal(result.execution.actualProviderName, "exa-fallback");
|
||||
assert.equal(result.execution.failoverFromProviderName, "tavily-main");
|
||||
assert.match(result.execution.failoverReason ?? "", /503/);
|
||||
});
|
||||
|
||||
test("search does not retry when Exa was explicitly selected", async () => {
|
||||
const runtime = createWebSearchRuntime({
|
||||
loadConfig: async () => ({
|
||||
path: "test.json",
|
||||
defaultProviderName: "tavily-main",
|
||||
defaultProvider: { name: "tavily-main", type: "tavily", apiKey: "tvly" },
|
||||
providers: [
|
||||
{ name: "tavily-main", type: "tavily", apiKey: "tvly" },
|
||||
{ name: "exa-fallback", type: "exa", apiKey: "exa" },
|
||||
],
|
||||
providersByName: new Map([
|
||||
["tavily-main", { name: "tavily-main", type: "tavily", apiKey: "tvly" }],
|
||||
["exa-fallback", { name: "exa-fallback", type: "exa", apiKey: "exa" }],
|
||||
]),
|
||||
}),
|
||||
createProvider(providerConfig) {
|
||||
return createProvider(providerConfig.name, providerConfig.type, {
|
||||
search: async () => {
|
||||
throw new Error(`boom:${providerConfig.name}`);
|
||||
@@ -78,8 +71,136 @@ test("search does not retry when Exa was explicitly selected", async () => {
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => runtime.search({ query: "pi docs", provider: "exa-fallback" }),
|
||||
/boom:exa-fallback/,
|
||||
);
|
||||
const result = await runtime.search({ query: "pi docs" });
|
||||
|
||||
assert.equal(result.execution.actualProviderName, "exa-fallback");
|
||||
assert.equal(result.execution.failoverFromProviderName, "firecrawl-main");
|
||||
assert.deepEqual(result.execution.attempts, [
|
||||
{
|
||||
providerName: "firecrawl-main",
|
||||
status: "failed",
|
||||
reason: "boom:firecrawl-main",
|
||||
},
|
||||
{
|
||||
providerName: "tavily-backup",
|
||||
status: "failed",
|
||||
reason: "boom:tavily-backup",
|
||||
},
|
||||
{
|
||||
providerName: "exa-fallback",
|
||||
status: "succeeded",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("search rejects a mismatched provider-specific options block before provider execution", async () => {
|
||||
let callCount = 0;
|
||||
|
||||
const runtime = createWebSearchRuntime({
|
||||
loadConfig: async () => ({
|
||||
path: "test.json",
|
||||
defaultProviderName: "firecrawl-main",
|
||||
defaultProvider: { name: "firecrawl-main", type: "firecrawl", apiKey: "fc" },
|
||||
providers: [{ name: "firecrawl-main", type: "firecrawl", apiKey: "fc" }],
|
||||
providersByName: new Map([["firecrawl-main", { name: "firecrawl-main", type: "firecrawl", apiKey: "fc" }]]),
|
||||
}),
|
||||
createProvider(providerConfig) {
|
||||
return createProvider(providerConfig.name, providerConfig.type, {
|
||||
search: async () => {
|
||||
callCount += 1;
|
||||
return {
|
||||
providerName: providerConfig.name,
|
||||
results: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => runtime.search({ query: "pi docs", tavily: { topic: "news" } }),
|
||||
/does not accept the "tavily" options block/,
|
||||
);
|
||||
assert.equal(callCount, 0);
|
||||
});
|
||||
|
||||
test("fetch rejects Firecrawl highlights before provider execution", async () => {
|
||||
let callCount = 0;
|
||||
|
||||
const runtime = createWebSearchRuntime({
|
||||
loadConfig: async () => ({
|
||||
path: "test.json",
|
||||
defaultProviderName: "firecrawl-main",
|
||||
defaultProvider: { name: "firecrawl-main", type: "firecrawl", apiKey: "fc" },
|
||||
providers: [{ name: "firecrawl-main", type: "firecrawl", apiKey: "fc" }],
|
||||
providersByName: new Map([["firecrawl-main", { name: "firecrawl-main", type: "firecrawl", apiKey: "fc" }]]),
|
||||
}),
|
||||
createProvider(providerConfig) {
|
||||
return createProvider(providerConfig.name, providerConfig.type, {
|
||||
fetch: async () => {
|
||||
callCount += 1;
|
||||
return {
|
||||
providerName: providerConfig.name,
|
||||
results: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => runtime.fetch({ urls: ["https://pi.dev"], highlights: true }),
|
||||
/does not support generic fetch option "highlights"/,
|
||||
);
|
||||
assert.equal(callCount, 0);
|
||||
});
|
||||
|
||||
test("search starts with the explicitly requested provider and still follows its fallback chain", async () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const runtime = createWebSearchRuntime({
|
||||
loadConfig: async () => ({
|
||||
path: "test.json",
|
||||
defaultProviderName: "tavily-main",
|
||||
defaultProvider: { name: "tavily-main", type: "tavily", apiKey: "tvly" },
|
||||
providers: [
|
||||
{ name: "tavily-main", type: "tavily", apiKey: "tvly" },
|
||||
{
|
||||
name: "firecrawl-main",
|
||||
type: "firecrawl",
|
||||
apiKey: "fc",
|
||||
fallbackProviders: ["exa-fallback"],
|
||||
},
|
||||
{ name: "exa-fallback", type: "exa", apiKey: "exa" },
|
||||
],
|
||||
providersByName: new Map([
|
||||
["tavily-main", { name: "tavily-main", type: "tavily", apiKey: "tvly" }],
|
||||
[
|
||||
"firecrawl-main",
|
||||
{ name: "firecrawl-main", type: "firecrawl", apiKey: "fc", fallbackProviders: ["exa-fallback"] },
|
||||
],
|
||||
["exa-fallback", { name: "exa-fallback", type: "exa", apiKey: "exa" }],
|
||||
]),
|
||||
}),
|
||||
createProvider(providerConfig) {
|
||||
return createProvider(providerConfig.name, providerConfig.type, {
|
||||
search: async () => {
|
||||
calls.push(providerConfig.name);
|
||||
if (providerConfig.name === "exa-fallback") {
|
||||
return {
|
||||
providerName: providerConfig.name,
|
||||
results: [{ title: "Exa hit", url: "https://exa.ai" }],
|
||||
};
|
||||
}
|
||||
throw new Error(`boom:${providerConfig.name}`);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runtime.search({ query: "pi docs", provider: "firecrawl-main" });
|
||||
|
||||
assert.deepEqual(calls, ["firecrawl-main", "exa-fallback"]);
|
||||
assert.equal(result.execution.requestedProviderName, "firecrawl-main");
|
||||
assert.equal(result.execution.actualProviderName, "exa-fallback");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user