{"version":3,"file":"tavily.mjs","names":[],"sources":["../../src/providers/tavily.ts"],"sourcesContent":["import type {\n  SearchResult,\n  SearchRequestOptions,\n  SearchResponse,\n  ReadOptions,\n  ReadResult,\n  ProviderConfig,\n} from \"../core/types.ts\";\nimport { Client } from \"../core/client.ts\";\nimport { Provider } from \"../core/provider.ts\";\nimport { TAVILY_SEARCH_TOPICS } from \"../core/providers.ts\";\nimport { AuthError, HTTPError, PaymentError, WebError, normalizeError } from \"../core/errors.ts\";\n\ntype TavilyTopic = (typeof TAVILY_SEARCH_TOPICS)[number];\n\ninterface TavilySearchRequest {\n  readonly api_key: string;\n  readonly query: string;\n  readonly max_results?: number;\n  readonly search_depth?: \"basic\" | \"advanced\";\n  readonly topic?: TavilyTopic;\n  readonly include_answer?: boolean;\n  readonly include_raw_content?: boolean;\n  readonly include_published_date?: boolean;\n  readonly include_domains?: readonly string[];\n  readonly exclude_domains?: readonly string[];\n  readonly start_date?: string;\n  readonly end_date?: string;\n}\n\ninterface TavilyResult {\n  readonly title: string;\n  readonly url: string;\n  readonly content: string;\n  readonly score: number;\n  readonly published_date?: string | null;\n  readonly raw_content?: string | null;\n}\n\ninterface TavilySearchResponse {\n  readonly results: readonly TavilyResult[];\n  readonly answer?: string;\n  readonly query: string;\n}\n\ntype TavilyExtractFormat = \"markdown\" | \"text\";\n\ninterface TavilyExtractRequest {\n  readonly urls: readonly string[];\n  readonly extract_depth: \"basic\" | \"advanced\";\n  readonly format: TavilyExtractFormat;\n  readonly timeout?: number;\n}\n\ninterface TavilyExtractResult {\n  readonly url: string;\n  readonly title?: string | null;\n  readonly raw_content?: string | null;\n}\n\ninterface TavilyExtractFailure {\n  readonly url: string;\n  readonly error: string;\n}\n\ninterface TavilyExtractResponse {\n  readonly results?: readonly TavilyExtractResult[];\n  readonly failed_results?: readonly TavilyExtractFailure[];\n  readonly request_id?: string;\n}\n\nconst TAVILY_USAGE_LIMIT_STATUS_CODES = new Set([432, 433]);\nconst TAVILY_MIN_EXTRACT_TIMEOUT_SECONDS = 1;\nconst TAVILY_MAX_EXTRACT_TIMEOUT_SECONDS = 60;\nconst TAVILY_EXTRACT_CLIENT_TIMEOUT_MS = 70_000;\n\nexport class TavilyProvider extends Provider {\n  static readonly providerName = \"tavily\";\n  static readonly defaultBaseURL = \"https://api.tavily.com\";\n\n  private readonly apiKey: string;\n  /** Extract waits up to 60 s on request and bills every attempt, so no retries and a longer window. */\n  private readonly readClient: Client;\n\n  constructor(config: Readonly<ProviderConfig>) {\n    super(config, TavilyProvider);\n    if (!config.apiKey) {\n      throw new AuthError(\"Missing API key for Tavily. Set TAVILY_API_KEY\", \"tavily\");\n    }\n\n    this.apiKey = config.apiKey;\n    this.readClient = new Client({ maxRetries: 0, timeout: TAVILY_EXTRACT_CLIENT_TIMEOUT_MS });\n  }\n\n  async search(query: string, options?: SearchRequestOptions): Promise<SearchResult[]> {\n    const response = await this.searchDetailed(query, options);\n    return response.results;\n  }\n\n  async searchDetailed(query: string, options?: SearchRequestOptions): Promise<SearchResponse> {\n    try {\n      const url = `${this.baseURL}/search`;\n      const response = await this.client.postJSON<TavilySearchResponse>(\n        url,\n        searchBody(this.apiKey, query, options ?? {}),\n        undefined,\n        options?.signal,\n      );\n      return {\n        results: response.results.map(mapResult),\n        ...(response.answer === undefined ? {} : { metadata: { answer: response.answer } }),\n      };\n    } catch (error) {\n      throw normalizeTavilyError(error);\n    }\n  }\n\n  async read(url: string, options?: Readonly<ReadOptions>): Promise<ReadResult> {\n    const format = normalizeReadFormat(options?.format);\n    try {\n      const response = await this.readClient.postJSON<TavilyExtractResponse>(\n        `${this.baseURL}/extract`,\n        extractBody(url, format, options?.timeout),\n        { Authorization: `Bearer ${this.apiKey}` },\n        options?.signal,\n      );\n      const result = response.results?.[0];\n      if (result) return mapExtractResult(result, format, response.request_id);\n      throw extractFailure(response.failed_results?.[0]);\n    } catch (error) {\n      throw normalizeTavilyError(error);\n    }\n  }\n}\n\n/**\n * Tavily answers a spent plan or pay-as-you-go cap with HTTP 432 or 433, never 402.\n * @param error - Rejected request.\n * @returns {WebError} Payment or normalized provider error.\n */\nfunction normalizeTavilyError(error: unknown): WebError {\n  if (error instanceof HTTPError && TAVILY_USAGE_LIMIT_STATUS_CODES.has(error.statusCode)) {\n    return new PaymentError(error.statusCode, error.url, error.body);\n  }\n  return normalizeError(error, \"tavily\");\n}\n\n/**\n * Tavily dates a hit only when asked, so every body asks; the date bounds are cut to `YYYY-MM-DD`.\n * @param apiKey - Tavily API key, which search takes in the body.\n * @param query - Search query.\n * @param options - Search options requested by the caller.\n * @returns {Record<string, unknown>} Request body for `POST /search`.\n */\nfunction searchBody(\n  apiKey: string,\n  query: string,\n  options: SearchRequestOptions,\n): Record<string, unknown> {\n  return {\n    api_key: apiKey,\n    query,\n    max_results: options.maxResults ?? 10,\n    search_depth: \"basic\",\n    ...(isTavilyTopic(options.category) ? { topic: options.category } : {}),\n    include_answer: options.summary ?? false,\n    include_raw_content: options.fullText ?? false,\n    include_published_date: true,\n    include_domains: options.includeDomains,\n    exclude_domains: options.excludeDomains,\n    ...(options.startPublishedDate ? { start_date: tavilyDate(options.startPublishedDate) } : {}),\n    ...(options.endPublishedDate ? { end_date: tavilyDate(options.endPublishedDate) } : {}),\n  } satisfies TavilySearchRequest;\n}\n\nfunction isTavilyTopic(category: string | undefined): category is TavilyTopic {\n  return TAVILY_SEARCH_TOPICS.some((topic) => topic === category);\n}\n\nfunction tavilyDate(value: string): string {\n  return value.slice(0, 10);\n}\n\n/**\n * `published_date` and `raw_content` are `null` when Tavily has none, so both are left out.\n * @param result - One Tavily search hit.\n * @returns {SearchResult} Normalized search result.\n */\nfunction mapResult(result: TavilyResult): SearchResult {\n  const publishedDate = isoDate(result.published_date);\n  return {\n    url: result.url,\n    title: result.title,\n    snippet: result.content,\n    score: result.score,\n    ...(publishedDate === undefined ? {} : { publishedDate }),\n    ...(typeof result.raw_content === \"string\" ? { text: result.raw_content } : {}),\n  };\n}\n\n/**\n * Tavily writes `Tue, 11 Mar 2025 17:00:00 GMT` where every other provider gives ISO 8601.\n * @param value - Tavily's `published_date`.\n * @returns {string | undefined} ISO 8601 date, the raw value when it does not parse, or nothing.\n */\nfunction isoDate(value: string | null | undefined): string | undefined {\n  if (typeof value !== \"string\" || value.length === 0) return undefined;\n  const time = Date.parse(value);\n  return Number.isNaN(time) ? value : new Date(time).toISOString();\n}\n\nfunction extractBody(\n  url: string,\n  format: TavilyExtractFormat,\n  timeout?: number,\n): Record<string, unknown> {\n  return {\n    urls: [url],\n    extract_depth: \"basic\",\n    format,\n    ...(timeout === undefined ? {} : { timeout: clampExtractTimeout(timeout) }),\n  } satisfies TavilyExtractRequest;\n}\n\nfunction clampExtractTimeout(timeout: number): number {\n  return Math.min(\n    Math.max(timeout, TAVILY_MIN_EXTRACT_TIMEOUT_SECONDS),\n    TAVILY_MAX_EXTRACT_TIMEOUT_SECONDS,\n  );\n}\n\nfunction normalizeReadFormat(format?: ReadOptions[\"format\"]): TavilyExtractFormat {\n  return format === \"text\" ? \"text\" : \"markdown\";\n}\n\n/**\n * Extract echoes the requested URL, so `url` stays the one asked for even after a redirect.\n * @param result - One extracted page.\n * @param format - Format the page was requested in.\n * @param requestId - Tavily request id for support.\n * @returns {ReadResult} Normalized page result.\n */\nfunction mapExtractResult(\n  result: Readonly<TavilyExtractResult>,\n  format: TavilyExtractFormat,\n  requestId?: string,\n): ReadResult {\n  const content = result.raw_content ?? \"\";\n  return {\n    url: result.url,\n    ...(result.title ? { title: result.title } : {}),\n    content,\n    ...(format === \"text\" ? { text: content } : {}),\n    ...(requestId === undefined ? {} : { metadata: { requestId } }),\n  };\n}\n\n/**\n * A page Extract could not fetch comes back inside HTTP 200 as a `failed_results` row with the reason.\n * @param failure - Failed row for the requested URL, when Tavily sent one.\n * @returns {WebError} Provider error carrying Tavily's reason.\n */\nfunction extractFailure(failure?: Readonly<TavilyExtractFailure>): WebError {\n  return new WebError(`Tavily extract failed: ${failure?.error ?? \"no result returned\"}`);\n}\n"],"mappings":"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"}