/** * Kagi Web Search Provider * * Thin wrapper that adapts shared Kagi API utilities to SearchResponse shape. */ import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai"; import type { SearchResponse } from "../types"; import { SearchProviderError } from "../../../web/search/types"; import { KagiApiError, searchWithKagi } from "../../kagi"; import type { StructuredQuery } from "../query"; import { formatQuery, GOOGLE_QUERY_SYNTAX, parseSearchQuery } from "../query"; import { clampNumResults } from "../utils"; import type { SearchParams } from "./base"; import { SearchProvider } from "./base"; import { classifyProviderHttpError, toSearchSources } from "./utils"; type SearchParamsWithFetch = SearchParams & { fetch?: FetchImpl }; const DEFAULT_NUM_RESULTS = 10; const MAX_NUM_RESULTS = 40; /** Execute Kagi web search. */ export async function searchKagi(params: { query: string; num_results?: number; recency?: SearchParams["recency"]; parsedQuery?: StructuredQuery; signal?: AbortSignal; timeoutMs?: number; authStorage: AuthStorage; sessionId?: string; fetch?: FetchImpl; }): Promise { const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS); // Kagi's index understands the classic Google operator set: canonicalize // directives (domain: -> site:, until: -> before:YYYY-MM-DD, ...) and pass // them through in the query string. Directive-free queries stay untouched. const parsed = params.parsedQuery ?? parseSearchQuery(params.query); const query = parsed.hasDirectives ? formatQuery(parsed, GOOGLE_QUERY_SYNTAX) : params.query; try { const result = await searchWithKagi( query, { limit: numResults, recency: params.recency, sessionId: params.sessionId, signal: params.signal, timeoutMs: params.timeoutMs, fetch: params.fetch, }, params.authStorage, ); return { provider: "kagi", sources: toSearchSources(result.sources, numResults), relatedQuestions: result.relatedQuestions.length > 0 ? result.relatedQuestions : undefined, requestId: result.requestId, answer: result.answer, }; } catch (err) { if (err instanceof KagiApiError) { if (typeof err.statusCode === "number") { const classified = classifyProviderHttpError("kagi", err.statusCode, err.message); if (classified) throw classified; } throw new SearchProviderError("kagi", err.message, err.statusCode); } throw err; } } /** Search provider for Kagi web search. */ export class KagiProvider extends SearchProvider { readonly id = "kagi"; readonly label = "Kagi"; isAvailable(authStorage: AuthStorage): boolean { return authStorage.keys.source("kagi") !== undefined; } search(params: SearchParamsWithFetch): Promise { const fetchImpl = params.fetch; return searchKagi({ query: params.query, parsedQuery: params.parsedQuery, num_results: params.numSearchResults ?? params.limit, recency: params.recency, signal: params.signal, timeoutMs: params.timeoutMs, authStorage: params.authStorage, sessionId: params.sessionId, fetch: fetchImpl, }); } }