Skip to main content

limit_llm/
types.rs

1//! Core types for LLM message passing and tool definitions.
2//!
3//! This module provides the fundamental types used throughout `limit-llm` for
4//! constructing messages, defining tools, and handling responses from LLM providers.
5//!
6//! # Overview
7//!
8//! - [`Message`] — A single message in a conversation with role and content
9//! - [`Role`] — The sender role (User, Assistant, System, or Tool)
10//! - [`Tool`] / [`ToolCall`] — Function calling definitions for LLM tool use
11//! - [`Response`] — Complete response with content, tool calls, and usage
12//! - [`Usage`] — Token counting for prompt and completion
13
14use serde::{Deserialize, Serialize};
15
16/// A single message in a conversation.
17///
18/// Messages are the fundamental unit of communication with LLM providers.
19/// Each message has a role (who sent it), content (the text), and optionally
20/// tool calls (for function calling).
21///
22/// # Examples
23///
24/// ## User Message
25///
26/// ```
27/// use limit_llm::{Message, Role};
28///
29/// let msg = Message {
30///     role: Role::User,
31///     content: Some("What is the capital of France?".to_string()),
32///     tool_calls: None,
33///     tool_call_id: None,
34/// };
35/// ```
36///
37/// ## Assistant Message with Tool Call
38///
39/// ```
40/// use limit_llm::{Message, Role, ToolCall, FunctionCall};
41///
42/// let msg = Message {
43///     role: Role::Assistant,
44///     content: None,
45///     tool_calls: Some(vec![ToolCall {
46///         id: "call_123".to_string(),
47///         tool_type: "function".to_string(),
48///         function: FunctionCall {
49///             name: "get_weather".to_string(),
50///             arguments: r#"{"location": "Paris"}"#.to_string(),
51///         },
52///     }]),
53///     tool_call_id: None,
54/// };
55/// ```
56///
57/// ## Tool Result Message
58///
59/// ```
60/// use limit_llm::{Message, Role};
61///
62/// let msg = Message {
63///     role: Role::Tool,
64///     content: Some(r#"{"temp": 22, "condition": "sunny"}"#.to_string()),
65///     tool_calls: None,
66///     tool_call_id: Some("call_123".to_string()),
67/// };
68/// ```
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Message {
71    /// The role of the message sender.
72    pub role: Role,
73
74    /// The text content of the message.
75    ///
76    /// Can be `None` for assistant messages that only contain tool calls.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub content: Option<String>,
79
80    /// Tool calls made by the assistant.
81    ///
82    /// Only present in assistant messages when the LLM decides to call tools.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub tool_calls: Option<Vec<ToolCall>>,
85
86    /// ID of the tool call this message is responding to.
87    ///
88    /// Only present in tool result messages.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub tool_call_id: Option<String>,
91}
92
93/// The role of a message sender in a conversation.
94///
95/// # Serialization
96///
97/// Roles are serialized as lowercase strings:
98/// - `User` → `"user"`
99/// - `Assistant` → `"assistant"`
100/// - `System` → `"system"`
101/// - `Tool` → `"tool"`
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103#[serde(rename_all = "lowercase")]
104pub enum Role {
105    /// A message from the user.
106    User,
107
108    /// A message from the assistant (LLM).
109    Assistant,
110
111    /// A system message providing instructions or context.
112    System,
113
114    /// A tool result message containing the output of a tool execution.
115    Tool,
116}
117
118/// A tool call made by the assistant.
119///
120/// When an LLM decides to use a tool, it returns a `ToolCall` containing
121/// the tool ID, type, and the function to call with its arguments.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ToolCall {
124    /// Unique identifier for this tool call.
125    pub id: String,
126
127    /// The type of tool (always "function" for now).
128    #[serde(rename = "type")]
129    pub tool_type: String,
130
131    /// The function call details.
132    pub function: FunctionCall,
133}
134
135/// A function call with name and JSON arguments.
136///
137/// The `arguments` field contains a JSON string representing the function
138/// parameters as defined in the tool schema.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct FunctionCall {
141    /// The name of the function to call.
142    pub name: String,
143
144    /// JSON string representation of the function arguments.
145    ///
146    /// This is a string because LLMs return arguments as JSON strings
147    /// during streaming. Parse with `serde_json::from_str` if needed.
148    pub arguments: String,
149}
150
151/// A tool definition for LLM function calling.
152///
153/// Tools allow LLMs to perform actions by calling functions with structured
154/// parameters. Define tools with JSON Schema for the parameters.
155///
156/// # Example
157///
158/// ```
159/// use limit_llm::{Tool, ToolFunction};
160/// use serde_json::json;
161///
162/// let tool = Tool {
163///     tool_type: "function".to_string(),
164///     function: ToolFunction {
165///         name: "get_weather".to_string(),
166///         description: "Get current weather for a location".to_string(),
167///         parameters: json!({
168///             "type": "object",
169///             "properties": {
170///                 "location": {
171///                     "type": "string",
172///                     "description": "City name"
173///                 }
174///             },
175///             "required": ["location"]
176///         }),
177///     },
178/// };
179/// ```
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct Tool {
182    /// The type of tool (always "function" for now).
183    #[serde(rename = "type")]
184    pub tool_type: String,
185
186    /// The function definition.
187    pub function: ToolFunction,
188}
189
190/// Function definition within a tool.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct ToolFunction {
193    /// The function name. Must be unique within the tool set.
194    pub name: String,
195
196    /// Human-readable description of what the function does.
197    /// This helps the LLM understand when to use the tool.
198    pub description: String,
199
200    /// JSON Schema defining the function parameters.
201    ///
202    /// Use `serde_json::json!` to construct the schema inline.
203    pub parameters: serde_json::Value,
204}
205
206/// A complete response from an LLM provider.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct Response {
209    /// The text content of the response.
210    pub content: String,
211
212    /// Tool calls made by the assistant, if any.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub tool_calls: Option<Vec<ToolCall>>,
215
216    /// Token usage statistics.
217    pub usage: Usage,
218}
219
220/// Token usage statistics for a request.
221///
222/// Tracks the number of tokens used in the prompt (input) and
223/// completion (output). Use with [`TrackingDb`](crate::TrackingDb)
224/// to monitor costs across sessions.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct Usage {
227    /// Number of tokens in the prompt/input.
228    pub input_tokens: u64,
229
230    /// Number of tokens in the completion/output.
231    pub output_tokens: u64,
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_message_serialization() {
240        let msg = Message {
241            role: Role::User,
242            content: Some("Hello".to_string()),
243            tool_calls: None,
244            tool_call_id: None,
245        };
246        let json = serde_json::to_string(&msg).unwrap();
247        let deserialized: Message = serde_json::from_str(&json).unwrap();
248        assert_eq!(msg.content, deserialized.content);
249    }
250
251    #[test]
252    fn test_message_with_tool_calls() {
253        let msg = Message {
254            role: Role::Assistant,
255            content: Some("".to_string()),
256            tool_calls: Some(vec![ToolCall {
257                id: "call_123".to_string(),
258                tool_type: "function".to_string(),
259                function: FunctionCall {
260                    name: "test_tool".to_string(),
261                    arguments: serde_json::json!({"arg": "value"}).to_string(),
262                },
263            }]),
264            tool_call_id: None,
265        };
266        let json = serde_json::to_string(&msg).unwrap();
267        let deserialized: Message = serde_json::from_str(&json).unwrap();
268        assert!(deserialized.tool_calls.is_some());
269    }
270
271    #[test]
272    fn test_tool_result_message() {
273        let msg = Message {
274            role: Role::Tool,
275            content: Some("result output".to_string()),
276            tool_calls: None,
277            tool_call_id: Some("call_123".to_string()),
278        };
279        let json = serde_json::to_string(&msg).unwrap();
280        println!("Tool result message JSON: {}", json);
281        assert!(json.contains("tool_call_id"));
282        let deserialized: Message = serde_json::from_str(&json).unwrap();
283        assert_eq!(deserialized.tool_call_id, Some("call_123".to_string()));
284    }
285
286    #[test]
287    fn test_assistant_with_tool_calls_serialization() {
288        let msg = Message {
289            role: Role::Assistant,
290            content: None, // Empty content
291            tool_calls: Some(vec![ToolCall {
292                id: "call_123".to_string(),
293                tool_type: "function".to_string(),
294                function: FunctionCall {
295                    name: "test_tool".to_string(),
296                    arguments: serde_json::json!({}).to_string(),
297                },
298            }]),
299            tool_call_id: None,
300        };
301        let json = serde_json::to_string(&msg).unwrap();
302        println!("Assistant with tool_calls JSON: {}", json);
303        // Content should be omitted when None
304        assert!(!json.contains("\"content\":null"));
305        assert!(json.contains("tool_calls"));
306    }
307
308    #[test]
309    fn test_role_serialization() {
310        let role = Role::User;
311        let json = serde_json::to_string(&role).unwrap();
312        assert_eq!(json, "\"user\"");
313    }
314
315    #[test]
316    fn test_tool_serialization() {
317        let tool = Tool {
318            tool_type: "function".to_string(),
319            function: ToolFunction {
320                name: "test_tool".to_string(),
321                description: "A test tool".to_string(),
322                parameters: serde_json::json!({"type": "object"}),
323            },
324        };
325        let json = serde_json::to_string(&tool).unwrap();
326        let deserialized: Tool = serde_json::from_str(&json).unwrap();
327        assert_eq!(tool.function.name, deserialized.function.name);
328    }
329
330    #[test]
331    fn test_response_serialization() {
332        let response = Response {
333            content: "Hello, world!".to_string(),
334            tool_calls: None,
335            usage: Usage {
336                input_tokens: 10,
337                output_tokens: 5,
338            },
339        };
340        let json = serde_json::to_string(&response).unwrap();
341        let deserialized: Response = serde_json::from_str(&json).unwrap();
342        assert_eq!(response.content, deserialized.content);
343        assert_eq!(response.usage.input_tokens, deserialized.usage.input_tokens);
344    }
345
346    #[test]
347    fn test_usage_serialization() {
348        let usage = Usage {
349            input_tokens: 100,
350            output_tokens: 50,
351        };
352        let json = serde_json::to_string(&usage).unwrap();
353        let deserialized: Usage = serde_json::from_str(&json).unwrap();
354        assert_eq!(usage.input_tokens, deserialized.input_tokens);
355        assert_eq!(usage.output_tokens, deserialized.output_tokens);
356    }
357}