Skip to main content

exa_async/types/
common.rs

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