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}