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}