mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
OmniRoute v3.8.29 — 115 commits since v3.8.28. Full CHANGELOG + 41 i18n mirrors. All content quality gates green (build, unit 8/8, vitest 188/188, PR test policy, quality gates extended, docs sync, quality ratchet). Remaining red CI checks are pre-existing release flakes (coverage-shard/integration/node-compat teardown), a new transitive undici advisory in electron devDeps, and a workflow-level CodeQL fail (0 open alerts). VPS-validated by the operator.
318 lines
8.5 KiB
TypeScript
318 lines
8.5 KiB
TypeScript
/**
|
|
* Search Provider Registry
|
|
*
|
|
* Defines providers that support the /v1/search endpoint.
|
|
* Unlike LLM/embedding providers, search providers don't have "models" —
|
|
* a provider IS the model (Serper = Google SERP, Brave = Brave index).
|
|
*
|
|
* API keys are stored in the same provider credentials system,
|
|
* keyed by provider ID (e.g. "serper-search", "brave-search").
|
|
* perplexity-search reuses credentials from the "perplexity" chat provider.
|
|
*/
|
|
|
|
export interface SearchProviderConfig {
|
|
id: string;
|
|
name: string;
|
|
baseUrl: string;
|
|
method: "GET" | "POST";
|
|
authType: "apikey" | "none";
|
|
authHeader: string;
|
|
costPerQuery: number;
|
|
freeMonthlyQuota: number;
|
|
searchTypes: string[];
|
|
defaultMaxResults: number;
|
|
maxMaxResults: number;
|
|
timeoutMs: number;
|
|
cacheTTLMs: number;
|
|
/**
|
|
* Last-resort provider: excluded from automatic (cost-based) selection so a
|
|
* cost-0 free provider never overrides a configured paid one. Only used when no
|
|
* credentialed provider is available, or when requested explicitly by id.
|
|
*/
|
|
fallbackOnly?: boolean;
|
|
}
|
|
|
|
export const SEARCH_PROVIDERS: Record<string, SearchProviderConfig> = {
|
|
"serper-search": {
|
|
id: "serper-search",
|
|
name: "Serper Search",
|
|
baseUrl: "https://google.serper.dev",
|
|
method: "POST",
|
|
authType: "apikey",
|
|
authHeader: "x-api-key",
|
|
costPerQuery: 0.001,
|
|
freeMonthlyQuota: 2500,
|
|
searchTypes: ["web", "news"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 100,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
"brave-search": {
|
|
id: "brave-search",
|
|
name: "Brave Search",
|
|
baseUrl: "https://api.search.brave.com/res/v1",
|
|
method: "GET",
|
|
authType: "apikey",
|
|
authHeader: "x-subscription-token",
|
|
costPerQuery: 0.005,
|
|
freeMonthlyQuota: 1000,
|
|
searchTypes: ["web", "news"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 20,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
"perplexity-search": {
|
|
id: "perplexity-search",
|
|
name: "Perplexity Search",
|
|
baseUrl: "https://api.perplexity.ai/search",
|
|
method: "POST",
|
|
authType: "apikey",
|
|
authHeader: "bearer",
|
|
costPerQuery: 0.005,
|
|
freeMonthlyQuota: 0,
|
|
searchTypes: ["web"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 20,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
"exa-search": {
|
|
id: "exa-search",
|
|
name: "Exa Search",
|
|
baseUrl: "https://api.exa.ai/search",
|
|
method: "POST",
|
|
authType: "apikey",
|
|
authHeader: "x-api-key",
|
|
costPerQuery: 0.007,
|
|
freeMonthlyQuota: 1000,
|
|
searchTypes: ["web", "news"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 100,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
"tavily-search": {
|
|
id: "tavily-search",
|
|
name: "Tavily Search",
|
|
baseUrl: "https://api.tavily.com/search",
|
|
method: "POST",
|
|
authType: "apikey",
|
|
authHeader: "bearer",
|
|
costPerQuery: 0.008,
|
|
freeMonthlyQuota: 1000,
|
|
searchTypes: ["web", "news"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 20,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
"google-pse-search": {
|
|
id: "google-pse-search",
|
|
name: "Google Programmable Search",
|
|
baseUrl: "https://www.googleapis.com/customsearch/v1",
|
|
method: "GET",
|
|
authType: "apikey",
|
|
authHeader: "key",
|
|
costPerQuery: 0.005,
|
|
freeMonthlyQuota: 3000,
|
|
searchTypes: ["web", "news"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 10,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
"linkup-search": {
|
|
id: "linkup-search",
|
|
name: "Linkup Search",
|
|
baseUrl: "https://api.linkup.so/v1/search",
|
|
method: "POST",
|
|
authType: "apikey",
|
|
authHeader: "bearer",
|
|
costPerQuery: 0.005,
|
|
freeMonthlyQuota: 1000,
|
|
searchTypes: ["web"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 50,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
"searchapi-search": {
|
|
id: "searchapi-search",
|
|
name: "SearchAPI",
|
|
baseUrl: "https://www.searchapi.io/api/v1/search",
|
|
method: "GET",
|
|
authType: "apikey",
|
|
authHeader: "api_key",
|
|
costPerQuery: 0.004,
|
|
freeMonthlyQuota: 100,
|
|
searchTypes: ["web", "news"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 100,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
"youcom-search": {
|
|
id: "youcom-search",
|
|
name: "You.com Search",
|
|
baseUrl: "https://ydc-index.io/v1/search",
|
|
method: "GET",
|
|
authType: "apikey",
|
|
authHeader: "x-api-key",
|
|
costPerQuery: 0.005,
|
|
freeMonthlyQuota: 0,
|
|
searchTypes: ["web", "news"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 100,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
"searxng-search": {
|
|
id: "searxng-search",
|
|
name: "SearXNG Search",
|
|
baseUrl: "http://localhost:8888/search",
|
|
method: "GET",
|
|
authType: "none",
|
|
authHeader: "none",
|
|
costPerQuery: 0,
|
|
freeMonthlyQuota: 999999,
|
|
searchTypes: ["web", "news"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 50,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 3 * 60 * 1000,
|
|
},
|
|
|
|
"ollama-search": {
|
|
id: "ollama-search",
|
|
name: "Ollama Search",
|
|
baseUrl: "https://ollama.com/api/web_search",
|
|
method: "POST",
|
|
authType: "apikey",
|
|
authHeader: "bearer",
|
|
costPerQuery: 0,
|
|
freeMonthlyQuota: 1000,
|
|
searchTypes: ["web"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 10,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
"zai-search": {
|
|
id: "zai-search",
|
|
name: "Z.AI Coding Plan Search",
|
|
baseUrl: "https://api.z.ai/api/mcp/web_search_prime/mcp",
|
|
method: "POST",
|
|
authType: "apikey",
|
|
authHeader: "bearer",
|
|
costPerQuery: 0,
|
|
freeMonthlyQuota: 0,
|
|
searchTypes: ["web"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 50,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
},
|
|
|
|
// Free, no-API-key DuckDuckGo lite scraping (free-claude-code port). Last-resort
|
|
// only (fallbackOnly): never auto-selected over a configured provider; served by
|
|
// the dedicated HTML path in open-sse/handlers/search.ts (not the generic JSON one).
|
|
"duckduckgo-free": {
|
|
id: "duckduckgo-free",
|
|
name: "DuckDuckGo (free)",
|
|
baseUrl: "https://lite.duckduckgo.com/lite/",
|
|
method: "POST",
|
|
authType: "none",
|
|
authHeader: "none",
|
|
costPerQuery: 0,
|
|
freeMonthlyQuota: 999999,
|
|
searchTypes: ["web"],
|
|
defaultMaxResults: 5,
|
|
maxMaxResults: 25,
|
|
timeoutMs: 10_000,
|
|
cacheTTLMs: 5 * 60 * 1000,
|
|
fallbackOnly: true,
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Credential fallback mapping — search providers that can reuse credentials
|
|
* from a related provider (e.g., perplexity-search uses the same API key as perplexity chat).
|
|
*/
|
|
export const SEARCH_CREDENTIAL_FALLBACKS: Record<string, string> = {
|
|
"perplexity-search": "perplexity",
|
|
"ollama-search": "ollama-cloud",
|
|
"zai-search": "zai",
|
|
};
|
|
|
|
/**
|
|
* Get search provider config by ID
|
|
*/
|
|
export function getSearchProvider(providerId: string): SearchProviderConfig | null {
|
|
return SEARCH_PROVIDERS[providerId] || null;
|
|
}
|
|
|
|
export function supportsSearchType(
|
|
providerOrId: SearchProviderConfig | string | null | undefined,
|
|
searchType: string
|
|
): boolean {
|
|
const provider =
|
|
typeof providerOrId === "string" ? getSearchProvider(providerOrId) : providerOrId || null;
|
|
if (!provider) return false;
|
|
return provider.searchTypes.includes(searchType);
|
|
}
|
|
|
|
/**
|
|
* Get all search providers as a flat list
|
|
*/
|
|
export function getAllSearchProviders(): Array<{
|
|
id: string;
|
|
name: string;
|
|
searchTypes: string[];
|
|
}> {
|
|
return Object.values(SEARCH_PROVIDERS).map((p) => ({
|
|
id: p.id,
|
|
name: p.name,
|
|
searchTypes: p.searchTypes,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Select the cheapest available provider.
|
|
* If an explicit provider is given, validate and return it.
|
|
* Otherwise, return the cheapest by costPerQuery.
|
|
*/
|
|
export function selectProvider(
|
|
explicitProvider?: string,
|
|
searchType?: string
|
|
): SearchProviderConfig | null {
|
|
if (explicitProvider) {
|
|
const provider = SEARCH_PROVIDERS[explicitProvider] || null;
|
|
if (!provider) return null;
|
|
if (searchType && !supportsSearchType(provider, searchType)) return null;
|
|
return provider;
|
|
}
|
|
|
|
// Auto-selection excludes fallbackOnly providers so a free cost-0 provider never
|
|
// overrides a configured paid one — they are reached only via explicit id or the
|
|
// route handler's last-resort step.
|
|
const providers = Object.values(SEARCH_PROVIDERS).filter(
|
|
(provider) =>
|
|
!provider.fallbackOnly && (searchType ? supportsSearchType(provider, searchType) : true)
|
|
);
|
|
if (providers.length === 0) return null;
|
|
|
|
return providers.reduce((cheapest, p) => (p.costPerQuery < cheapest.costPerQuery ? p : cheapest));
|
|
}
|