mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 17:12:27 +03:00
Validated on the combined batch board + this branch: context7-provider 33/33 + search-registry 45/45 (propagated to 17 providers), docs-counts green (350 everywhere: PROVIDER_REFERENCE regenerated, SVGs, README, AGENTS.md, package.json, llm.txt + 42 mirrors synced), provider-consistency OK. Hardening reviewed: canonical isValidContext7LibraryId shared by normalizer+executor, byte-capped error bodies, validated baseUrl with public fallback, anonymous-tier quota sentinel, excluded from auto-select walks. Thank you @HouMinXi — 15 forge-review cycles show!
178 lines
5.3 KiB
TypeScript
178 lines
5.3 KiB
TypeScript
/**
|
|
* Web Fetch Handler
|
|
*
|
|
* Handles POST /v1/web/fetch requests.
|
|
* Dispatches to a web-fetch provider executor (Firecrawl, Jina Reader, Tavily, or TinyFish).
|
|
*
|
|
* Request format:
|
|
* {
|
|
* "url": "https://example.com",
|
|
* "provider": "firecrawl" | "jina-reader" | "tavily-search" | "tinyfish", // optional
|
|
* "format": "markdown" | "html" | "links" | "screenshot",
|
|
* "depth": 0 | 1 | 2,
|
|
* "wait_for_selector": "main",
|
|
* "include_metadata": true
|
|
* }
|
|
*/
|
|
|
|
import { buildErrorBody, sanitizeErrorMessage } from "../utils/error.ts";
|
|
import { context7Fetch } from "../executors/context7-fetch.ts";
|
|
import { firecrawlFetch } from "../executors/firecrawl-fetch.ts";
|
|
import { jinaReaderFetch } from "../executors/jina-reader-fetch.ts";
|
|
import { tavilyFetch } from "../executors/tavily-fetch.ts";
|
|
import { tinyfishFetch } from "../executors/tinyfish-fetch.ts";
|
|
|
|
export type WebFetchFormat = "markdown" | "html" | "links" | "screenshot";
|
|
|
|
export interface WebFetchRequest {
|
|
url: string;
|
|
provider?: "firecrawl" | "jina-reader" | "tavily-search" | "tinyfish" | "context7";
|
|
format?: WebFetchFormat;
|
|
depth?: 0 | 1 | 2;
|
|
wait_for_selector?: string;
|
|
include_metadata?: boolean;
|
|
}
|
|
|
|
export interface WebFetchResponse {
|
|
provider: string;
|
|
url: string;
|
|
content: string;
|
|
links: string[];
|
|
metadata: { title: string | null; description: string | null; truncated?: boolean } | null;
|
|
screenshot_url: string | null;
|
|
}
|
|
|
|
export interface WebFetchResult {
|
|
success: boolean;
|
|
status?: number;
|
|
error?: string;
|
|
data?: WebFetchResponse;
|
|
}
|
|
|
|
export interface WebFetchCredentials {
|
|
apiKey?: string;
|
|
baseUrl?: string;
|
|
providerSpecificData?: Record<string, unknown>;
|
|
}
|
|
|
|
export const WEB_FETCH_PROVIDERS = Object.freeze([
|
|
"firecrawl",
|
|
"jina-reader",
|
|
"tavily-search",
|
|
"tinyfish",
|
|
"context7",
|
|
] as const);
|
|
// Derived from the array — adding a provider to WEB_FETCH_PROVIDERS
|
|
// automatically widens the union; they cannot drift apart.
|
|
export type WebFetchProviderId = (typeof WEB_FETCH_PROVIDERS)[number];
|
|
|
|
/**
|
|
* Providers that only run when the caller names them explicitly — they are not
|
|
* candidates for generic URL auto-select or fallback walks.
|
|
*
|
|
* The ReadonlySet type is compile-time protection only: Object.freeze cannot
|
|
* seal a Set's internal slots, so a determined JS caller could still mutate it.
|
|
* All repo consumers go through TypeScript, which is the threat model here.
|
|
*/
|
|
export const EXPLICIT_ONLY_WEB_FETCH_PROVIDERS: ReadonlySet<WebFetchProviderId> =
|
|
new Set<WebFetchProviderId>(["context7"]);
|
|
|
|
/**
|
|
* Providers whose upstream serves a usable anonymous tier, so an explicit
|
|
* request succeeds even with no configured connection.
|
|
*
|
|
* Compile-time protection only (see the note above on ReadonlySet).
|
|
*/
|
|
export const ANONYMOUS_CAPABLE_WEB_FETCH_PROVIDERS: ReadonlySet<WebFetchProviderId> =
|
|
new Set<WebFetchProviderId>(["context7"]);
|
|
|
|
/**
|
|
* Execute a web fetch request against the specified (or auto-selected) provider.
|
|
*
|
|
* @param req - Validated web fetch request body
|
|
* @param credentials - Provider API credentials (apiKey)
|
|
* @param resolvedProvider - Provider ID to use; if omitted auto-selects based on available creds
|
|
*/
|
|
export async function handleWebFetch(
|
|
req: WebFetchRequest,
|
|
credentials: WebFetchCredentials,
|
|
resolvedProvider?: WebFetchProviderId
|
|
): Promise<WebFetchResult> {
|
|
const provider = resolvedProvider ?? req.provider ?? "firecrawl";
|
|
|
|
const format: WebFetchFormat = req.format ?? "markdown";
|
|
const includeMetadata = req.include_metadata ?? false;
|
|
|
|
try {
|
|
switch (provider) {
|
|
case "firecrawl":
|
|
return await firecrawlFetch({
|
|
url: req.url,
|
|
format,
|
|
depth: req.depth ?? 0,
|
|
waitForSelector: req.wait_for_selector,
|
|
includeMetadata,
|
|
credentials,
|
|
});
|
|
|
|
case "jina-reader":
|
|
return await jinaReaderFetch({
|
|
url: req.url,
|
|
format,
|
|
includeMetadata,
|
|
credentials,
|
|
});
|
|
|
|
case "tavily-search":
|
|
return await tavilyFetch({
|
|
url: req.url,
|
|
format,
|
|
includeMetadata,
|
|
credentials,
|
|
});
|
|
|
|
case "tinyfish":
|
|
return await tinyfishFetch({
|
|
url: req.url,
|
|
format,
|
|
includeMetadata,
|
|
credentials,
|
|
});
|
|
|
|
case "context7":
|
|
// Context7 returns llms.txt text only: html/links/screenshot formats are
|
|
// unsupported, and the format field is validated/ignored below.
|
|
if (req.format && req.format !== "markdown") {
|
|
const body = buildErrorBody(
|
|
400,
|
|
`Provider 'context7' only supports format 'markdown' (llms.txt), got '${req.format}'`
|
|
);
|
|
return { success: false, status: 400, error: body.error.message };
|
|
}
|
|
return await context7Fetch({
|
|
url: req.url,
|
|
includeMetadata,
|
|
credentials,
|
|
});
|
|
|
|
default: {
|
|
const _exhaustive: never = provider;
|
|
return {
|
|
success: false,
|
|
status: 400,
|
|
error: `Unknown web fetch provider: ${_exhaustive}`,
|
|
};
|
|
}
|
|
}
|
|
} catch (err: unknown) {
|
|
const msg =
|
|
err instanceof Error ? sanitizeErrorMessage(err.message) : sanitizeErrorMessage(String(err));
|
|
const body = buildErrorBody(502, msg);
|
|
return {
|
|
success: false,
|
|
status: 502,
|
|
error: body.error.message,
|
|
};
|
|
}
|
|
}
|