Skip to main content

vtcode_core/open_responses/
request.rs

1//! Request object for Open Responses.
2//!
3//! The Request is the top-level object sent to the API,
4//! containing input items, tool definitions, and model parameters.
5
6use serde::{Deserialize, Deserializer, Serialize};
7use serde_json::Value;
8
9use super::{MessageRole, OutputItem};
10use crate::llm::provider::ToolDefinition;
11
12/// The main request object per the Open Responses specification.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct Request {
15    /// The model to use for the request.
16    pub model: String,
17
18    /// The input items that form the context for the model.
19    /// Per the spec, these are polymorphic items (messages, tool outputs, etc.).
20    pub input: Vec<OutputItem>,
21
22    /// Tools available to the model.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub tools: Option<Vec<ToolDefinition>>,
25
26    /// Tool choice parameter.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub tool_choice: Option<ToolChoice>,
29
30    /// Whether to stream the response.
31    #[serde(default)]
32    pub stream: bool,
33
34    /// Sampling temperature.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub temperature: Option<f64>,
37
38    /// Nucleus sampling parameter.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub top_p: Option<f64>,
41
42    /// Truncation configuration.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub truncation: Option<Box<TruncationConfig>>,
45
46    /// Maximum output tokens allowed.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub max_output_tokens: Option<u64>,
49
50    /// Maximum tool calls allowed in a single request.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub max_tool_calls: Option<u64>,
53
54    /// Stop sequences for the model.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub stop: Option<Vec<String>>,
57
58    /// Presence penalty.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub presence_penalty: Option<f64>,
61
62    /// Frequency penalty.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub frequency_penalty: Option<f64>,
65
66    /// Logit bias for token sampling.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub logit_bias: Option<hashbrown::HashMap<String, f64>>,
69
70    /// Whether to return log probabilities.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub logprobs: Option<bool>,
73
74    /// Number of top log probabilities to return.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub top_logprobs: Option<u32>,
77
78    /// User ID for tracking and rate limiting.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub user: Option<String>,
81
82    /// Service tier requested.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub service_tier: Option<String>,
85
86    /// Reasoning configuration.
87    #[serde(
88        default,
89        skip_serializing_if = "Option::is_none",
90        deserialize_with = "deserialize_boxed_reasoning_config_opt"
91    )]
92    pub reasoning: Option<Box<ReasoningConfig>>,
93
94    /// Whether to store the request/response.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub store: Option<bool>,
97
98    /// Optional ID of the previous response for server-side continuity.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub previous_response_id: Option<String>,
101
102    /// Optional response fields to include.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub include: Option<Vec<String>>,
105
106    /// Metadata for the request.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub metadata: Option<Value>,
109}
110
111/// Reasoning configuration for the request.
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct ReasoningConfig {
114    /// Reasoning effort level (low, medium, high).
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub effort: Option<String>,
117}
118
119impl ReasoningConfig {
120    fn is_empty(&self) -> bool {
121        self.effort.is_none()
122    }
123
124    fn into_boxed_if_non_empty(self) -> Option<Box<Self>> {
125        (!self.is_empty()).then_some(Box::new(self))
126    }
127}
128
129/// Truncation configuration for the request.
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub struct TruncationConfig {
132    /// The truncation strategy to use.
133    pub strategy: String,
134    /// The number of tokens to keep.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub max_prompt_tokens: Option<u64>,
137}
138
139impl Request {
140    /// Creates a new request with the given model and input.
141    pub fn new(model: impl Into<String>, input: Vec<OutputItem>) -> Self {
142        Self {
143            model: model.into(),
144            input,
145            tools: None,
146            tool_choice: None,
147            stream: false,
148            temperature: None,
149            top_p: None,
150            truncation: None,
151            max_output_tokens: None,
152            max_tool_calls: None,
153            stop: None,
154            presence_penalty: None,
155            frequency_penalty: None,
156            logit_bias: None,
157            logprobs: None,
158            top_logprobs: None,
159            user: None,
160            service_tier: None,
161            reasoning: None,
162            store: None,
163            previous_response_id: None,
164            include: None,
165            metadata: None,
166        }
167    }
168
169    /// Convenience method to create a request from a single user message.
170    pub fn from_message(model: impl Into<String>, text: impl Into<String>) -> Self {
171        let item = OutputItem::completed_message(
172            "msg_init",
173            MessageRole::User,
174            vec![super::ContentPart::input_text(text)],
175        );
176        Self::new(model, vec![item])
177    }
178}
179
180fn deserialize_boxed_reasoning_config_opt<'de, D>(
181    deserializer: D,
182) -> Result<Option<Box<ReasoningConfig>>, D::Error>
183where
184    D: Deserializer<'de>,
185{
186    Option::<ReasoningConfig>::deserialize(deserializer)
187        .map(|value| value.and_then(ReasoningConfig::into_boxed_if_non_empty))
188}
189
190/// Tool choice options.
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[serde(untagged)]
193pub enum ToolChoice {
194    /// Standard tool choice mode (auto, none, required).
195    Mode(ToolChoiceMode),
196    /// Specific tool to call.
197    Tool(SpecificToolChoice),
198}
199
200/// Standard tool choice modes.
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
202#[serde(rename_all = "lowercase")]
203pub enum ToolChoiceMode {
204    /// Model decides whether to call a tool.
205    Auto,
206    /// Model MUST NOT call any tools.
207    None,
208    /// Model MUST call at least one tool.
209    Required,
210}
211
212/// Specific tool choice.
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct SpecificToolChoice {
215    /// The type of the tool, always "function".
216    #[serde(rename = "type")]
217    pub tool_type: String,
218    /// The name of the function to call.
219    pub function: FunctionName,
220}
221
222/// Function name wrapper for tool choice.
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct FunctionName {
225    /// Name of the function.
226    pub name: String,
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_request_serialization() {
235        let req = Request::from_message("gpt-5", "Hello");
236        let json = serde_json::to_string(&req).unwrap();
237        assert!(json.contains("\"model\":\"gpt-5\""));
238        assert!(json.contains("\"input\":["));
239        assert!(json.contains("\"type\":\"message\""));
240    }
241
242    #[test]
243    fn empty_reasoning_config_deserializes_to_none() {
244        let req: Request = serde_json::from_str(
245            r#"{
246                "model": "gpt-5",
247                "input": [],
248                "reasoning": {}
249            }"#,
250        )
251        .unwrap();
252
253        assert!(req.reasoning.is_none());
254    }
255
256    #[test]
257    fn boxed_reasoning_config_is_smaller_than_inline_option() {
258        use std::mem::size_of;
259
260        assert!(size_of::<Option<Box<ReasoningConfig>>>() < size_of::<Option<ReasoningConfig>>());
261    }
262}