1use serde::{Deserialize, Serialize};
4
5use crate::effort::Effort;
6use crate::error::{Error, Result};
7use crate::message::{ContentBlock, Message, Role};
8use crate::thinking::ThinkingSetting;
9use crate::tool::{Tool, ToolChoice};
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct CompletionRequest {
14 pub model: String,
16 pub messages: Vec<Message>,
18 #[serde(default, skip_serializing_if = "Vec::is_empty")]
20 pub tools: Vec<Tool>,
21 #[serde(default)]
23 pub tool_choice: ToolChoice,
24 pub max_tokens: u32,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub temperature: Option<f32>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub top_p: Option<f32>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub top_k: Option<u32>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub stop_sequences: Vec<String>,
38 #[serde(default)]
42 pub thinking: ThinkingSetting,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub effort: Option<Effort>,
47 #[serde(default)]
49 pub metadata: RequestMetadata,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum RequestPurpose {
59 MainLoop,
61 Summarization,
63 FastClassifier,
65 SubAgent,
67 Embedding,
69 Other,
71}
72
73#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
75pub struct RequestMetadata {
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub user_id: Option<String>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub purpose: Option<RequestPurpose>,
83}
84
85impl CompletionRequest {
86 pub fn builder(model: impl Into<String>) -> CompletionRequestBuilder {
88 CompletionRequestBuilder {
89 model: model.into(),
90 messages: Vec::new(),
91 tools: Vec::new(),
92 tool_choice: ToolChoice::default(),
93 max_tokens: 1024,
94 temperature: None,
95 top_p: None,
96 top_k: None,
97 stop_sequences: Vec::new(),
98 thinking: ThinkingSetting::Auto,
99 effort: None,
100 metadata: RequestMetadata::default(),
101 }
102 }
103
104 pub fn validate(&self) -> Result<()> {
112 if self.model.is_empty() {
113 return Err(Error::InvalidRequest("model is empty".into()));
114 }
115 if self.max_tokens == 0 {
116 return Err(Error::InvalidRequest("max_tokens must be > 0".into()));
117 }
118 validate_messages(&self.messages)
119 }
120}
121
122fn validate_messages(messages: &[Message]) -> Result<()> {
123 let mut seen_non_system = false;
124 let mut has_user_or_assistant = false;
125 for (i, msg) in messages.iter().enumerate() {
126 match msg.role {
127 Role::System => {
128 if seen_non_system {
129 return Err(Error::InvalidRequest(format!(
130 "Role::System message at index {i} appears after a User/Assistant \
131 message; System must lead"
132 )));
133 }
134 for block in &msg.content {
135 if !matches!(block, ContentBlock::Text(_)) {
136 return Err(Error::InvalidRequest(format!(
137 "Role::System message at index {i} contains a non-text block"
138 )));
139 }
140 }
141 }
142 Role::User | Role::Assistant => {
143 seen_non_system = true;
144 has_user_or_assistant = true;
145 }
146 }
147 }
148 if !has_user_or_assistant {
149 return Err(Error::InvalidRequest(
150 "request has no User or Assistant messages".into(),
151 ));
152 }
153 Ok(())
154}
155
156#[must_use = "builder has no effect until .build() is called"]
158pub struct CompletionRequestBuilder {
159 model: String,
160 messages: Vec<Message>,
161 tools: Vec<Tool>,
162 tool_choice: ToolChoice,
163 max_tokens: u32,
164 temperature: Option<f32>,
165 top_p: Option<f32>,
166 top_k: Option<u32>,
167 stop_sequences: Vec<String>,
168 thinking: ThinkingSetting,
169 effort: Option<Effort>,
170 metadata: RequestMetadata,
171}
172
173impl CompletionRequestBuilder {
174 pub fn system(mut self, text: impl Into<String>) -> Self {
179 let insertion_index = self
181 .messages
182 .iter()
183 .position(|m| m.role != Role::System)
184 .unwrap_or(self.messages.len());
185 self.messages
186 .insert(insertion_index, Message::system_text(text));
187 self
188 }
189
190 pub fn user_text(mut self, text: impl Into<String>) -> Self {
192 self.messages.push(Message::user_text(text));
193 self
194 }
195
196 pub fn assistant_text(mut self, text: impl Into<String>) -> Self {
198 self.messages.push(Message::assistant_text(text));
199 self
200 }
201
202 pub fn message(mut self, m: Message) -> Self {
204 self.messages.push(m);
205 self
206 }
207
208 pub fn tool(mut self, t: Tool) -> Self {
210 self.tools.push(t);
211 self
212 }
213
214 pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
216 self.tool_choice = choice;
217 self
218 }
219
220 pub fn max_tokens(mut self, n: u32) -> Self {
222 self.max_tokens = n;
223 self
224 }
225
226 pub fn temperature(mut self, t: f32) -> Self {
228 self.temperature = Some(t);
229 self
230 }
231
232 pub fn top_p(mut self, p: f32) -> Self {
234 self.top_p = Some(p);
235 self
236 }
237
238 pub fn top_k(mut self, k: u32) -> Self {
240 self.top_k = Some(k);
241 self
242 }
243
244 pub fn stop_sequence(mut self, s: impl Into<String>) -> Self {
246 self.stop_sequences.push(s.into());
247 self
248 }
249
250 pub fn thinking(mut self, setting: ThinkingSetting) -> Self {
252 self.thinking = setting;
253 self
254 }
255
256 pub fn effort(mut self, e: Effort) -> Self {
260 self.effort = Some(e);
261 self
262 }
263
264 pub fn user_id(mut self, id: impl Into<String>) -> Self {
266 self.metadata.user_id = Some(id.into());
267 self
268 }
269
270 #[must_use = "discarding the Result silently ignores validation errors"]
276 pub fn build(self) -> Result<CompletionRequest> {
277 let req = CompletionRequest {
278 model: self.model,
279 messages: self.messages,
280 tools: self.tools,
281 tool_choice: self.tool_choice,
282 max_tokens: self.max_tokens,
283 temperature: self.temperature,
284 top_p: self.top_p,
285 top_k: self.top_k,
286 stop_sequences: self.stop_sequences,
287 thinking: self.thinking,
288 effort: self.effort,
289 metadata: self.metadata,
290 };
291 req.validate()?;
292 Ok(req)
293 }
294}