UNPKG

exa-js

Version:

Exa SDK for Node.js and the browser

1 lines 302 kB
{"version":3,"sources":["../src/index.ts","../package.json","../src/errors.ts","../src/zod-utils.ts","../src/agent/base.ts","../src/agent/client.ts","../src/monitors/base.ts","../src/monitors/client.ts","../src/research/base.ts","../src/research/client.ts","../src/websets/base.ts","../src/websets/enrichments.ts","../src/websets/events.ts","../src/websets/openapi.ts","../src/websets/imports.ts","../src/websets/items.ts","../src/websets/monitors.ts","../src/websets/searches.ts","../src/websets/webhooks.ts","../src/websets/client.ts","../src/agent/types.ts"],"sourcesContent":["import fetch, { Headers } from \"cross-fetch\";\nimport { ZodSchema } from \"zod\";\nimport packageJson from \"../package.json\";\nimport { AgentClient, BetaClient } from \"./agent/client\";\nimport { ExaError, HttpStatusCode } from \"./errors\";\nimport { SearchMonitorsClient } from \"./monitors/client\";\nimport { ResearchClient } from \"./research/client\";\nimport { WebsetsClient } from \"./websets/client\";\nimport { isZodSchema, zodToJsonSchema } from \"./zod-utils\";\n\n// Use native fetch in Node.js environments\nconst fetchImpl =\n typeof global !== \"undefined\" && global.fetch ? global.fetch : fetch;\nconst HeadersImpl =\n typeof global !== \"undefined\" && global.Headers ? global.Headers : Headers;\n\nconst DEFAULT_MAX_CHARACTERS = 10_000;\n\n// Longest snippet of a non-JSON response body to include in error messages.\nconst NON_JSON_BODY_SNIPPET_LENGTH = 500;\n\n/**\n * Reads a fetch Response body once and attempts to parse it as JSON.\n *\n * API gateways and proxies (Cloudflare, load balancers, nginx) sometimes\n * return HTML or plain-text error pages even when the SDK expects JSON.\n * Calling `response.json()` on those bodies throws an opaque\n * `SyntaxError: Unexpected token '<', \"<!DOCTYPE \"... is not valid JSON`\n * that hides the real HTTP status. Reading the body as text first lets the\n * caller surface the actual status and a snippet of the response instead.\n *\n * @returns The parsed JSON value (or `undefined` when the body is empty or not\n * valid JSON) alongside the raw response text.\n */\nasync function readResponseBody(\n response: Response\n): Promise<{ json: any; text: string }> {\n const text = await response.text();\n if (!text) {\n return { json: undefined, text };\n }\n try {\n return { json: JSON.parse(text), text };\n } catch {\n return { json: undefined, text };\n }\n}\n\n/**\n * Collapses whitespace and truncates a non-JSON response body so it stays\n * readable when embedded in an error message.\n */\nfunction truncateResponseBody(text: string): string {\n const collapsed = text.replace(/\\s+/g, \" \").trim();\n return collapsed.length > NON_JSON_BODY_SNIPPET_LENGTH\n ? `${collapsed.slice(0, NON_JSON_BODY_SNIPPET_LENGTH)}…`\n : collapsed;\n}\n\n/**\n * Options for retrieving page contents\n * @typedef {Object} ContentsOptions\n * @property {TextContentsOptions | boolean} [text] - Options for retrieving text contents.\n * @property {HighlightsContentsOptions | boolean} [highlights] - Options for retrieving highlights.\n * @property {SummaryContentsOptions | boolean} [summary] - Options for retrieving summary.\n * @property {ContextOptions | boolean} [context] - DEPRECATED: Use `text` or `highlights` instead. Will be removed in a future version.\n * @property {number} [maxAgeHours] - Maximum age of cached content in hours. If content is older, it will be fetched fresh. Special values: 0 = always fetch fresh content, -1 = never fetch fresh (use cached content only). Example: 168 = fetch fresh for pages older than 7 days.\n * @property {boolean} [filterEmptyResults] - If true, filters out results with no contents. Default is true.\n * @property {number} [subpages] - The number of subpages to return for each result, where each subpage is derived from an internal link for the result.\n * @property {string | string[]} [subpageTarget] - Text used to match/rank subpages in the returned subpage list. You could use \"about\" to get *about* page for websites. Note that this is a fuzzy matcher.\n * @property {ExtrasOptions} [extras] - Miscelleneous data derived from results\n */\nexport type ContentsOptions = {\n text?: TextContentsOptions | true;\n highlights?: HighlightsContentsOptions | true;\n summary?: SummaryContentsOptions | true;\n livecrawl?: LivecrawlOptions;\n /**\n * DEPRECATED: Use `text` or `highlights` instead. Will be removed in a future version.\n * @deprecated Use `text` or `highlights` instead. This legacy `context` field will be removed in a future version.\n */\n context?: ContextOptions | true;\n livecrawlTimeout?: number;\n maxAgeHours?: number;\n filterEmptyResults?: boolean;\n subpages?: number;\n subpageTarget?: string | string[];\n extras?: ExtrasOptions;\n};\n\n/**\n * Options for performing a search query\n * @typedef {Object} SearchOptions\n * @property {ContentsOptions | boolean} [contents] - Options for retrieving page contents for each result returned. Default is { text: { maxCharacters: 10_000 } }.\n * @property {number} [numResults] - Number of search results to return. Default 10.\n * @property {string[]} [includeDomains] - List of domains to include in the search.\n * @property {string[]} [excludeDomains] - List of domains to exclude in the search.\n * @property {string} [startCrawlDate] - Start date for results based on crawl date.\n * @property {string} [endCrawlDate] - End date for results based on crawl date.\n * @property {string} [startPublishedDate] - Start date for results based on published date.\n * @property {string} [endPublishedDate] - End date for results based on published date.\n * @property {string} [category] - A data category to focus on, with higher comprehensivity and data cleanliness.\n * @property {string[]} [includeText] - List of strings that must be present in webpage text of results. Currently only supports 1 string of up to 5 words.\n * @property {string[]} [excludeText] - List of strings that must not be present in webpage text of results. Currently only supports 1 string of up to 5 words.\n * @property {string[]} [flags] - Experimental flags\n * @property {string} [userLocation] - The two-letter ISO country code of the user, e.g. US.\n * @property {boolean} [stream] - Whether to stream back OpenAI-style chat completion chunks. Use `streamSearch()` instead of `search({ stream: true })`.\n */\nexport type BaseSearchOptions = {\n contents?: ContentsOptions;\n numResults?: number;\n includeDomains?: string[];\n excludeDomains?: string[];\n startCrawlDate?: string;\n endCrawlDate?: string;\n startPublishedDate?: string;\n endPublishedDate?: string;\n category?:\n | \"company\"\n | \"research paper\"\n | \"news\"\n | \"pdf\"\n | \"personal site\"\n | \"financial report\"\n | \"people\";\n includeText?: string[];\n excludeText?: string[];\n flags?: string[];\n userLocation?: string;\n stream?: boolean;\n};\n\n/**\n * Base search options shared across all search types\n */\ntype BaseRegularSearchOptions = BaseSearchOptions & {\n /**\n * If true, the search results are moderated for safety.\n */\n moderation?: boolean;\n useAutoprompt?: boolean;\n /**\n * Additional instructions that guide both the search process and the final returned synthesis.\n * Use this to prefer certain sources, emphasize novelty, avoid duplicates, or constrain output style.\n */\n systemPrompt?: string;\n /**\n * Output schema for search responses. When provided, the API returns synthesized output in `output`.\n * - `type: \"text\"` for plain text output (optionally guided by `description`)\n * - `type: \"object\"` for structured JSON output\n *\n * Note: For object schemas, the API enforces max depth 2 and max 10 total properties.\n */\n outputSchema?: DeepOutputSchema;\n};\n\nexport type DeepSearchType = \"deep-lite\" | \"deep\" | \"deep-reasoning\";\n\n/**\n * Deep search output schema mode for plain text responses.\n */\nexport type DeepTextOutputSchema = {\n type: \"text\";\n /**\n * Optional formatting guidance for text output.\n */\n description?: string;\n};\n\n/**\n * Deep search output schema mode for structured JSON object responses.\n */\nexport type DeepObjectOutputSchema = {\n type: \"object\";\n /**\n * JSON-schema-style properties for the result object.\n */\n properties?: Record<string, unknown>;\n /**\n * Required property names.\n */\n required?: string[];\n};\n\n/**\n * Search output schema.\n * - `type: \"text\"` returns plain text in `output.content` (with optional description guidance).\n * - `type: \"object\"` returns structured JSON in `output.content`.\n *\n * Note: For object schemas, the API enforces a maximum nesting depth of 2 and a maximum of 10 total properties.\n */\nexport type DeepOutputSchema = DeepTextOutputSchema | DeepObjectOutputSchema;\n\n/**\n * Contents options for deep search.\n */\ntype DeepContentsOptions = Omit<ContentsOptions, \"context\"> & {\n /**\n * DEPRECATED: Use `text` or `highlights` instead. Will be removed in a future version.\n * @deprecated Use `text` or `highlights` instead. This legacy `context` field will be removed in a future version.\n */\n context?: ContextOptions | true;\n};\n\n/**\n * Search options for deep search types, which additionally support additional queries.\n */\ntype DeepSearchOptions = Omit<BaseRegularSearchOptions, \"contents\"> & {\n type: DeepSearchType;\n /**\n * Alternative query formulations for deep search variants to skip automatic LLM-based query expansion.\n * Max 5 queries.\n * @example [\"machine learning\", \"ML algorithms\", \"neural networks\"]\n */\n additionalQueries?: string[];\n /**\n * Options for retrieving page contents.\n */\n contents?: DeepContentsOptions;\n};\n\n/**\n * Search options for non-deep search types (keyword, neural, auto, hybrid, fast, instant)\n */\ntype NonDeepSearchOptions = BaseRegularSearchOptions & {\n type?: \"keyword\" | \"neural\" | \"auto\" | \"hybrid\" | \"fast\" | \"instant\";\n};\n\n/**\n * Search options for performing a search query.\n * Uses a discriminated union to ensure additionalQueries is only allowed when type is a deep search variant.\n */\nexport type RegularSearchOptions = DeepSearchOptions | NonDeepSearchOptions;\n\n/**\n * DEPRECATED: Used only by deprecated `findSimilar()` APIs. Use `search()` and `RegularSearchOptions` for new search flows. Will be removed in a future version.\n * @deprecated Use `search()` and `RegularSearchOptions` for new search flows. There is no direct replacement for URL-based similarity.\n *\n * Options for finding similar links.\n * @typedef {Object} FindSimilarOptions\n * @property {boolean} [excludeSourceDomain] - If true, excludes links from the base domain of the input.\n */\nexport type FindSimilarOptions = BaseSearchOptions & {\n excludeSourceDomain?: boolean;\n};\n\nexport type ExtrasOptions = { links?: number; imageLinks?: number };\n\n/**\n * Options for livecrawling contents\n * @typedef {string} LivecrawlOptions\n */\nexport type LivecrawlOptions =\n | \"never\"\n | \"fallback\"\n | \"always\"\n | \"auto\"\n | \"preferred\";\n\n/**\n * Verbosity levels for content filtering.\n * - compact: Most concise output, main content only (default)\n * - standard: Balanced content with more detail\n * - full: Complete content including all sections\n */\nexport type VerbosityOptions = \"compact\" | \"standard\" | \"full\";\n\n/**\n * Section tags for semantic content filtering.\n */\nexport type SectionTag =\n | \"unspecified\"\n | \"header\"\n | \"navigation\"\n | \"banner\"\n | \"body\"\n | \"sidebar\"\n | \"footer\"\n | \"metadata\";\n\n/**\n * Options for retrieving text from page.\n * @typedef {Object} TextContentsOptions\n * @property {number} [maxCharacters] - The maximum number of characters to return.\n * @property {boolean} [includeHtmlTags] - If true, includes HTML tags in the returned text. Default: false\n * @property {VerbosityOptions} [verbosity] - Controls verbosity level of returned content. Default: \"compact\". Requires maxAgeHours: 0.\n * @property {SectionTag[]} [includeSections] - Only include content from these semantic sections. Requires maxAgeHours: 0.\n * @property {SectionTag[]} [excludeSections] - Exclude content from these semantic sections. Requires maxAgeHours: 0.\n */\nexport type TextContentsOptions = {\n maxCharacters?: number;\n includeHtmlTags?: boolean;\n verbosity?: VerbosityOptions;\n includeSections?: SectionTag[];\n excludeSections?: SectionTag[];\n};\n\n/**\n * Options for retrieving highlights from page.\n * Deep search variants also support these options for returned highlights.\n * @typedef {Object} HighlightsContentsOptions\n * @property {string} [query] - The query string to use for highlights search.\n * @property {number} [maxCharacters] - The maximum number of characters to return for highlights.\n * @property {number} [numSentences] - DEPRECATED: Use maxCharacters instead.\n * @property {number} [highlightsPerUrl] - DEPRECATED: Use maxCharacters instead.\n */\nexport type HighlightsContentsOptions = {\n query?: string;\n maxCharacters?: number;\n /**\n * DEPRECATED: Use `maxCharacters` instead. Will be removed in a future version.\n * @deprecated Use `maxCharacters` instead. This legacy sizing field will be removed in a future version.\n */\n numSentences?: number;\n /**\n * DEPRECATED: Use `maxCharacters` instead. Will be removed in a future version.\n * @deprecated Use `maxCharacters` instead. This legacy sizing field will be removed in a future version.\n */\n highlightsPerUrl?: number;\n};\n/**\n * Options for retrieving summary from page.\n * @typedef {Object} SummaryContentsOptions\n * @property {string} [query] - The query string to use for summary generation.\n * @property {Record<string, unknown> | ZodSchema} [schema] - JSON schema for structured output from summary.\n */\nexport type SummaryContentsOptions = {\n query?: string;\n schema?: Record<string, unknown> | ZodSchema;\n};\n\n/**\n * DEPRECATED: Use `Record<string, unknown>` instead. Will be removed in a future version.\n * @deprecated Use `Record<string, unknown>` instead.\n */\nexport type JSONSchema = Record<string, unknown>;\n\n/**\n * DEPRECATED: Use `text` or `highlights` instead. Will be removed in a future version.\n * @deprecated Use `text` or `highlights` instead. The `context` option will be removed in a future version.\n *\n * Options for retrieving the context from a list of search results. The context is a string\n * representation of all the search results.\n * @typedef {Object} ContextOptions\n * @property {number} [maxCharacters] - The maximum number of characters.\n */\nexport type ContextOptions = {\n maxCharacters?: number;\n};\n\n/**\n * @typedef {Object} TextResponse\n * @property {string} text - Text from page\n */\nexport type TextResponse = { text: string };\n\n/**\n * @typedef {Object} HighlightsResponse\n * @property {string[]} highlights - The highlights as an array of strings.\n * @property {number[]} [highlightScores] - The corresponding scores as an array of floats, 0 to 1\n */\nexport type HighlightsResponse = {\n highlights: string[];\n highlightScores?: number[];\n};\n\n/**\n * @typedef {Object} SummaryResponse\n * @property {string} summary - The generated summary of the page content.\n */\nexport type SummaryResponse = { summary: string };\n\n/**\n * @typedef {Object} ExtrasResponse\n * @property {string[]} links - The links on the page of a result\n * @property {string[]} imageLinks - The image links on the page of a result\n */\nexport type ExtrasResponse = {\n extras: { links?: string[]; imageLinks?: string[] };\n};\n\n/**\n * @typedef {Object} SubpagesResponse\n * @property {ContentsResultComponent<T extends ContentsOptions>} subpages - The subpages for a result\n */\nexport type SubpagesResponse<T extends ContentsOptions> = {\n subpages: ContentsResultComponent<T>[];\n};\n\nexport type Default<T extends {}, U> = [keyof T] extends [never] ? U : T;\n\n/**\n * @typedef {Object} ContentsResultComponent\n * Depending on 'ContentsOptions', this yields a combination of 'TextResponse', 'HighlightsResponse', 'SummaryResponse', or an empty object.\n *\n * @template T - A type extending from 'ContentsOptions'.\n */\nexport type ContentsResultComponent<T extends ContentsOptions> =\n (T[\"text\"] extends object | true ? TextResponse : {}) &\n (T[\"highlights\"] extends object | true ? HighlightsResponse : {}) &\n (T[\"summary\"] extends object | true ? SummaryResponse : {}) &\n (T[\"subpages\"] extends number ? SubpagesResponse<T> : {}) &\n (T[\"extras\"] extends object ? ExtrasResponse : {});\n\n/**\n * Represents the cost breakdown related to contents retrieval. Fields are optional because\n * only non-zero costs are included.\n * @typedef {Object} CostDollarsContents\n * @property {number} [text] - The cost in dollars for retrieving text.\n * @property {number} [highlights] - The cost in dollars for retrieving highlights.\n * @property {number} [summary] - The cost in dollars for retrieving summary.\n */\nexport type CostDollarsContents = {\n text?: number;\n highlights?: number;\n summary?: number;\n};\n\n/**\n * Represents the cost breakdown related to search. Fields are optional because\n * only non-zero costs are included.\n * @typedef {Object} CostDollarsSeearch\n * @property {number} [neural] - The cost in dollars for neural search.\n * @property {number} [keyword] - The cost in dollars for keyword search.\n */\nexport type CostDollarsSeearch = {\n neural?: number;\n keyword?: number;\n};\n\n/**\n * Represents the total cost breakdown. Only non-zero costs are included.\n * @typedef {Object} CostDollars\n * @property {number} total - The total cost in dollars.\n * @property {CostDollarsSeearch} [search] - The cost breakdown for search.\n * @property {CostDollarsContents} [contents] - The cost breakdown for contents.\n */\nexport type CostDollars = {\n total: number;\n search?: CostDollarsSeearch;\n contents?: CostDollarsContents;\n};\n\n/**\n * Entity types for company/people search results.\n * Only returned when using category=company or category=people searches.\n */\n\n/** Company workforce information. */\nexport type EntityCompanyPropertiesWorkforce = {\n total?: number | null;\n};\n\n/** Company headquarters information. */\nexport type EntityCompanyPropertiesHeadquarters = {\n address?: string | null;\n city?: string | null;\n postalCode?: string | null;\n country?: string | null;\n};\n\n/** Funding round information. */\nexport type EntityCompanyPropertiesFundingRound = {\n name?: string | null;\n date?: string | null;\n amount?: number | null;\n};\n\n/** Company financial information. */\nexport type EntityCompanyPropertiesFinancials = {\n revenueAnnual?: number | null;\n fundingTotal?: number | null;\n fundingLatestRound?: EntityCompanyPropertiesFundingRound | null;\n};\n\n/** Company web traffic information. */\nexport type EntityCompanyPropertiesWebTraffic = {\n visitsMonthly?: number | null;\n};\n\n/** Structured properties for a company entity. */\nexport type EntityCompanyProperties = {\n name?: string | null;\n foundedYear?: number | null;\n description?: string | null;\n workforce?: EntityCompanyPropertiesWorkforce | null;\n headquarters?: EntityCompanyPropertiesHeadquarters | null;\n financials?: EntityCompanyPropertiesFinancials | null;\n webTraffic?: EntityCompanyPropertiesWebTraffic | null;\n};\n\n/** Date range for work history entries. */\nexport type EntityDateRange = {\n from?: string | null;\n to?: string | null;\n};\n\n/** Reference to a company in work history. */\nexport type EntityPersonPropertiesCompanyRef = {\n id?: string | null;\n name?: string | null;\n};\n\n/** A single work history entry for a person. */\nexport type EntityPersonPropertiesWorkHistoryEntry = {\n title?: string | null;\n location?: string | null;\n dates?: EntityDateRange | null;\n company?: EntityPersonPropertiesCompanyRef | null;\n};\n\n/** Structured properties for a person entity. */\nexport type EntityPersonProperties = {\n name?: string | null;\n location?: string | null;\n workHistory?: EntityPersonPropertiesWorkHistoryEntry[];\n};\n\n/** Structured entity data for a company. */\nexport type CompanyEntity = {\n id: string;\n type: \"company\";\n version: number;\n properties: EntityCompanyProperties;\n};\n\n/** Structured entity data for a person. */\nexport type PersonEntity = {\n id: string;\n type: \"person\";\n version: number;\n properties: EntityPersonProperties;\n};\n\n/** Structured entity data for company or person search results. */\nexport type Entity = CompanyEntity | PersonEntity;\n\n/**\n * Represents a search result object.\n * @typedef {Object} SearchResult\n * @property {string} title - The title of the search result.\n * @property {string} url - The URL of the search result.\n * @property {string} [publishedDate] - The estimated creation date of the content.\n * @property {string} [author] - The author of the content, if available.\n * @property {number} [score] - Similarity score between the query/url and the result.\n * @property {string} id - The temporary ID for the document.\n * @property {string} [image] - A representative image for the content, if any.\n * @property {string} [favicon] - A favicon for the site, if any.\n * @property {Entity[]} [entities] - Structured entity data for company or person search results.\n */\nexport type SearchResult<T extends ContentsOptions> = {\n title: string | null;\n url: string;\n publishedDate?: string;\n author?: string;\n score?: number;\n id: string;\n image?: string;\n favicon?: string;\n entities?: Entity[];\n} & ContentsResultComponent<T>;\n\nexport type DeepSearchOutputGroundingCitation = {\n url: string;\n title: string;\n};\n\nexport type DeepSearchOutputGroundingConfidence = \"low\" | \"medium\" | \"high\";\n\nexport type DeepSearchOutputGrounding = {\n field: string;\n citations: DeepSearchOutputGroundingCitation[];\n confidence: DeepSearchOutputGroundingConfidence;\n};\n\nexport type DeepSearchOutput = {\n content: string | Record<string, unknown>;\n grounding: DeepSearchOutputGrounding[];\n};\n\n/**\n * Represents a search response object.\n * @typedef {Object} SearchResponse\n * @property {Result[]} results - The list of search results.\n * @property {string} [context] - DEPRECATED: Use `text` or `highlights` on individual results instead. Will be removed in a future version.\n * @property {DeepSearchOutput} [output] - Search synthesized output object returned when `outputSchema` is provided.\n * @property {string} [autoDate] - The autoprompt date, if applicable.\n * @property {string} requestId - The request ID for the search.\n * @property {CostDollars} [costDollars] - The cost breakdown for this request.\n * @property {string} [resolvedSearchType] - The resolved search type ('neural' or 'keyword') when using 'auto' search.\n * @property {number} [searchTime] - Time taken for the search in milliseconds.\n */\nexport type SearchResponse<T extends ContentsOptions> = {\n results: SearchResult<T>[];\n /**\n * DEPRECATED: Use `text` or `highlights` on individual results instead. Will be removed in a future version.\n * @deprecated Use `text` or `highlights` on individual results instead. This legacy `context` field will be removed in a future version.\n */\n context?: string;\n output?: DeepSearchOutput;\n autoDate?: string;\n requestId: string;\n statuses?: Array<Status>;\n costDollars?: CostDollars;\n resolvedSearchType?: string;\n searchTime?: number;\n};\n\nexport type Status = {\n id: string;\n status: string;\n source: string;\n};\n\n/**\n * Options for the answer endpoint\n * @typedef {Object} AnswerOptions\n * @property {boolean} [stream] - Whether to stream the response. Default false.\n * @property {boolean} [text] - Whether to include text in the source results. Default false.\n * @property {\"exa\"} [model] - The model to use for generating the answer. Default \"exa\".\n * @property {string} [systemPrompt] - A system prompt to guide the LLM's behavior when generating the answer.\n * @property {Object} [outputSchema] - A JSON Schema specification for the structure you expect the output to take\n */\nexport type AnswerOptions = {\n stream?: boolean;\n text?: boolean;\n model?: \"exa\";\n systemPrompt?: string;\n outputSchema?: Record<string, unknown>;\n userLocation?: string;\n};\n\n/**\n * Represents an answer response object from the /answer endpoint.\n * @typedef {Object} AnswerResponse\n * @property {string | Object} answer - The generated answer text (or an object matching `outputSchema`, if provided)\n * @property {SearchResult<{}>[]} citations - The sources used to generate the answer.\n * @property {CostDollars} [costDollars] - The cost breakdown for this request.\n * @property {string} [requestId] - Optional request ID for the answer.\n */\nexport type AnswerResponse = {\n answer: string | Record<string, unknown>;\n citations: SearchResult<{}>[];\n requestId?: string;\n costDollars?: CostDollars;\n};\n\nexport type AnswerStreamChunk = {\n /**\n * The partial text content of the answer (if present in this chunk).\n */\n content?: string;\n /**\n * Citations associated with the current chunk of text (if present).\n */\n citations?: Array<{\n id: string;\n url: string;\n title?: string;\n publishedDate?: string;\n author?: string;\n text?: string;\n }>;\n};\n\nexport type SearchStreamChunk = AnswerStreamChunk;\n\n/**\n * Represents a streaming answer response chunk from the /answer endpoint.\n * @typedef {Object} AnswerStreamResponse\n * @property {string} [answer] - A chunk of the generated answer text.\n * @property {SearchResult<{}>[]]} [citations] - The sources used to generate the answer.\n */\nexport type AnswerStreamResponse = {\n answer?: string;\n citations?: SearchResult<{}>[];\n};\n\n// ==========================================\n// Zod-Enhanced Types\n// ==========================================\n\n/**\n * Enhanced answer options that accepts either JSON schema or Zod schema\n */\nexport type AnswerOptionsTyped<T> = Omit<AnswerOptions, \"outputSchema\"> & {\n outputSchema: T;\n};\n\n/**\n * Enhanced answer response with strongly typed answer when using Zod\n */\nexport type AnswerResponseTyped<T> = Omit<AnswerResponse, \"answer\"> & {\n answer: T;\n};\n\n/**\n * Enhanced summary contents options that accepts either JSON schema or Zod schema\n */\nexport type SummaryContentsOptionsTyped<T> = Omit<\n SummaryContentsOptions,\n \"schema\"\n> & {\n schema: T;\n};\n\n/**\n * The Exa class encapsulates the API's endpoints.\n */\nexport class Exa {\n private baseURL: string;\n private headers: Headers;\n\n /**\n * Websets API client\n */\n websets: WebsetsClient;\n\n /**\n * Research API client\n */\n research: ResearchClient;\n\n /**\n * Search Monitors API client\n */\n monitors: SearchMonitorsClient;\n\n /**\n * Agent API client\n */\n agent: AgentClient;\n\n /**\n * Beta API clients\n */\n beta: BetaClient;\n\n /**\n * Helper method to separate out the contents-specific options from the rest.\n */\n private extractContentsOptions<T extends ContentsOptions>(\n options: T\n ): {\n contentsOptions: ContentsOptions;\n restOptions: Omit<T, keyof ContentsOptions>;\n } {\n const {\n text,\n highlights,\n summary,\n subpages,\n subpageTarget,\n extras,\n livecrawl,\n livecrawlTimeout,\n maxAgeHours,\n // DEPRECATED FIELD: preserve legacy `context` only for backward compatibility.\n context,\n ...rest\n } = options;\n\n const contentsOptions: ContentsOptions = {};\n\n // Default: if none of text, summary, or highlights is provided, we retrieve text\n if (\n text === undefined &&\n summary === undefined &&\n highlights === undefined &&\n extras === undefined\n ) {\n contentsOptions.text = true;\n }\n\n if (text !== undefined) contentsOptions.text = text;\n if (highlights !== undefined) contentsOptions.highlights = highlights;\n if (summary !== undefined) {\n // Handle zod schema conversion for summary\n if (\n typeof summary === \"object\" &&\n summary !== null &&\n \"schema\" in summary &&\n summary.schema &&\n isZodSchema(summary.schema)\n ) {\n contentsOptions.summary = {\n ...summary,\n schema: zodToJsonSchema(summary.schema),\n };\n } else {\n contentsOptions.summary = summary;\n }\n }\n if (subpages !== undefined) contentsOptions.subpages = subpages;\n if (subpageTarget !== undefined)\n contentsOptions.subpageTarget = subpageTarget;\n if (extras !== undefined) contentsOptions.extras = extras;\n if (livecrawl !== undefined) contentsOptions.livecrawl = livecrawl;\n if (livecrawlTimeout !== undefined)\n contentsOptions.livecrawlTimeout = livecrawlTimeout;\n if (maxAgeHours !== undefined) contentsOptions.maxAgeHours = maxAgeHours;\n // DEPRECATED FIELD: pass through only so existing callers do not break.\n if (context !== undefined) contentsOptions.context = context;\n\n return {\n contentsOptions,\n restOptions: rest as Omit<T, keyof ContentsOptions>,\n };\n }\n\n private buildSearchRequestBody(\n query: string,\n options?: RegularSearchOptions & {\n contents?: ContentsOptions | false | null | undefined;\n }\n ): Record<string, unknown> {\n const requestOptions = { ...(options ?? {}) } as Record<string, unknown>;\n delete requestOptions.stream;\n\n if (options === undefined || !(\"contents\" in options)) {\n return {\n query,\n ...requestOptions,\n contents: { text: { maxCharacters: DEFAULT_MAX_CHARACTERS } },\n };\n }\n\n if (\n options.contents === false ||\n options.contents === null ||\n options.contents === undefined\n ) {\n delete requestOptions.contents;\n return { query, ...requestOptions };\n }\n\n return { query, ...requestOptions };\n }\n\n /**\n * Constructs the Exa API client.\n * @param {string} apiKey - The API key for authentication.\n * @param {string} [baseURL] - The base URL of the Exa API.\n */\n constructor(apiKey?: string, baseURL: string = \"https://api.exa.ai\") {\n this.baseURL = baseURL;\n if (!apiKey) {\n apiKey = process.env.EXA_API_KEY;\n if (!apiKey) {\n throw new ExaError(\n \"API key must be provided as an argument or as an environment variable (EXA_API_KEY)\",\n HttpStatusCode.Unauthorized\n );\n }\n }\n this.headers = new HeadersImpl({\n \"x-api-key\": apiKey,\n \"Content-Type\": \"application/json\",\n \"User-Agent\": `exa-node ${packageJson.version}`,\n });\n\n // Initialize the Websets client\n this.websets = new WebsetsClient(this);\n // Initialize the Research client\n this.research = new ResearchClient(this);\n // Initialize the Search Monitors client\n this.monitors = new SearchMonitorsClient(this);\n // Initialize the Agent client\n this.agent = new AgentClient(this);\n // Initialize beta clients\n this.beta = new BetaClient(this.agent);\n }\n\n /**\n * Makes a request to the Exa API.\n * @param {string} endpoint - The API endpoint to call.\n * @param {string} method - The HTTP method to use.\n * @param {any} [body] - The request body for POST requests.\n * @param {Record<string, any>} [params] - The query parameters.\n * @returns {Promise<any>} The response from the API.\n * @throws {ExaError} When any API request fails with structured error information\n */\n async request<T = unknown>(\n endpoint: string,\n method: string,\n body?: any,\n params?: Record<string, any>,\n headers?: Record<string, string>\n ): Promise<T> {\n // Build URL with query parameters if provided\n let url = this.baseURL + endpoint;\n if (params && Object.keys(params).length > 0) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n searchParams.append(key, item);\n }\n } else if (value !== undefined) {\n searchParams.append(key, String(value));\n }\n }\n url += `?${searchParams.toString()}`;\n }\n\n let combinedHeaders: Record<string, string> = {};\n\n if (this.headers instanceof HeadersImpl) {\n this.headers.forEach((value, key) => {\n combinedHeaders[key] = value;\n });\n } else {\n combinedHeaders = { ...(this.headers as Record<string, string>) };\n }\n\n if (headers) {\n combinedHeaders = { ...combinedHeaders, ...headers };\n }\n\n const response = await fetchImpl(url, {\n method,\n headers: combinedHeaders,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n if (!response.ok) {\n const { json, text } = await readResponseBody(response);\n\n // Gateways/proxies can return non-JSON (e.g. HTML) error pages. Surface\n // the real status and a snippet of the body rather than throwing an\n // opaque JSON parse error that hides the underlying failure.\n if (!json || typeof json !== \"object\") {\n const snippet = truncateResponseBody(text);\n const message = snippet\n ? `Request failed with status ${response.status}. Response body was not valid JSON: ${snippet}`\n : `Request failed with status ${response.status}.`;\n throw new ExaError(\n message,\n response.status,\n new Date().toISOString(),\n endpoint\n );\n }\n\n const errorData = json;\n if (!errorData.statusCode) {\n errorData.statusCode = response.status;\n }\n if (!errorData.timestamp) {\n errorData.timestamp = new Date().toISOString();\n }\n if (!errorData.path) {\n errorData.path = endpoint;\n }\n\n // For other APIs, throw a simple ExaError with just message and status\n let message = errorData.error || \"Unknown error\";\n if (errorData.message) {\n message += (message.length > 0 ? \". \" : \"\") + errorData.message;\n }\n throw new ExaError(\n message,\n response.status,\n errorData.timestamp,\n errorData.path\n );\n }\n\n // If the server responded with an SSE stream, parse it and return the final payload.\n const contentType = response.headers.get(\"content-type\") || \"\";\n if (contentType.includes(\"text/event-stream\")) {\n return (await this.parseSSEStream<T>(response)) as T;\n }\n\n const { json, text } = await readResponseBody(response);\n\n // A successful status with a non-JSON body usually means a proxy returned\n // an error/redirect page; surface it instead of an opaque parse error.\n if (json === undefined && text) {\n throw new ExaError(\n `Expected a JSON response but received a non-JSON body (status ${response.status}): ${truncateResponseBody(text)}`,\n response.status,\n new Date().toISOString(),\n endpoint\n );\n }\n\n return json as T;\n }\n\n async rawRequest(\n endpoint: string,\n method: string = \"POST\",\n body?: Record<string, unknown>,\n queryParams?: Record<\n string,\n string | number | boolean | string[] | undefined\n >,\n headers?: Record<string, string>\n ): Promise<Response> {\n let url = this.baseURL + endpoint;\n\n if (queryParams) {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(queryParams)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n searchParams.append(key, String(item));\n }\n } else if (value !== undefined) {\n searchParams.append(key, String(value));\n }\n }\n url += `?${searchParams.toString()}`;\n }\n\n let combinedHeaders: Record<string, string> = {};\n\n if (this.headers instanceof HeadersImpl) {\n this.headers.forEach((value, key) => {\n combinedHeaders[key] = value;\n });\n } else {\n combinedHeaders = { ...(this.headers as Record<string, string>) };\n }\n\n if (headers) {\n combinedHeaders = { ...combinedHeaders, ...headers };\n }\n\n const response = await fetchImpl(url, {\n method,\n headers: combinedHeaders,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n return response;\n }\n\n /**\n * Performs a search with an Exa prompt-engineered query.\n * By default, returns text contents. Use contents: false to opt-out.\n *\n * @param {string} query - The query string.\n * @returns {Promise<SearchResponse<{ text: { maxCharacters: 10_000 } }>>} A list of relevant search results with text contents.\n */\n async search(\n query: string\n ): Promise<SearchResponse<{ text: { maxCharacters: 10_000 } }>>;\n /**\n * Performs a search without contents.\n *\n * @param {string} query - The query string.\n * @param {RegularSearchOptions & { contents: false }} options - Search options with contents explicitly disabled\n * @returns {Promise<SearchResponse<{}>>} A list of relevant search results without contents.\n */\n async search(\n query: string,\n options: RegularSearchOptions & { contents: false | null | undefined }\n ): Promise<SearchResponse<{}>>;\n /**\n * Performs a search with specific contents.\n *\n * @param {string} query - The query string.\n * @param {RegularSearchOptions & { contents: T }} options - Search options with specific contents\n * @returns {Promise<SearchResponse<T>>} A list of relevant search results with requested contents.\n */\n async search<T extends ContentsOptions>(\n query: string,\n options: RegularSearchOptions & { contents: T }\n ): Promise<SearchResponse<T>>;\n /**\n * Performs a search with an Exa prompt-engineered query.\n * When no contents option is specified, returns text contents by default.\n *\n * @param {string} query - The query string.\n * @param {Omit<DeepSearchOptions, 'contents'> | Omit<NonDeepSearchOptions, 'contents'>} options - Search options without contents\n * @returns {Promise<SearchResponse<{ text: true }>>} A list of relevant search results with text contents.\n */\n async search(\n query: string,\n options:\n | Omit<DeepSearchOptions, \"contents\">\n | Omit<NonDeepSearchOptions, \"contents\">\n ): Promise<SearchResponse<{ text: true }>>;\n async search<T extends ContentsOptions>(\n query: string,\n options?: RegularSearchOptions & { contents?: T | false | null | undefined }\n ): Promise<SearchResponse<T | { text: true } | {}>> {\n if (options?.stream) {\n throw new ExaError(\n \"For streaming responses, please use streamSearch() instead:\\n\\n\" +\n \"for await (const chunk of exa.streamSearch(query)) {\\n\" +\n \" // Handle chunks\\n\" +\n \"}\",\n HttpStatusCode.BadRequest\n );\n }\n\n return await this.request(\n \"/search\",\n \"POST\",\n this.buildSearchRequestBody(query, options)\n );\n }\n\n /**\n * Stream a search response as an async generator of OpenAI-style chat completion chunks.\n *\n * Each iteration yields a chunk with partial text (`content`) or new citations.\n * Use this if you'd like to read synthesized search output incrementally.\n */\n streamSearch(\n query: string,\n options?: RegularSearchOptions & {\n contents?: ContentsOptions | false | null | undefined;\n }\n ): AsyncGenerator<SearchStreamChunk> {\n return this.streamChatCompletions(\"/search\", {\n ...this.buildSearchRequestBody(query, options),\n stream: true,\n });\n }\n\n /**\n * DEPRECATED: Use `search()` instead. This legacy wrapper will be removed in a future version.\n * @deprecated Use `search()` instead. The search method now returns text contents by default.\n *\n * Migration examples:\n * - `searchAndContents(query)` → `search(query)`\n * - `searchAndContents(query, { text: true })` → `search(query, { contents: { text: true } })`\n * - `searchAndContents(query, { summary: true })` → `search(query, { contents: { summary: true } })`\n *\n * Compatibility wrapper for the legacy top-level contents options shape.\n *\n * @param {string} query - The query string.\n * @param {RegularSearchOptions & T} [options] - Additional search + contents options\n * @returns {Promise<SearchResponse<T>>} A list of relevant search results with requested contents.\n */\n async searchAndContents<T extends ContentsOptions>(\n query: string,\n options?: RegularSearchOptions & T\n ): Promise<SearchResponse<T>> {\n const { contentsOptions, restOptions } =\n options === undefined\n ? {\n contentsOptions: {\n text: { maxCharacters: DEFAULT_MAX_CHARACTERS },\n },\n restOptions: {},\n }\n : this.extractContentsOptions(options);\n\n return await this.request(\"/search\", \"POST\", {\n query,\n contents: contentsOptions,\n ...restOptions,\n });\n }\n\n /**\n * DEPRECATED: Use `search()` for new discovery flows. There is no direct replacement for URL-based similarity.\n * @deprecated Use `search()` for new discovery flows. This legacy URL-similarity method will be removed in a future version.\n *\n * Finds similar links to the provided URL.\n * By default, returns text contents. Use contents: false to opt-out.\n *\n * @param {string} url - The URL for which to find similar links.\n * @returns {Promise<SearchResponse<{ text: { maxCharacters: 10_000 } }>>} A list of similar search results with text contents.\n */\n async findSimilar(\n url: string\n ): Promise<SearchResponse<{ text: { maxCharacters: 10_000 } }>>;\n /**\n * DEPRECATED: Use `search()` for new discovery flows. There is no direct replacement for URL-based similarity.\n * @deprecated Use `search()` for new discovery flows. This legacy URL-similarity method will be removed in a future version.\n *\n * Finds similar links to the provided URL without contents.\n *\n * @param {string} url - The URL for which to find similar links.\n * @param {FindSimilarOptions & { contents: false }} options - Options with contents explicitly disabled\n * @returns {Promise<SearchResponse<{}>>} A list of similar search results without contents.\n */\n async findSimilar(\n url: string,\n options: FindSimilarOptions & { contents: false | null | undefined }\n ): Promise<SearchResponse<{}>>;\n /**\n * DEPRECATED: Use `search()` with `contents` for new discovery flows. There is no direct replacement for URL-based similarity.\n * @deprecated Use `search()` with `contents` for new discovery flows. This legacy URL-similarity method will be removed in a future version.\n *\n * Finds similar links to the provided URL with specific contents.\n *\n * @param {string} url - The URL for which to find similar links.\n * @param {FindSimilarOptions & { contents: T }} options - Options with specific contents\n * @returns {Promise<SearchResponse<T>>} A list of similar search results with requested contents.\n */\n async findSimilar<T extends ContentsOptions>(\n url: string,\n options: FindSimilarOptions & { contents: T }\n ): Promise<SearchResponse<T>>;\n /**\n * DEPRECATED: Use `search()` for new discovery flows. There is no direct replacement for URL-based similarity.\n * @deprecated Use `search()` for new discovery flows. This legacy URL-similarity method will be removed in a future version.\n *\n * Finds similar links to the provided URL.\n * When no contents option is specified, returns text contents by default.\n *\n * @param {string} url - The URL for which to find similar links.\n * @param {Omit<FindSimilarOptions, 'contents'>} options - Options without contents\n * @returns {Promise<SearchResponse<{ text: true }>>} A list of similar search results with text contents.\n */\n async findSimilar(\n url: string,\n options: Omit<FindSimilarOptions, \"contents\">\n ): Promise<SearchResponse<{ text: true }>>;\n async findSimilar<T extends ContentsOptions>(\n url: string,\n options?: FindSimilarOptions & { contents?: T | false | null | undefined }\n ): Promise<SearchResponse<T | { text: { maxCharacters: 10_000 } } | {}>> {\n // DEPRECATED METHOD: preserve legacy URL-similarity endpoint for compatibility.\n if (options === undefined || !(\"contents\" in options)) {\n // No options or no contents property → default to text contents\n return await this.request(\"/findSimilar\", \"POST\", {\n url,\n ...options,\n contents: { text: { maxCharacters: DEFAULT_MAX_CHARACTERS } },\n });\n }\n\n // If contents is false, null, or undefined, don't send it to the API\n if (\n options.contents === false ||\n options.contents === null ||\n options.contents === undefined\n ) {\n const { contents, ...restOptions } = options;\n return await this.request(\"/findSimilar\", \"POST\", {\n url,\n ...restOptions,\n });\n }\n\n // Contents property exists with value - pass it through\n return await this.request(\"/findSimilar\", \"POST\", { url, ...options });\n }\n\n /**\n * DEPRECATED: Use `search()` for new discovery flows instead. This legacy wrapper will be removed in a future version.\n * @deprecated Use `search()` for new discovery flows instead. There is no direct replacement for URL-based similarity.\n *\n * Migration examples:\n * - `findSimilarAndContents(url)` → `search(query)`\n * - `findSimilarAndContents(url, { text: true })` → `search(query, { contents: { text: true } })`\n * - `findSimilarAndContents(url, { summary: true })` → `search(query, { contents: { summary: true } })`\n *\n * Compatibility wrapper for the legacy top-level contents options shape.\n * @param {string} url - The URL for which to find similar links.\n * @param {FindSimilarOptions & T} [options] - Additional options for finding similar links + contents.\n * @returns {Promise<SearchResponse<T>>} A list of similar search results, including requested contents.\n */\n async findSimilarAndContents<T extends ContentsOptions>(\n url: string,\n options?: FindSimilarOptions & T\n ): Promise<SearchResponse<T>> {\n const { contentsOptions, restOptions } =\n options === undefined\n ? {\n contentsOptions: {\n text: { maxCharacters: DEFAULT_MAX_CHARACTERS },\n },\n restOptions: {},\n }\n : this.extractContentsOptions(options);\n\n return await this.request(\"/findSimilar\", \"POST\", {\n url,\n contents: contentsOptions,\n ...restOptions,\n });\n }\n\n /**\n * Retrieves contents of documents based on URLs.\n * @param {string | string[] | SearchResult[]} urls - A URL or array of URLs, or an array of SearchResult objects.\n * @param {ContentsOptions} [options] - Additional options for retrieving document contents.\n * @returns {Promise<SearchResponse<T>>} A list of document contents for the requested URLs.\n */\n async getContents<T extends ContentsOptions>(\n urls: string | string[] | SearchResult<T>[],\n options?: T\n ): Promise<SearchResponse<T>> {\n if (!urls || (Array.isArray(urls) && urls.length === 0)) {\n throw new ExaError(\n \"Must provide at least one URL\",\n HttpStatusCode.BadRequest\n );\n }\n\n let requestUrls: string[];\n\n if (typeof urls === \"string\") {\n requestUrls = [urls];\n } else if (typeof urls[0] === \"string\") {\n requestUrls = urls as string[];\n } else {\n requestUrls = (urls as SearchResult<T>[]).map((result) => result.url);\n }\n\n const payload = {\n urls: requestUrls,\n ...options,\n };\n\n return await this.request(\"/contents\", \"POST\", payload);\n }\n\n /**\n * Generate an answer with Zod schema for strongly typed output\n */\n async answer<T>(\n query: string,\n options: AnswerOptionsTyped<ZodSchema<T>>\n ): Promise<AnswerResponseTyped<T>>;\n\n /**\n * Generate an answer to a query.\n * @param {string} query - The question or query to answer.\n * @param {AnswerOptions} [options] - Additional options for answer generation.\n * @returns {Promise<AnswerResponse>} The generated answer and source references.\n *\n * Example with systemPrompt:\n * ```ts\n * const answer = await exa.answer(\"What is quantum computing?\", {\n * text: true,\n * model: \"exa\",\n * systemPrompt: \"Answer in a technical manner suitable for experts.\"\n * });\n * ```\n *\n * Note: For streaming responses, use the `streamAnswer` method:\n * ```ts\n * for await (const chunk of exa.streamAnswer(query)) {\n * // Handle chunks\n * }\n * ```\n */\n async answer(query: string, options?: AnswerOptions): Promise<AnswerResponse>;\n\n async answer<T>(\n query: string,\n options?: AnswerOptions | AnswerOptionsTyped<ZodSchema<T>>\n ): Promise<AnswerResponse | AnswerResponseTyped<T>> {\n if (options?.stream) {\n throw new ExaError(\n \"For streaming responses, please use streamAnswer() instead:\\n\\n\" +\n \"for await (const chunk of exa.streamAnswer(query)) {\\n\" +\n \" // Handle chunks\\n\" +\n \"}\",\n HttpStatusCode.BadRequest\n );\n