1use serde::ser::SerializeStruct;
7use serde::{Deserialize, Serialize, Serializer};
8use serde_json::Value;
9use std::fmt;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ToolDefinition {
15 #[serde(rename = "type")]
16 pub kind: String,
17 pub function: FunctionDefinition,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct FunctionDefinition {
22 pub name: String,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub description: Option<String>,
25 pub parameters: Value,
26}
27
28impl ToolDefinition {
29 pub fn function(
30 name: impl Into<String>,
31 description: impl Into<String>,
32 parameters: Value,
33 ) -> Self {
34 Self {
35 kind: "function".to_string(),
36 function: FunctionDefinition {
37 name: name.into(),
38 description: Some(description.into()),
39 parameters,
40 },
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum ToolChoice {
49 None,
50 Auto,
51 Required,
52 Function(String),
53}
54
55impl Serialize for ToolChoice {
56 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
57 match self {
58 ToolChoice::None => ser.serialize_str("none"),
59 ToolChoice::Auto => ser.serialize_str("auto"),
60 ToolChoice::Required => ser.serialize_str("required"),
61 ToolChoice::Function(name) => {
62 #[derive(Serialize)]
63 struct Named<'a> {
64 name: &'a str,
65 }
66 let mut s = ser.serialize_struct("ToolChoice", 2)?;
67 s.serialize_field("type", "function")?;
68 s.serialize_field("function", &Named { name })?;
69 s.end()
70 }
71 }
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78pub struct ToolCall {
79 pub id: String,
80 #[serde(rename = "type")]
81 pub kind: String,
82 pub function: FunctionCall,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86pub struct FunctionCall {
87 pub name: String,
88 pub arguments: String,
91}
92
93#[derive(Clone, Serialize, Deserialize, PartialEq)]
98#[serde(tag = "type", rename_all = "snake_case")]
99pub enum ChatContentPart {
100 Text { text: String },
101 ImageUrl { image_url: ChatImageUrl },
102 File { file: ChatFile },
103}
104
105impl ChatContentPart {
106 pub fn text(text: impl Into<String>) -> Self {
107 Self::Text { text: text.into() }
108 }
109}
110
111#[derive(Clone, Serialize, Deserialize, PartialEq)]
112pub struct ChatImageUrl {
113 pub url: String,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub detail: Option<String>,
116}
117
118#[derive(Clone, Serialize, Deserialize, PartialEq)]
119pub struct ChatFile {
120 pub filename: String,
121 pub file_data: String,
122}
123
124#[derive(Clone)]
125pub struct ChatMessage {
126 pub role: String,
127
128 pub content: Option<String>,
131
132 pub content_parts: Option<Vec<ChatContentPart>>,
136
137 pub tool_calls: Option<Vec<ToolCall>>,
138 pub tool_call_id: Option<String>,
139 pub name: Option<String>,
140}
141
142impl fmt::Debug for ChatContentPart {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 Self::Text { text } => f
148 .debug_struct("Text")
149 .field("text_bytes", &text.len())
150 .finish(),
151 Self::ImageUrl { image_url } => f.debug_tuple("ImageUrl").field(image_url).finish(),
152 Self::File { file } => f.debug_tuple("File").field(file).finish(),
153 }
154 }
155}
156
157impl fmt::Debug for ChatImageUrl {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 f.debug_struct("ChatImageUrl")
160 .field("url_bytes", &self.url.len())
161 .field("has_detail", &self.detail.is_some())
162 .finish()
163 }
164}
165
166impl fmt::Debug for ChatFile {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 f.debug_struct("ChatFile")
169 .field("filename_bytes", &self.filename.len())
170 .field("file_data_bytes", &self.file_data.len())
171 .finish()
172 }
173}
174
175impl fmt::Debug for ChatMessage {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 let role = match self.role.as_str() {
178 "user" => "user",
179 "assistant" => "assistant",
180 "system" => "system",
181 "developer" => "developer",
182 "tool" => "tool",
183 _ => "other",
184 };
185 f.debug_struct("ChatMessage")
186 .field("role", &role)
187 .field("content_bytes", &self.content.as_ref().map(String::len))
188 .field("content_parts", &self.content_parts)
189 .field("tool_call_count", &self.tool_calls.as_ref().map(Vec::len))
190 .field("has_tool_call_id", &self.tool_call_id.is_some())
191 .field("has_name", &self.name.is_some())
192 .finish()
193 }
194}
195
196impl Serialize for ChatMessage {
197 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
198 let mut s = ser.serialize_struct(
199 "ChatMessage",
200 2 + usize::from(self.tool_calls.is_some())
201 + usize::from(self.tool_call_id.is_some())
202 + usize::from(self.name.is_some()),
203 )?;
204 s.serialize_field("role", &self.role)?;
205 if let Some(parts) = &self.content_parts {
206 s.serialize_field("content", parts)?;
207 } else {
208 s.serialize_field("content", &self.content)?;
209 }
210 if let Some(calls) = &self.tool_calls {
211 s.serialize_field("tool_calls", calls)?;
212 }
213 if let Some(id) = &self.tool_call_id {
214 s.serialize_field("tool_call_id", id)?;
215 }
216 if let Some(name) = &self.name {
217 s.serialize_field("name", name)?;
218 }
219 s.end()
220 }
221}
222
223impl<'de> Deserialize<'de> for ChatMessage {
224 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
225 #[derive(Deserialize)]
226 #[serde(untagged)]
227 enum Content {
228 Text(String),
229 Parts(Vec<ChatContentPart>),
230 }
231 #[derive(Deserialize)]
232 struct WireMessage {
233 role: String,
234 content: Option<Content>,
235 tool_calls: Option<Vec<ToolCall>>,
236 tool_call_id: Option<String>,
237 name: Option<String>,
238 }
239 let wire = WireMessage::deserialize(de)?;
240 let (content, content_parts) = match wire.content {
241 Some(Content::Text(text)) => (Some(text), None),
242 Some(Content::Parts(parts)) => (None, Some(parts)),
243 None => (None, None),
244 };
245 Ok(Self {
246 role: wire.role,
247 content,
248 content_parts,
249 tool_calls: wire.tool_calls,
250 tool_call_id: wire.tool_call_id,
251 name: wire.name,
252 })
253 }
254}
255
256impl ChatMessage {
257 pub fn user(content: impl Into<String>) -> Self {
258 Self {
259 role: "user".into(),
260 content: Some(content.into()),
261 content_parts: None,
262 tool_calls: None,
263 tool_call_id: None,
264 name: None,
265 }
266 }
267 pub fn user_parts(parts: Vec<ChatContentPart>) -> Self {
268 Self {
269 role: "user".into(),
270 content: None,
271 content_parts: Some(parts),
272 tool_calls: None,
273 tool_call_id: None,
274 name: None,
275 }
276 }
277 pub fn system(content: impl Into<String>) -> Self {
278 Self {
279 role: "system".into(),
280 content: Some(content.into()),
281 content_parts: None,
282 tool_calls: None,
283 tool_call_id: None,
284 name: None,
285 }
286 }
287 pub fn assistant(content: impl Into<String>) -> Self {
288 Self {
289 role: "assistant".into(),
290 content: Some(content.into()),
291 content_parts: None,
292 tool_calls: None,
293 tool_call_id: None,
294 name: None,
295 }
296 }
297 pub fn assistant_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
298 Self {
299 role: "assistant".into(),
300 content: None,
301 content_parts: None,
302 tool_calls: Some(tool_calls),
303 tool_call_id: None,
304 name: None,
305 }
306 }
307 pub fn tool_result(
308 tool_call_id: impl Into<String>,
309 name: impl Into<String>,
310 content: impl Into<String>,
311 ) -> Self {
312 Self {
313 role: "tool".into(),
314 content: Some(content.into()),
315 content_parts: None,
316 tool_calls: None,
317 tool_call_id: Some(tool_call_id.into()),
318 name: Some(name.into()),
319 }
320 }
321
322 pub fn content(&self) -> Option<&str> {
323 self.content.as_deref()
324 }
325}
326
327#[derive(Debug, Clone, Default)]
330pub struct ChatOptions {
331 pub max_tokens: Option<u32>,
332 pub temperature: Option<f32>,
333 pub tools: Option<Vec<ToolDefinition>>,
334 pub tool_choice: Option<ToolChoice>,
335}
336
337#[derive(Debug, Clone, Serialize)]
338pub struct StreamOptions {
339 pub include_usage: bool,
340}
341
342#[derive(Debug, Clone, Serialize)]
343pub struct ChatRequest {
344 pub model: String,
345 pub messages: Vec<ChatMessage>,
346 pub stream: bool,
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub stream_options: Option<StreamOptions>,
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub max_tokens: Option<u32>,
351 #[serde(skip_serializing_if = "Option::is_none")]
352 pub temperature: Option<f32>,
353 #[serde(skip_serializing_if = "Option::is_none")]
354 pub tools: Option<Vec<ToolDefinition>>,
355 #[serde(skip_serializing_if = "Option::is_none")]
356 pub tool_choice: Option<ToolChoice>,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub enum FinishReason {
363 Stop,
364 Length,
365 ToolCalls,
366 ContentFilter,
367}
368
369#[derive(Debug, Clone)]
372#[non_exhaustive]
373pub enum OaiEvent {
374 RoleStart(String),
375 TextDelta(String),
376 ToolCallStart {
377 index: u32,
378 id: String,
379 name: String,
380 },
381 ToolCallArgumentsDelta {
382 index: u32,
383 id: String,
384 delta: String,
385 },
386 ToolCallsComplete {
387 calls: Vec<ToolCall>,
388 truncated: bool,
389 },
390 Usage {
391 prompt_tokens: u32,
392 completion_tokens: u32,
393 cached_tokens: u32,
394 },
395 Warning(String),
396 Done,
397}
398
399#[derive(Clone, Debug)]
405pub struct ProviderConfig {
406 pub base_url: String,
407 pub model: String,
408 pub provider: String,
409}
410
411#[cfg(test)]
412mod chat_message_tests {
413 use super::*;
414 use serde_json::json;
415
416 #[test]
417 fn legacy_text_and_null_serialization_is_byte_stable() {
418 let call = ToolCall {
419 id: "t1".into(),
420 kind: "function".into(),
421 function: FunctionCall {
422 name: "read".into(),
423 arguments: "{}".into(),
424 },
425 };
426 let cases = vec![
427 (
428 ChatMessage::user("hello"),
429 r#"{"role":"user","content":"hello"}"#,
430 ),
431 (
432 ChatMessage::system("rules"),
433 r#"{"role":"system","content":"rules"}"#,
434 ),
435 (
436 ChatMessage::assistant("answer"),
437 r#"{"role":"assistant","content":"answer"}"#,
438 ),
439 (
440 ChatMessage::assistant_tool_calls(vec![call]),
441 r#"{"role":"assistant","content":null,"tool_calls":[{"id":"t1","type":"function","function":{"name":"read","arguments":"{}"}}]}"#,
442 ),
443 (
444 ChatMessage::tool_result("t1", "read", "done"),
445 r#"{"role":"tool","content":"done","tool_call_id":"t1","name":"read"}"#,
446 ),
447 ];
448 for (message, expected) in cases {
449 assert_eq!(serde_json::to_string(&message).unwrap(), expected);
450 let restored: ChatMessage = serde_json::from_str(expected).unwrap();
451 assert_eq!(restored.content(), message.content());
452 assert!(restored.content_parts.is_none());
453 assert_eq!(serde_json::to_string(&restored).unwrap(), expected);
454 }
455 }
456
457 #[test]
458 fn multipart_serializes_only_as_content_and_round_trips() {
459 let wire = json!({"role":"user","content":[
460 {"type":"text","text":"private text document"},
461 {"type":"image_url","image_url":{"url":"data:image/png;base64,aW1hZ2U=","detail":"high"}},
462 {"type":"file","file":{"filename":"report.pdf","file_data":"data:application/pdf;base64,JVBERi0="}}
463 ]});
464 let mut restored: ChatMessage = serde_json::from_value(wire.clone()).unwrap();
465 assert_eq!(restored.content(), None);
466 assert_eq!(restored.content_parts.as_ref().unwrap().len(), 3);
467 assert_eq!(serde_json::to_value(&restored).unwrap(), wire);
468 restored.content = Some("legacy field is not concatenated or duplicated".into());
469 assert_eq!(serde_json::to_value(restored).unwrap(), wire);
470 }
471
472 #[test]
473 fn debug_is_metadata_only_for_all_attachment_types_and_messages() {
474 let image = ChatImageUrl {
475 url: "data:image/png;base64,IMAGE_PAYLOAD_SENTINEL".into(),
476 detail: Some("PRIVATE_DETAIL_SENTINEL".into()),
477 };
478 let file = ChatFile {
479 filename: "PRIVATE_FILENAME_SENTINEL.pdf".into(),
480 file_data: "data:application/pdf;base64,PDF_PAYLOAD_SENTINEL".into(),
481 };
482 let parts = vec![
483 ChatContentPart::text("TEXT_DOCUMENT_SENTINEL"),
484 ChatContentPart::ImageUrl {
485 image_url: image.clone(),
486 },
487 ChatContentPart::File { file: file.clone() },
488 ];
489 let mut message = ChatMessage::user_parts(parts.clone());
490 message.content = Some("LEGACY_TEXT_SENTINEL".into());
491 message.name = Some("PRIVATE_NAME_SENTINEL".into());
492 message.tool_call_id = Some("PRIVATE_ID_SENTINEL".into());
493 message.tool_calls = Some(vec![ToolCall {
494 id: "PRIVATE_ID_SENTINEL".into(),
495 kind: "function".into(),
496 function: FunctionCall {
497 name: "PRIVATE_NAME_SENTINEL".into(),
498 arguments: "PRIVATE_ARGUMENTS_SENTINEL".into(),
499 },
500 }]);
501 let debug = format!("{image:?} {file:?} {parts:?} {message:?}");
502 assert!(
503 !debug.contains("SENTINEL"),
504 "Debug must not emit private values"
505 );
506 assert!(!debug.contains("data:"));
507 assert!(debug.contains("file_data_bytes"));
508 assert!(debug.contains("text_bytes"));
509 assert!(debug.contains("tool_call_count: Some(1)"));
510 }
511}