Skip to main content

exa_async/types/
common.rs

1//! Shared types used across Exa API endpoints
2
3use serde::{Deserialize, Serialize};
4
5/// Search type for Exa queries
6#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum SearchType {
9    /// Automatic selection
10    Auto,
11    /// Neural/semantic search (default)
12    #[default]
13    Neural,
14    /// Keyword-based search
15    Keyword,
16    /// Hybrid neural + keyword
17    Hybrid,
18}
19
20/// Livecrawl option for content retrieval
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22#[serde(rename_all = "lowercase")]
23pub enum LivecrawlOption {
24    /// Always livecrawl
25    Always,
26    /// Livecrawl if needed (fallback)
27    Fallback,
28    /// Never livecrawl
29    Never,
30    /// Automatically decide
31    Auto,
32}
33
34/// Options for what content to retrieve with search results
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct ContentsOptions {
38    /// Include full text content
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub text: Option<TextContentsOptions>,
41    /// Include highlights/snippets
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub highlights: Option<HighlightsContentsOptions>,
44    /// Include summary
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub summary: Option<SummaryContentsOptions>,
47}
48
49/// Options for text content retrieval
50#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct TextContentsOptions {
53    /// Maximum number of characters to return
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub max_characters: Option<u32>,
56    /// Include HTML tags
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub include_html_tags: Option<bool>,
59}
60
61/// Options for highlight content retrieval
62#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct HighlightsContentsOptions {
65    /// Number of sentences per highlight
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub num_sentences: Option<u32>,
68    /// Number of highlights per URL
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub highlights_per_url: Option<u32>,
71    /// Query for highlights
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub query: Option<String>,
74}
75
76/// Options for summary content retrieval
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct SummaryContentsOptions {
80    /// Custom query for summary generation
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub query: Option<String>,
83}
84
85/// A single search result from the Exa API
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct SearchResult {
89    /// URL of the result
90    pub url: String,
91    /// Unique ID for the result
92    #[serde(default)]
93    pub id: Option<String>,
94    /// Title of the page
95    #[serde(default)]
96    pub title: Option<String>,
97    /// Relevance score
98    #[serde(default)]
99    pub score: Option<f64>,
100    /// Date the page was published
101    #[serde(default)]
102    pub published_date: Option<String>,
103    /// Author of the page
104    #[serde(default)]
105    pub author: Option<String>,
106    /// Full text content (if requested)
107    #[serde(default)]
108    pub text: Option<String>,
109    /// Summary (if requested)
110    #[serde(default)]
111    pub summary: Option<String>,
112    /// Highlights (if requested)
113    #[serde(default)]
114    pub highlights: Option<Vec<String>>,
115    /// Highlight scores
116    #[serde(default)]
117    pub highlight_scores: Option<Vec<f64>>,
118}
119
120/// Represents the cost breakdown related to search.
121/// Fields are optional because only non-zero costs are included by the API.
122#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct CostDollarsSearch {
125    /// The cost in dollars for neural search.
126    #[serde(default)]
127    pub neural: Option<f64>,
128    /// The cost in dollars for keyword search.
129    #[serde(default)]
130    pub keyword: Option<f64>,
131}
132
133/// Represents the cost breakdown related to contents retrieval.
134/// Fields are optional because only non-zero costs are included by the API.
135#[derive(Debug, Clone, Default, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct CostDollarsContents {
138    /// The cost in dollars for retrieving text.
139    #[serde(default)]
140    pub text: Option<f64>,
141    /// The cost in dollars for retrieving highlights.
142    #[serde(default)]
143    pub highlights: Option<f64>,
144    /// The cost in dollars for retrieving summary.
145    #[serde(default)]
146    pub summary: Option<f64>,
147}
148
149/// Represents the total cost breakdown for a request.
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct CostDollars {
153    /// Total cost
154    #[serde(default)]
155    pub total: Option<f64>,
156    /// Search cost component (nested breakdown)
157    #[serde(default)]
158    pub search: Option<CostDollarsSearch>,
159    /// Contents cost component (nested breakdown)
160    #[serde(default)]
161    pub contents: Option<CostDollarsContents>,
162}
163
164#[cfg(test)]
165mod cost_dollars_tests {
166    use super::*;
167    use serde_json::json;
168
169    #[test]
170    fn deserializes_full_breakdown() {
171        let v = json!({
172            "total": 0.005,
173            "search": { "neural": 0.003 },
174            "contents": { "text": 0.001, "highlights": 0.0005, "summary": 0.0005 }
175        });
176
177        let cost: CostDollars = serde_json::from_value(v).unwrap();
178
179        assert!((cost.total.unwrap() - 0.005).abs() < 1e-12);
180        assert!((cost.search.as_ref().unwrap().neural.unwrap() - 0.003).abs() < 1e-12);
181        assert!(cost.search.as_ref().unwrap().keyword.is_none());
182        assert!((cost.contents.as_ref().unwrap().text.unwrap() - 0.001).abs() < 1e-12);
183        assert!((cost.contents.as_ref().unwrap().highlights.unwrap() - 0.0005).abs() < 1e-12);
184        assert!((cost.contents.as_ref().unwrap().summary.unwrap() - 0.0005).abs() < 1e-12);
185    }
186
187    #[test]
188    fn deserializes_with_missing_optional_fields() {
189        let v = json!({
190            "total": 0.003,
191            "search": { "neural": 0.003 }
192        });
193
194        let cost: CostDollars = serde_json::from_value(v).unwrap();
195
196        assert!(cost.contents.is_none());
197        let search = cost.search.unwrap();
198        assert!((search.neural.unwrap() - 0.003).abs() < 1e-12);
199        assert!(search.keyword.is_none());
200    }
201
202    #[test]
203    fn deserializes_with_empty_nested_objects() {
204        let v = json!({
205            "total": 0.005,
206            "search": {},
207            "contents": {}
208        });
209
210        let cost: CostDollars = serde_json::from_value(v).unwrap();
211
212        assert!(cost.search.is_some());
213        assert!(cost.search.as_ref().unwrap().neural.is_none());
214        assert!(cost.contents.is_some());
215        assert!(cost.contents.as_ref().unwrap().text.is_none());
216        assert!(cost.contents.as_ref().unwrap().highlights.is_none());
217        assert!(cost.contents.as_ref().unwrap().summary.is_none());
218    }
219
220    #[test]
221    fn deserializes_with_null_nested_fields() {
222        let v = json!({
223            "total": 0.005,
224            "search": null,
225            "contents": { "text": null, "highlights": 0.0005 }
226        });
227
228        let cost: CostDollars = serde_json::from_value(v).unwrap();
229
230        assert!(cost.search.is_none());
231
232        let contents = cost.contents.unwrap();
233        assert!(contents.text.is_none());
234        assert!((contents.highlights.unwrap() - 0.0005).abs() < 1e-12);
235        assert!(contents.summary.is_none());
236    }
237
238    #[test]
239    fn ignores_unknown_fields_for_forward_compatibility() {
240        let v = json!({
241            "total": 0.005,
242            "search": { "neural": 0.003, "fast": 0.001 },
243            "contents": { "text": 0.002, "images": 0.123 },
244            "someFutureTopLevelField": "ignored"
245        });
246
247        let cost: CostDollars = serde_json::from_value(v).unwrap();
248
249        assert!((cost.search.as_ref().unwrap().neural.unwrap() - 0.003).abs() < 1e-12);
250        assert!(cost.search.as_ref().unwrap().keyword.is_none());
251        assert!((cost.contents.as_ref().unwrap().text.unwrap() - 0.002).abs() < 1e-12);
252    }
253}