vtcode_core/open_responses/
request.rs1use serde::{Deserialize, Deserializer, Serialize};
7use serde_json::Value;
8
9use super::{MessageRole, OutputItem};
10use crate::llm::provider::ToolDefinition;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct Request {
15 pub model: String,
17
18 pub input: Vec<OutputItem>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub tools: Option<Vec<ToolDefinition>>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub tool_choice: Option<ToolChoice>,
29
30 #[serde(default)]
32 pub stream: bool,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub temperature: Option<f64>,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub top_p: Option<f64>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub truncation: Option<Box<TruncationConfig>>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub max_output_tokens: Option<u64>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub max_tool_calls: Option<u64>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub stop: Option<Vec<String>>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub presence_penalty: Option<f64>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub frequency_penalty: Option<f64>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub logit_bias: Option<hashbrown::HashMap<String, f64>>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub logprobs: Option<bool>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub top_logprobs: Option<u32>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub user: Option<String>,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub service_tier: Option<String>,
85
86 #[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 #[serde(skip_serializing_if = "Option::is_none")]
96 pub store: Option<bool>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub previous_response_id: Option<String>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub include: Option<Vec<String>>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub metadata: Option<Value>,
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct ReasoningConfig {
114 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub struct TruncationConfig {
132 pub strategy: String,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub max_prompt_tokens: Option<u64>,
137}
138
139impl Request {
140 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[serde(untagged)]
193pub enum ToolChoice {
194 Mode(ToolChoiceMode),
196 Tool(SpecificToolChoice),
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
202#[serde(rename_all = "lowercase")]
203pub enum ToolChoiceMode {
204 Auto,
206 None,
208 Required,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct SpecificToolChoice {
215 #[serde(rename = "type")]
217 pub tool_type: String,
218 pub function: FunctionName,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct FunctionName {
225 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}