Skip to main content

x_ai/
responses.rs

1// Copyright 2026 Mahmoud Harmouch.
2//
3// Licensed under the MIT license
4// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
5// option. This file may not be copied, modified, or distributed
6// except according to those terms.
7
8//! Reference: [docs/api-reference#responses](https://docs.x.ai/docs/api-reference#responses)
9//!
10//! Implements:
11//! - `POST /v1/responses` - create a response
12//! - `GET  /v1/responses/{response_id}` - retrieve a stored response
13//! - `DELETE /v1/responses/{response_id}` - delete a stored response
14
15use crate::chat_compl::SearchParameters;
16use crate::error::XaiError;
17use crate::error::check_for_model_error;
18use crate::traits::{ClientConfig, ResponsesFetcher};
19use reqwest::Method;
20use serde::{Deserialize, Serialize};
21
22/// Input to a `/v1/responses` request - either a plain string or a structured
23/// list of message objects.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(untagged)]
26pub enum ResponseInput {
27    Text(String),
28    Messages(Vec<serde_json::Value>),
29}
30
31/// Reasoning configuration for thinking models.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ReasoningConfig {
34    /// `"low"` | `"medium"` | `"high"`
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub effort: Option<String>,
37    /// Compatibility field - ignored by the server.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub generate_summary: Option<String>,
40    /// Compatibility field - `"auto"` | `"concise"` | `"detailed"`.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub summary: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct CreateResponseRequest {
47    pub model: String,
48    pub input: ResponseInput,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub instructions: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub previous_response_id: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub store: Option<bool>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub stream: Option<bool>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub temperature: Option<f32>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub top_p: Option<f32>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub max_output_tokens: Option<u32>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub max_turns: Option<u32>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub reasoning: Option<ReasoningConfig>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub search_parameters: Option<SearchParameters>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub tools: Option<Vec<serde_json::Value>>,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub tool_choice: Option<serde_json::Value>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub parallel_tool_calls: Option<bool>,
75    /// Output text format configuration.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub text: Option<serde_json::Value>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub logprobs: Option<bool>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub top_logprobs: Option<u32>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub user: Option<String>,
84    /// Not executed yet - parsed for compatibility.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub context_management: Option<Vec<serde_json::Value>>,
87    /// Which additional data to include (`"reasoning.encrypted_content"`).
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub include: Option<Vec<String>>,
90    /// Compatibility only - not supported.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub service_tier: Option<String>,
93    /// Compatibility only - not supported.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub truncation: Option<String>,
96    /// Compatibility only - not supported.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub background: Option<bool>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub prompt_cache_key: Option<String>,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub metadata: Option<serde_json::Value>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ResponseInputTokensDetails {
107    pub cached_tokens: u32,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ResponseOutputTokensDetails {
112    pub reasoning_tokens: u32,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ServerSideToolUsageDetails {
117    pub code_interpreter_calls: u32,
118    pub document_search_calls: u32,
119    pub file_search_calls: u32,
120    pub mcp_calls: u32,
121    pub web_search_calls: u32,
122    pub x_search_calls: u32,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ResponseUsage {
127    pub input_tokens: u32,
128    pub output_tokens: u32,
129    pub total_tokens: u32,
130    pub input_tokens_details: ResponseInputTokensDetails,
131    pub output_tokens_details: ResponseOutputTokensDetails,
132    pub num_sources_used: u32,
133    pub num_server_side_tools_used: u32,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub cost_in_usd_ticks: Option<i64>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub cost_in_nano_usd: Option<i64>,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub server_side_tool_usage_details: Option<ServerSideToolUsageDetails>,
140}
141
142/// A response object returned by POST/GET `/v1/responses`.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct ResponseObject {
145    pub id: String,
146    pub object: String,
147    pub model: String,
148    /// `"completed"` | `"in_progress"` | `"incomplete"`
149    pub status: String,
150    pub created_at: u64,
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub completed_at: Option<u64>,
153    /// The generated output items (messages, reasoning blocks, tool calls, etc.)
154    pub output: Vec<serde_json::Value>,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub usage: Option<ResponseUsage>,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub instructions: Option<String>,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub previous_response_id: Option<String>,
161    pub store: bool,
162    pub parallel_tool_calls: bool,
163    pub tool_choice: serde_json::Value,
164    pub tools: Vec<serde_json::Value>,
165    pub text: serde_json::Value,
166    pub truncation: String,
167    pub service_tier: String,
168    pub top_logprobs: u32,
169    pub frequency_penalty: f64,
170    pub presence_penalty: f64,
171    pub metadata: serde_json::Value,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub temperature: Option<f32>,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub top_p: Option<f32>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub max_output_tokens: Option<u32>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub reasoning: Option<ReasoningConfig>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub user: Option<String>,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub safety_identifier: Option<String>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub prompt_cache_key: Option<String>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub incomplete_details: Option<serde_json::Value>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub error: Option<serde_json::Value>,
190    pub background: bool,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub max_tool_calls: Option<u32>,
193}
194
195/// Response body for `DELETE /v1/responses/{response_id}`.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct DeleteResponseObject {
198    pub id: String,
199    pub object: String,
200    pub deleted: bool,
201}
202
203#[derive(Debug, Clone)]
204pub struct ResponsesRequestBuilder<T: ClientConfig + Clone + Send + Sync> {
205    client: T,
206    request: CreateResponseRequest,
207}
208
209impl<T> ResponsesRequestBuilder<T>
210where
211    T: ClientConfig + Clone + Send + Sync,
212{
213    pub fn new(client: T, model: impl Into<String>, input: ResponseInput) -> Self {
214        Self {
215            client,
216            request: CreateResponseRequest {
217                model: model.into(),
218                input,
219                instructions: None,
220                previous_response_id: None,
221                store: None,
222                stream: None,
223                temperature: None,
224                top_p: None,
225                max_output_tokens: None,
226                max_turns: None,
227                reasoning: None,
228                search_parameters: None,
229                tools: None,
230                tool_choice: None,
231                parallel_tool_calls: None,
232                text: None,
233                logprobs: None,
234                top_logprobs: None,
235                user: None,
236                context_management: None,
237                include: None,
238                service_tier: None,
239                truncation: None,
240                background: None,
241                prompt_cache_key: None,
242                metadata: None,
243            },
244        }
245    }
246
247    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
248        self.request.instructions = Some(instructions.into());
249        self
250    }
251
252    pub fn previous_response_id(mut self, id: impl Into<String>) -> Self {
253        self.request.previous_response_id = Some(id.into());
254        self
255    }
256
257    pub fn store(mut self, store: bool) -> Self {
258        self.request.store = Some(store);
259        self
260    }
261
262    pub fn stream(mut self, stream: bool) -> Self {
263        self.request.stream = Some(stream);
264        self
265    }
266
267    pub fn temperature(mut self, temperature: f32) -> Self {
268        self.request.temperature = Some(temperature);
269        self
270    }
271
272    pub fn top_p(mut self, top_p: f32) -> Self {
273        self.request.top_p = Some(top_p);
274        self
275    }
276
277    pub fn max_output_tokens(mut self, max_output_tokens: u32) -> Self {
278        self.request.max_output_tokens = Some(max_output_tokens);
279        self
280    }
281
282    pub fn max_turns(mut self, max_turns: u32) -> Self {
283        self.request.max_turns = Some(max_turns);
284        self
285    }
286
287    pub fn reasoning(mut self, reasoning: ReasoningConfig) -> Self {
288        self.request.reasoning = Some(reasoning);
289        self
290    }
291
292    pub fn search_parameters(mut self, search_parameters: SearchParameters) -> Self {
293        self.request.search_parameters = Some(search_parameters);
294        self
295    }
296
297    pub fn tools(mut self, tools: Vec<serde_json::Value>) -> Self {
298        self.request.tools = Some(tools);
299        self
300    }
301
302    pub fn tool_choice(mut self, tool_choice: serde_json::Value) -> Self {
303        self.request.tool_choice = Some(tool_choice);
304        self
305    }
306
307    pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
308        self.request.parallel_tool_calls = Some(parallel_tool_calls);
309        self
310    }
311
312    pub fn logprobs(mut self, logprobs: bool) -> Self {
313        self.request.logprobs = Some(logprobs);
314        self
315    }
316
317    pub fn top_logprobs(mut self, top_logprobs: u32) -> Self {
318        self.request.top_logprobs = Some(top_logprobs);
319        self
320    }
321
322    pub fn user(mut self, user: impl Into<String>) -> Self {
323        self.request.user = Some(user.into());
324        self
325    }
326
327    pub fn include(mut self, include: Vec<String>) -> Self {
328        self.request.include = Some(include);
329        self
330    }
331
332    pub fn prompt_cache_key(mut self, key: impl Into<String>) -> Self {
333        self.request.prompt_cache_key = Some(key.into());
334        self
335    }
336
337    pub fn build(self) -> Result<CreateResponseRequest, XaiError> {
338        if self.request.model.trim().is_empty() {
339            return Err(XaiError::Validation("model cannot be empty".to_string()));
340        }
341        Ok(self.request)
342    }
343}
344
345impl<T> ResponsesFetcher for ResponsesRequestBuilder<T>
346where
347    T: ClientConfig + Clone + Send + Sync,
348{
349    async fn create_response(
350        &self,
351        request: CreateResponseRequest,
352    ) -> Result<ResponseObject, XaiError> {
353        let response = self
354            .client
355            .request(Method::POST, "responses")?
356            .json(&request)
357            .send()
358            .await?;
359
360        if response.status().is_success() {
361            let obj = response.json::<ResponseObject>().await?;
362            Ok(obj)
363        } else {
364            let error_body = response.text().await.unwrap_or_else(|_| "".to_string());
365            if let Some(model_error) = check_for_model_error(&error_body) {
366                return Err(model_error);
367            }
368            Err(XaiError::Http(error_body))
369        }
370    }
371
372    async fn get_response(&self, response_id: &str) -> Result<ResponseObject, XaiError> {
373        let url = format!("responses/{}", response_id);
374        let response = self.client.request(Method::GET, &url)?.send().await?;
375
376        if response.status().is_success() {
377            let obj = response.json::<ResponseObject>().await?;
378            Ok(obj)
379        } else {
380            let error_body = response.text().await.unwrap_or_else(|_| "".to_string());
381            if let Some(model_error) = check_for_model_error(&error_body) {
382                return Err(model_error);
383            }
384            Err(XaiError::Http(error_body))
385        }
386    }
387
388    async fn delete_response(&self, response_id: &str) -> Result<DeleteResponseObject, XaiError> {
389        let url = format!("responses/{}", response_id);
390        let response = self.client.request(Method::DELETE, &url)?.send().await?;
391
392        if response.status().is_success() {
393            let obj = response.json::<DeleteResponseObject>().await?;
394            Ok(obj)
395        } else {
396            let error_body = response.text().await.unwrap_or_else(|_| "".to_string());
397            if let Some(model_error) = check_for_model_error(&error_body) {
398                return Err(model_error);
399            }
400            Err(XaiError::Http(error_body))
401        }
402    }
403}
404// Copyright 2026 Mahmoud Harmouch.
405//
406// Licensed under the MIT license
407// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
408// option. This file may not be copied, modified, or distributed
409// except according to those terms.