Skip to main content

openai_compat/types/responses/
request.rs

1//! `CreateResponseRequest`, mirroring the `create` parameter list in
2//! `resources/responses/responses.py`.
3
4use std::collections::HashMap;
5
6use serde::Serialize;
7
8use super::config::{Reasoning, ResponseIncludable, ResponseTextConfig, Truncation};
9use super::input::Input;
10use super::tools::{Tool, ToolChoice};
11
12/// Request body for `POST /responses`.
13#[derive(Debug, Clone, Default, Serialize)]
14pub struct CreateResponseRequest {
15    pub model: String,
16    /// Omitted from the request body when empty (e.g. a continuation request
17    /// driven purely by `previous_response_id`) rather than sent as `null`.
18    #[serde(skip_serializing_if = "Input::is_empty")]
19    pub input: Input,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub instructions: Option<String>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub max_output_tokens: Option<u64>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub max_tool_calls: Option<u64>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub metadata: Option<HashMap<String, String>>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub previous_response_id: Option<String>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub reasoning: Option<Reasoning>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub store: Option<bool>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub stream: Option<bool>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub temperature: Option<f64>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub text: Option<ResponseTextConfig>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub tool_choice: Option<ToolChoice>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub tools: Option<Vec<Tool>>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub top_p: Option<f64>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub truncation: Option<Truncation>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub include: Option<Vec<ResponseIncludable>>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub background: Option<bool>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub parallel_tool_calls: Option<bool>,
54    /// Escape hatch: additional request parameters flattened into the JSON
55    /// body, for provider-specific or newly-added params not yet typed.
56    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
57    pub extra: HashMap<String, serde_json::Value>,
58}
59
60impl CreateResponseRequest {
61    pub fn new(model: impl Into<String>, input: impl Into<Input>) -> Self {
62        Self {
63            model: model.into(),
64            input: input.into(),
65            ..Self::default()
66        }
67    }
68
69    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
70        self.instructions = Some(instructions.into());
71        self
72    }
73
74    pub fn max_output_tokens(mut self, max: u64) -> Self {
75        self.max_output_tokens = Some(max);
76        self
77    }
78
79    pub fn max_tool_calls(mut self, max: u64) -> Self {
80        self.max_tool_calls = Some(max);
81        self
82    }
83
84    pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
85        self.metadata = Some(metadata);
86        self
87    }
88
89    /// Chain this request onto a prior response, for stateful multi-turn
90    /// tool-calling loops.
91    pub fn previous_response_id(mut self, id: impl Into<String>) -> Self {
92        self.previous_response_id = Some(id.into());
93        self
94    }
95
96    pub fn reasoning(mut self, reasoning: Reasoning) -> Self {
97        self.reasoning = Some(reasoning);
98        self
99    }
100
101    pub fn store(mut self, store: bool) -> Self {
102        self.store = Some(store);
103        self
104    }
105
106    pub fn temperature(mut self, temperature: f64) -> Self {
107        self.temperature = Some(temperature);
108        self
109    }
110
111    pub fn text(mut self, text: ResponseTextConfig) -> Self {
112        self.text = Some(text);
113        self
114    }
115
116    pub fn tool_choice(mut self, tool_choice: ToolChoice) -> Self {
117        self.tool_choice = Some(tool_choice);
118        self
119    }
120
121    pub fn tools(mut self, tools: Vec<Tool>) -> Self {
122        self.tools = Some(tools);
123        self
124    }
125
126    pub fn top_p(mut self, top_p: f64) -> Self {
127        self.top_p = Some(top_p);
128        self
129    }
130
131    pub fn truncation(mut self, truncation: Truncation) -> Self {
132        self.truncation = Some(truncation);
133        self
134    }
135
136    pub fn include(mut self, include: Vec<ResponseIncludable>) -> Self {
137        self.include = Some(include);
138        self
139    }
140
141    /// Run this response asynchronously; poll or stream-resume via
142    /// `retrieve`/`retrieve_stream`.
143    pub fn background(mut self, background: bool) -> Self {
144        self.background = Some(background);
145        self
146    }
147
148    pub fn parallel_tool_calls(mut self, parallel: bool) -> Self {
149        self.parallel_tool_calls = Some(parallel);
150        self
151    }
152
153    /// Set an arbitrary extra parameter on the request body (escape hatch
154    /// for provider-specific or newly-added params). Do not use this to set
155    /// `stream`: `extra` is flattened after the typed fields, so a
156    /// `param("stream", ...)` value serializes last and silently overrides
157    /// whatever `create`/`create_stream` set on the typed `stream` field.
158    pub fn param(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
159        self.extra.insert(key.into(), value.into());
160        self
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::super::input::InputItem;
167    use super::*;
168
169    #[test]
170    fn request_skips_none_fields() {
171        let request = CreateResponseRequest::new("gpt-5", "hi");
172        let json = serde_json::to_value(&request).unwrap();
173        assert_eq!(json, serde_json::json!({"model": "gpt-5", "input": "hi"}));
174    }
175
176    #[test]
177    fn request_with_items_input_serializes() {
178        let request =
179            CreateResponseRequest::new("gpt-5", vec![InputItem::user("hi")]).temperature(0.5);
180        let json = serde_json::to_value(&request).unwrap();
181        assert_eq!(json["model"], "gpt-5");
182        assert_eq!(json["temperature"], 0.5);
183        assert_eq!(json["input"][0]["type"], "message");
184    }
185
186    #[test]
187    fn continuation_request_omits_input_entirely() {
188        // A follow-up request driven purely by `previous_response_id` has no
189        // `input` to send; it must be omitted, not serialized as `null`.
190        let request = CreateResponseRequest {
191            model: "gpt-5".to_string(),
192            ..Default::default()
193        }
194        .previous_response_id("resp_1");
195        let json = serde_json::to_value(&request).unwrap();
196        assert_eq!(
197            json,
198            serde_json::json!({"model": "gpt-5", "previous_response_id": "resp_1"})
199        );
200    }
201}