vtcode_llm/open_responses/
request.rs1use serde::{Deserialize, Deserializer, Serialize};
7use serde_json::Value;
8
9use super::{MessageRole, OutputItem};
10use crate::provider::ToolDefinition;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct Request {
15 model: String,
17
18 input: Vec<OutputItem>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub(crate) tools: Option<Vec<ToolDefinition>>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 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 top_p: Option<f64>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 truncation: Option<Box<TruncationConfig>>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub(crate) max_output_tokens: Option<u64>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 max_tool_calls: Option<u64>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 stop: Option<Vec<String>>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 presence_penalty: Option<f64>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 frequency_penalty: Option<f64>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 logit_bias: Option<hashbrown::HashMap<String, f64>>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 logprobs: Option<bool>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 top_logprobs: Option<u32>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
80 user: Option<String>,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
84 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 reasoning: Option<Box<ReasoningConfig>>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub(crate) store: Option<bool>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub(crate) previous_response_id: Option<String>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub(crate) include: Option<Vec<String>>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
108 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(crate) 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 =
172 OutputItem::completed_message("msg_init", MessageRole::User, vec![super::ContentPart::input_text(text)]);
173 Self::new(model, vec![item])
174 }
175}
176
177fn deserialize_boxed_reasoning_config_opt<'de, D>(deserializer: D) -> Result<Option<Box<ReasoningConfig>>, D::Error>
178where
179 D: Deserializer<'de>,
180{
181 Option::<ReasoningConfig>::deserialize(deserializer)
182 .map(|value| value.and_then(ReasoningConfig::into_boxed_if_non_empty))
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187#[serde(untagged)]
188pub enum ToolChoice {
189 Mode(ToolChoiceMode),
191 Tool(SpecificToolChoice),
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
197#[serde(rename_all = "lowercase")]
198pub enum ToolChoiceMode {
199 Auto,
201 None,
203 Required,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209pub struct SpecificToolChoice {
210 #[serde(rename = "type")]
212 tool_type: String,
213 function: FunctionName,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub struct FunctionName {
220 pub name: String,
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_request_serialization() {
230 let req = Request::from_message("gpt-5", "Hello");
231 let json = serde_json::to_string(&req).unwrap();
232 assert!(json.contains("\"model\":\"gpt-5\""));
233 assert!(json.contains("\"input\":["));
234 assert!(json.contains("\"type\":\"message\""));
235 }
236
237 #[test]
238 fn empty_reasoning_config_deserializes_to_none() {
239 let req: Request = serde_json::from_str(
240 r#"{
241 "model": "gpt-5",
242 "input": [],
243 "reasoning": {}
244 }"#,
245 )
246 .unwrap();
247
248 assert!(req.reasoning.is_none());
249 }
250
251 #[test]
252 fn boxed_reasoning_config_is_smaller_than_inline_option() {
253 use std::mem::size_of;
254
255 assert!(size_of::<Option<Box<ReasoningConfig>>>() < size_of::<Option<ReasoningConfig>>());
256 }
257}