openai_tools/common.rs
1//! # Common Types and Structures
2//!
3//! This module contains common data structures and types used across the OpenAI Tools library.
4//! These structures represent core concepts like messages, token usage, and other shared
5//! components that are used by multiple API endpoints.
6//!
7//! ## Key Components
8//!
9//! - **Message**: Represents a single message in a conversation
10//! - **Usage**: Token usage statistics for API requests
11//!
12//! ## Example
13//!
14//! ```rust
15//! use openai_tools::common::{Message, Usage};
16//!
17//! // Create a user message
18//! let message = Message::from_string("user".to_string(), "Hello, world!".to_string());
19//!
20//! // Usage is typically returned by API responses
21//! let usage = Usage::new(
22//! Some(10), // input_tokens
23//! None, // input_tokens_details
24//! Some(20), // output_tokens
25//! None, // output_tokens_details
26//! Some(10), // prompt_tokens
27//! Some(20), // completion_tokens
28//! Some(30), // total_tokens
29//! None, // completion_tokens_details
30//! );
31//!
32//! println!("Total tokens used: {:?}", usage.total_tokens);
33//! ```
34
35use base64::prelude::*;
36use derive_new::new;
37use fxhash::FxHashMap;
38use serde::{ser::SerializeStruct, Deserialize, Serialize};
39
40/// Token usage statistics for OpenAI API requests.
41///
42/// This structure contains detailed information about token consumption during
43/// API requests, including both input (prompt) and output (completion) tokens.
44/// Different fields may be populated depending on the specific API endpoint
45/// and model used.
46///
47/// # Fields
48///
49/// * `input_tokens` - Number of tokens in the input/prompt
50/// * `input_tokens_details` - Detailed breakdown of input token usage by category
51/// * `output_tokens` - Number of tokens in the output/completion
52/// * `output_tokens_details` - Detailed breakdown of output token usage by category
53/// * `prompt_tokens` - Legacy field for input tokens (may be deprecated)
54/// * `completion_tokens` - Legacy field for output tokens (may be deprecated)
55/// * `total_tokens` - Total number of tokens used (input + output)
56/// * `completion_tokens_details` - Detailed breakdown of completion token usage
57///
58/// # Note
59///
60/// Not all fields will be populated for every request. The availability of
61/// detailed token breakdowns depends on the model and API endpoint being used.
62///
63/// # Example
64///
65/// ```rust
66/// use openai_tools::common::Usage;
67///
68/// // Create usage statistics manually (typically done by API response parsing)
69/// let usage = Usage::new(
70/// Some(25), // input tokens
71/// None, // no detailed input breakdown
72/// Some(50), // output tokens
73/// None, // no detailed output breakdown
74/// Some(25), // prompt tokens (legacy)
75/// Some(50), // completion tokens (legacy)
76/// Some(75), // total tokens
77/// None, // no detailed completion breakdown
78/// );
79///
80/// if let Some(total) = usage.total_tokens {
81/// println!("Request used {} tokens total", total);
82/// }
83/// ```
84#[derive(Debug, Clone, Default, Deserialize, Serialize, new)]
85pub struct Usage {
86 pub input_tokens: Option<usize>,
87 pub input_tokens_details: Option<FxHashMap<String, usize>>,
88 pub output_tokens: Option<usize>,
89 pub output_tokens_details: Option<FxHashMap<String, usize>>,
90 pub prompt_tokens: Option<usize>,
91 pub completion_tokens: Option<usize>,
92 pub total_tokens: Option<usize>,
93 pub completion_tokens_details: Option<FxHashMap<String, usize>>,
94}
95
96/// Represents the content of a message, which can be either text or an image.
97///
98/// This structure is used to encapsulate different types of message content
99/// that can be sent to or received from AI models. It supports both text-based
100/// content and image content (either as URLs or base64-encoded data).
101///
102/// # Fields
103///
104/// * `type_name` - The type of content ("input_text" for text, "input_image" for images)
105/// * `text` - Optional text content when the message contains text
106/// * `image_url` - Optional image URL or base64-encoded image data when the message contains an image
107///
108/// # Example
109///
110/// ```rust
111/// use openai_tools::common::MessageContent;
112///
113/// // Create text content
114/// let text_content = MessageContent::from_text("Hello, world!".to_string());
115///
116/// // Create image content from URL
117/// // let image_content = MessageContent::from_image_url("https://example.com/image.jpg".to_string());
118///
119/// // Create image content from file
120/// // let file_content = MessageContent::from_image_file("path/to/image.jpg".to_string());
121/// ```
122#[derive(Debug, Clone, Default, Deserialize, Serialize)]
123pub struct MessageContent {
124 #[serde(rename = "type")]
125 pub type_name: String,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub text: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub image_url: Option<String>,
130}
131
132impl MessageContent {
133 /// Creates a new `MessageContent` containing text.
134 ///
135 /// This constructor creates a message content instance specifically for text-based
136 /// messages. The content type is automatically set to "input_text" and the
137 /// image_url field is set to None.
138 ///
139 /// # Arguments
140 ///
141 /// * `text` - The text content to include in the message
142 ///
143 /// # Returns
144 ///
145 /// A new `MessageContent` instance configured for text content.
146 ///
147 /// # Example
148 ///
149 /// ```rust
150 /// use openai_tools::common::MessageContent;
151 ///
152 /// let content = MessageContent::from_text("Hello, AI assistant!".to_string());
153 /// assert_eq!(content.type_name, "input_text");
154 /// assert_eq!(content.text, Some("Hello, AI assistant!".to_string()));
155 /// assert_eq!(content.image_url, None);
156 /// ```
157 pub fn from_text(text: String) -> Self {
158 Self {
159 type_name: "input_text".to_string(),
160 text: Some(text),
161 image_url: None,
162 }
163 }
164
165 /// Creates a new `MessageContent` containing an image from a URL.
166 ///
167 /// This constructor creates a message content instance for image-based messages
168 /// using an existing image URL. The content type is automatically set to
169 /// "input_image" and the text field is set to None.
170 ///
171 /// # Arguments
172 ///
173 /// * `image_url` - The URL or base64-encoded data URI of the image
174 ///
175 /// # Returns
176 ///
177 /// A new `MessageContent` instance configured for image content.
178 ///
179 /// # Example
180 ///
181 /// ```rust
182 /// use openai_tools::common::MessageContent;
183 ///
184 /// let content = MessageContent::from_image_url("https://example.com/image.jpg".to_string());
185 /// assert_eq!(content.type_name, "input_image");
186 /// assert_eq!(content.text, None);
187 /// assert_eq!(content.image_url, Some("https://example.com/image.jpg".to_string()));
188 /// ```
189 pub fn from_image_url(image_url: String) -> Self {
190 Self {
191 type_name: "input_image".to_string(),
192 text: None,
193 image_url: Some(image_url),
194 }
195 }
196
197 /// Creates a new `MessageContent` containing an image loaded from a file.
198 ///
199 /// This constructor reads an image file from the local filesystem, encodes it
200 /// as base64, and creates a data URI suitable for use with AI models. The
201 /// content type is automatically set to "input_image" and the text field
202 /// is set to None.
203 ///
204 /// # Arguments
205 ///
206 /// * `file_path` - The path to the image file to load
207 ///
208 /// # Returns
209 ///
210 /// A new `MessageContent` instance configured for image content with base64-encoded data.
211 ///
212 /// # Supported Formats
213 ///
214 /// - PNG (.png)
215 /// - JPEG (.jpg, .jpeg)
216 /// - GIF (.gif)
217 ///
218 pub fn from_image_file(file_path: String) -> Self {
219 let ext = file_path.clone();
220 let ext = std::path::Path::new(&ext)
221 .extension()
222 .and_then(|s| s.to_str())
223 .unwrap();
224 let img = image::ImageReader::open(file_path.clone())
225 .expect("Failed to open image file")
226 .decode()
227 .expect("Failed to decode image");
228 let img_fmt = match ext {
229 "png" => image::ImageFormat::Png,
230 "jpg" | "jpeg" => image::ImageFormat::Jpeg,
231 "gif" => image::ImageFormat::Gif,
232 _ => panic!("Unsupported image format"),
233 };
234 let mut buf = std::io::Cursor::new(Vec::new());
235 img.write_to(&mut buf, img_fmt)
236 .expect("Failed to write image to buffer");
237 let base64_string = BASE64_STANDARD.encode(buf.into_inner());
238 let image_url = format!("data:image/{};base64,{}", ext, base64_string);
239 Self {
240 type_name: "input_image".to_string(),
241 text: None,
242 image_url: Some(image_url),
243 }
244 }
245}
246
247/// Represents a single message in a conversation with an AI model.
248///
249/// Messages are the fundamental building blocks of conversations in chat-based
250/// AI interactions. Each message has a role (indicating who sent it) and content
251/// (the actual message text). Messages can also contain refusal information
252/// when the AI model declines to respond to certain requests.
253///
254/// # Roles
255///
256/// Common roles include:
257/// - **"system"**: System messages that set the behavior or context for the AI
258/// - **"user"**: Messages from the human user
259/// - **"assistant"**: Messages from the AI assistant
260/// - **"function"**: Messages related to function/tool calls (for advanced use cases)
261///
262/// # Fields
263///
264/// * `role` - The role of the message sender
265/// * `content` - The text content of the message
266/// * `refusal` - Optional refusal message if the AI declined to respond
267///
268/// # Example
269///
270/// ```rust
271/// use openai_tools::common::Message;
272///
273/// // Create a system message to set context
274/// let system_msg = Message::from_string(
275/// "system".to_string(),
276/// "You are a helpful assistant that explains complex topics simply.".to_string()
277/// );
278///
279/// // Create a user message
280/// let user_msg = Message::from_string(
281/// "user".to_string(),
282/// "What is quantum computing?".to_string()
283/// );
284///
285/// // Create an assistant response
286/// let assistant_msg = Message::from_string(
287/// "assistant".to_string(),
288/// "Quantum computing is a type of computation that uses quantum mechanics...".to_string()
289/// );
290///
291/// // Messages are typically used in vectors for conversation history
292/// let conversation = vec![system_msg, user_msg, assistant_msg];
293/// ```
294#[derive(Debug, Clone, Deserialize)]
295pub struct Message {
296 role: String,
297 content: Option<MessageContent>,
298 contents: Option<Vec<MessageContent>>,
299}
300
301impl Serialize for Message {
302 /// Custom serialization implementation for `Message`.
303 ///
304 /// This method ensures that messages are serialized correctly by enforcing
305 /// that either `content` or `contents` is present, but not both. This prevents
306 /// invalid message structures from being serialized.
307 ///
308 /// # Arguments
309 ///
310 /// * `serializer` - The serializer to use for output
311 ///
312 /// # Returns
313 ///
314 /// Result of the serialization operation
315 ///
316 /// # Errors
317 ///
318 /// Returns a serialization error if:
319 /// - Both `content` and `contents` are present
320 /// - Neither `content` nor `contents` are present
321 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
322 where
323 S: serde::Serializer,
324 {
325 let mut state = serializer.serialize_struct("Message", 3)?;
326 state.serialize_field("role", &self.role)?;
327
328 // Ensure that either content or contents is present, but not both
329 if (self.content.is_none() && self.contents.is_none())
330 || (self.content.is_some() && self.contents.is_some())
331 {
332 return Err(serde::ser::Error::custom(
333 "Message must have either content or contents",
334 ));
335 }
336
337 // Serialize content or contents based on which one is present
338 if let Some(content) = &self.content {
339 state.serialize_field("content", &content.text)?;
340 }
341 if let Some(contents) = &self.contents {
342 state.serialize_field("content", contents)?;
343 }
344 state.end()
345 }
346}
347
348impl Message {
349 /// Creates a new `Message` with the specified role and content.
350 ///
351 /// This is the primary constructor for creating message instances.
352 /// The `refusal` field is automatically set to `None` and can be
353 /// modified separately if needed.
354 ///
355 /// # Arguments
356 ///
357 /// * `role` - The role of the message sender (e.g., "user", "assistant", "system")
358 /// * `message` - The text content of the message
359 ///
360 /// # Returns
361 ///
362 /// A new `Message` instance with the specified role and content.
363 ///
364 /// # Example
365 ///
366 /// ```rust
367 /// # use openai_tools::common::Message;
368 /// # pub fn main() {
369 /// // Create various types of messages
370 /// let system_message = Message::from_string(
371 /// "system".to_string(),
372 /// "You are a helpful AI assistant.".to_string()
373 /// );
374 ///
375 /// let user_message = Message::from_string(
376 /// "user".to_string(),
377 /// "Hello! How are you today?".to_string()
378 /// );
379 ///
380 /// let assistant_message = Message::from_string(
381 /// "assistant".to_string(),
382 /// "Hello! I'm doing well, thank you for asking.".to_string()
383 /// );
384 /// # }
385 /// ```
386 /// Creates a new `Message` from a role and text string.
387 ///
388 /// This constructor creates a message with a single text content. It's the
389 /// most common way to create simple text-based messages for conversations
390 /// with AI models.
391 ///
392 /// # Arguments
393 ///
394 /// * `role` - The role of the message sender (e.g., "user", "assistant", "system")
395 /// * `message` - The text content of the message
396 ///
397 /// # Returns
398 ///
399 /// A new `Message` instance with the specified role and text content.
400 ///
401 /// # Example
402 ///
403 /// ```rust
404 /// use openai_tools::common::Message;
405 ///
406 /// let user_message = Message::from_string(
407 /// "user".to_string(),
408 /// "What is the weather like today?".to_string()
409 /// );
410 ///
411 /// let system_message = Message::from_string(
412 /// "system".to_string(),
413 /// "You are a helpful weather assistant.".to_string()
414 /// );
415 /// ```
416 pub fn from_string(role: String, message: String) -> Self {
417 Self {
418 role: String::from(role),
419 content: Some(MessageContent::from_text(String::from(message))),
420 contents: None,
421 }
422 }
423
424 /// Creates a new `Message` from a role and an array of message contents.
425 ///
426 /// This constructor allows creating messages with multiple content types,
427 /// such as messages that contain both text and images. This is useful for
428 /// multimodal conversations where a single message may include various
429 /// types of content.
430 ///
431 /// # Arguments
432 ///
433 /// * `role` - The role of the message sender (e.g., "user", "assistant", "system")
434 /// * `contents` - A vector of `MessageContent` instances representing different content types
435 ///
436 /// # Returns
437 ///
438 /// A new `Message` instance with the specified role and multiple content elements.
439 ///
440 /// # Example
441 ///
442 /// ```rust
443 /// use openai_tools::common::{Message, MessageContent};
444 ///
445 /// let contents = vec![
446 /// MessageContent::from_text("Please analyze this image:".to_string()),
447 /// MessageContent::from_image_url("https://example.com/image.jpg".to_string()),
448 /// ];
449 ///
450 /// let multimodal_message = Message::from_message_array(
451 /// "user".to_string(),
452 /// contents
453 /// );
454 /// ```
455 pub fn from_message_array(role: String, contents: Vec<MessageContent>) -> Self {
456 Self {
457 role: String::from(role),
458 content: None,
459 contents: Some(contents),
460 }
461 }
462
463 /// Calculates the number of input tokens for this message.
464 ///
465 /// This method uses the OpenAI tiktoken tokenizer (o200k_base) to count
466 /// the number of tokens in the text content of the message. This is useful
467 /// for estimating API costs and ensuring messages don't exceed token limits.
468 ///
469 /// # Returns
470 ///
471 /// The number of tokens in the message's text content. Returns 0 if:
472 /// - The message has no content
473 /// - The message content has no text (e.g., image-only messages)
474 ///
475 /// # Note
476 ///
477 /// This method only counts tokens for text content. Image content tokens
478 /// are not included in the count as they are calculated differently by
479 /// the OpenAI API.
480 ///
481 /// # Example
482 ///
483 /// ```rust
484 /// use openai_tools::common::Message;
485 ///
486 /// let message = Message::from_string(
487 /// "user".to_string(),
488 /// "Hello, how are you today?".to_string()
489 /// );
490 ///
491 /// let token_count = message.get_input_token_count();
492 /// println!("Message contains {} tokens", token_count);
493 /// ```
494 pub fn get_input_token_count(&self) -> usize {
495 let bpe = tiktoken_rs::o200k_base().unwrap();
496 if let Some(content) = &self.content {
497 return bpe
498 .encode_with_special_tokens(&content.clone().text.unwrap())
499 .len();
500 } else if let Some(contents) = &self.contents {
501 let mut total_tokens = 0;
502 for content in contents {
503 if let Some(text) = &content.text {
504 total_tokens += bpe.encode_with_special_tokens(text).len();
505 }
506 }
507 return total_tokens;
508 } else {
509 return 0; // No content to count tokens for
510 }
511 }
512}