bamboo_agent/agent/llm/models.rs
1//! LLM API request and response models.
2//!
3//! This module defines the data structures used to communicate with
4//! various LLM providers (OpenAI, Anthropic, etc.) following OpenAI's API format.
5//!
6//! # Key Types
7//!
8//! ## Request Types
9//! - [`ChatCompletionRequest`] - Main request structure
10//! - [`ChatMessage`] - Message in conversation
11//!
12//! ## Content Types
13//! - [`Role`] - Message role (system, user, assistant, tool)
14//! - [`Content`] - Message content (text or parts)
15//! - [`ContentPart`] - Content part (text or image)
16//!
17//! ## Tool Types
18//! - [`Tool`] - Tool definition
19//! - [`ToolChoice`] - Tool selection strategy
20//! - [`ToolCall`] - Tool invocation
21//!
22//! # Example
23//!
24//! ```rust,ignore
25//! use bamboo_agent::agent::llm::models::*;
26//!
27//! let request = ChatCompletionRequest {
28//! model: "gpt-4o-mini".to_string(),
29//! messages: vec![
30//! ChatMessage {
31//! role: Role::User,
32//! content: Content::Text("Hello".to_string()),
33//! tool_calls: None,
34//! tool_call_id: None,
35//! }
36//! ],
37//! tools: None,
38//! tool_choice: None,
39//! stream: Some(true),
40//! stream_options: Some(StreamOptions { include_usage: true }),
41//! parameters: HashMap::new(),
42//! };
43//! ```
44
45use serde::{Deserialize, Serialize};
46use std::collections::HashMap;
47
48// ========== Core Request Body ==========
49
50/// Chat completion request to LLM API.
51///
52/// Main request structure sent to LLM providers to generate
53/// chat completions with optional tool calling support.
54///
55/// # Fields
56///
57/// * `model` - Model identifier (e.g., "gpt-4o-mini", "claude-3-opus")
58/// * `messages` - Conversation history
59/// * `tools` - Available tools for the model
60/// * `tool_choice` - Tool selection strategy
61/// * `stream` - Whether to stream the response
62/// * `stream_options` - Streaming options
63/// * `parameters` - Additional model parameters (temperature, etc.)
64///
65/// # Example
66///
67/// ```rust,ignore
68/// let request = ChatCompletionRequest {
69/// model: "gpt-4o-mini".to_string(),
70/// messages: vec![
71/// ChatMessage::user("What is Rust?"),
72/// ],
73/// stream: Some(true),
74/// ..Default::default()
75/// };
76/// ```
77#[derive(Debug, Serialize, Deserialize, Clone, Default)]
78pub struct ChatCompletionRequest {
79 /// The model to use for the completion.
80 pub model: String,
81 /// A list of messages comprising the conversation so far.
82 pub messages: Vec<ChatMessage>,
83 /// A list of tools the model may call.
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub tools: Option<Vec<Tool>>,
86 /// Controls which function is called by the model.
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub tool_choice: Option<ToolChoice>,
89 /// Whether to stream the response.
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub stream: Option<bool>,
92 /// Options for streaming response. Set `include_usage: true` to receive usage information.
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub stream_options: Option<StreamOptions>,
95 /// Additional parameters like temperature, top_p, etc.
96 #[serde(flatten)]
97 pub parameters: HashMap<String, serde_json::Value>,
98}
99
100/// Options for streaming responses.
101///
102/// # Fields
103///
104/// * `include_usage` - Include token usage in final chunk
105#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
106pub struct StreamOptions {
107 /// If set to true, the streaming response will include a `usage` field in the final chunk.
108 pub include_usage: bool,
109}
110
111// ========== Message and Content Structures ==========
112
113/// A message in the conversation.
114///
115/// Represents one turn in the conversation with role and content.
116///
117/// # Fields
118///
119/// * `role` - Message author role
120/// * `content` - Message contents
121/// * `tool_calls` - Tool calls (for assistant messages)
122/// * `tool_call_id` - Tool call ID (for tool result messages)
123///
124/// # Example
125///
126/// ```rust,ignore
127/// let user_msg = ChatMessage {
128/// role: Role::User,
129/// content: Content::Text("Hello".to_string()),
130/// tool_calls: None,
131/// tool_call_id: None,
132/// };
133///
134/// let tool_result = ChatMessage {
135/// role: Role::Tool,
136/// content: Content::Text("Result".to_string()),
137/// tool_calls: None,
138/// tool_call_id: Some("call-123".to_string()),
139/// };
140/// ```
141#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
142pub struct ChatMessage {
143 /// The role of the message author.
144 pub role: Role,
145 /// The contents of the message.
146 pub content: Content,
147 /// The tool calls generated by the model, if any.
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub tool_calls: Option<Vec<ToolCall>>,
150 /// The ID of the tool call this message is a response to.
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub tool_call_id: Option<String>,
153}
154
155/// Role of a message author.
156///
157/// # Variants
158///
159/// * `System` - System instructions
160/// * `User` - User input
161/// * `Assistant` - AI response
162/// * `Tool` - Tool execution result
163#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
164#[serde(rename_all = "lowercase")]
165pub enum Role {
166 /// System instructions or prompts
167 System,
168 /// User input message
169 User,
170 /// AI assistant response
171 Assistant,
172 /// Tool execution result
173 Tool,
174}
175
176/// Message content.
177///
178/// Can be either plain text or a list of content parts
179/// (for multimodal messages with text and images).
180///
181/// # Variants
182///
183/// * `Text(String)` - Simple text content
184/// * `Parts(Vec<ContentPart>)` - Multiple content parts
185///
186/// # Example
187///
188/// ```rust,ignore
189/// // Simple text
190/// let text = Content::Text("Hello".to_string());
191///
192/// // Multimodal
193/// let parts = Content::Parts(vec![
194/// ContentPart::Text { text: "What's in this image?".to_string() },
195/// ContentPart::ImageUrl { image_url: ImageUrl { url: "...".to_string(), detail: None } },
196/// ]);
197/// ```
198#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
199#[serde(untagged)]
200pub enum Content {
201 /// A single string of text content.
202 Text(String),
203 /// A list of content parts, for complex messages (e.g., with images).
204 Parts(Vec<ContentPart>),
205}
206
207/// A part of message content.
208///
209/// For multimodal messages, content can be text or image URLs.
210///
211/// # Variants
212///
213/// * `Text` - Text content
214/// * `ImageUrl` - Image URL reference
215///
216/// # Example
217///
218/// ```rust,ignore
219/// let text_part = ContentPart::Text {
220/// text: "Describe this image".to_string()
221/// };
222///
223/// let image_part = ContentPart::ImageUrl {
224/// image_url: ImageUrl {
225/// url: "https://example.com/image.png".to_string(),
226/// detail: Some("high".to_string()),
227/// }
228/// };
229/// ```
230#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
231#[serde(tag = "type", rename_all = "snake_case")]
232pub enum ContentPart {
233 /// Text content part
234 Text { text: String },
235 /// Image URL content part
236 ImageUrl { image_url: ImageUrl },
237}
238
239/// Image URL reference.
240///
241/// # Fields
242///
243/// * `url` - Image URL or base64 data URI
244/// * `detail` - Detail level ("low", "high", "auto")
245#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
246pub struct ImageUrl {
247 /// The URL of the image.
248 pub url: String,
249 /// The level of detail to use for the image.
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub detail: Option<String>,
252}
253
254// ========== Tool-Related Structures ==========
255
256/// Tool definition for LLM function calling.
257///
258/// Defines a tool that the model can call during generation.
259///
260/// # Fields
261///
262/// * `tool_type` - Tool type (always "function")
263/// * `function` - Function definition
264///
265/// # Example
266///
267/// ```rust,ignore
268/// let tool = Tool {
269/// tool_type: "function".to_string(),
270/// function: FunctionDefinition {
271/// name: "read_file".to_string(),
272/// description: Some("Read file contents".to_string()),
273/// parameters: json!({
274/// "type": "object",
275/// "properties": {
276/// "path": {"type": "string"}
277/// }
278/// }),
279/// },
280/// };
281/// ```
282#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
283pub struct Tool {
284 /// Tool type (always "function")
285 #[serde(rename = "type")]
286 pub tool_type: String,
287 /// Function definition
288 pub function: FunctionDefinition,
289}
290
291/// Function definition for tool schema.
292///
293/// # Fields
294///
295/// * `name` - Function name
296/// * `description` - Function description
297/// * `parameters` - JSON Schema for parameters
298#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
299pub struct FunctionDefinition {
300 /// Function name
301 pub name: String,
302 /// Function description for the model
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub description: Option<String>,
305 /// JSON Schema for function parameters
306 pub parameters: serde_json::Value, // JSON Schema
307}
308
309/// Tool selection strategy.
310///
311/// Controls which tool (if any) the model should call.
312///
313/// # Variants
314///
315/// * `String(String)` - "none", "auto", or "required"
316/// * `Object` - Specific function to call
317///
318/// # Example
319///
320/// ```rust,ignore
321/// // No tools
322/// let none = ToolChoice::String("none".to_string());
323///
324/// // Automatic selection
325/// let auto = ToolChoice::String("auto".to_string());
326///
327/// // Force specific tool
328/// let specific = ToolChoice::Object {
329/// tool_type: "function".to_string(),
330/// function: FunctionChoice {
331/// name: "read_file".to_string(),
332/// },
333/// };
334/// ```
335#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
336#[serde(untagged)]
337pub enum ToolChoice {
338 /// Tool selection mode: "none", "auto", or "required"
339 String(String),
340 /// Force specific function call
341 Object {
342 /// Tool type (always "function")
343 #[serde(rename = "type")]
344 tool_type: String,
345 /// Function to call
346 function: FunctionChoice,
347 },
348}
349
350/// Specific function choice for tool_choice.
351#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
352pub struct FunctionChoice {
353 /// Function name to call
354 pub name: String,
355}
356
357/// Tool call from the model.
358///
359/// Represents a tool invocation requested by the LLM.
360///
361/// # Fields
362///
363/// * `id` - Unique call identifier
364/// * `tool_type` - Tool type (always "function")
365/// * `function` - Function call details
366#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
367pub struct ToolCall {
368 /// Unique tool call identifier
369 pub id: String,
370 /// Tool type (always "function")
371 #[serde(rename = "type")]
372 pub tool_type: String,
373 /// Function call details
374 pub function: FunctionCall,
375}
376
377/// Function call details.
378///
379/// # Fields
380///
381/// * `name` - Function name
382/// * `arguments` - JSON-encoded arguments
383#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
384pub struct FunctionCall {
385 /// Function name to invoke
386 pub name: String,
387 /// JSON-encoded function arguments
388 pub arguments: String, // JSON string
389}
390
391// ========== Response Structures ==========
392
393/// Chat completion response from LLM API.
394///
395/// # Fields
396///
397/// * `id` - Response ID
398/// * `object` - Object type
399/// * `created` - Creation timestamp
400/// * `model` - Model used
401/// * `choices` - Completion choices
402/// * `usage` - Token usage statistics
403/// * `system_fingerprint` - System fingerprint
404#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
405pub struct ChatCompletionResponse {
406 /// Response identifier
407 pub id: String,
408 /// Object type (e.g., "chat.completion")
409 #[serde(default)]
410 pub object: Option<String>,
411 /// Unix timestamp when response was created
412 #[serde(default)]
413 pub created: Option<u64>,
414 /// Model name used for generation
415 #[serde(default)]
416 pub model: Option<String>,
417 #[serde(default)]
418 pub choices: Vec<ResponseChoice>,
419 #[serde(default)]
420 pub usage: Option<Usage>,
421 #[serde(skip_serializing_if = "Option::is_none")]
422 pub system_fingerprint: Option<String>,
423}
424
425/// A completion choice in the response.
426///
427/// # Fields
428///
429/// * `index` - Choice index
430/// * `message` - Completion message
431/// * `finish_reason` - Reason for finishing
432#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
433pub struct ResponseChoice {
434 #[serde(default)]
435 pub index: u32,
436 pub message: ChatMessage,
437 #[serde(default)]
438 pub finish_reason: Option<String>,
439}
440
441/// Token usage statistics.
442///
443/// # Fields
444///
445/// * `prompt_tokens` - Tokens in prompt
446/// * `completion_tokens` - Tokens in completion
447/// * `total_tokens` - Total tokens used
448#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
449pub struct Usage {
450 /// Tokens in the prompt
451 #[serde(default)]
452 pub prompt_tokens: u32,
453 /// Tokens in the completion
454 #[serde(default)]
455 pub completion_tokens: u32,
456 /// Total tokens used
457 #[serde(default)]
458 pub total_tokens: u32,
459}
460
461// For Stream Responses
462
463/// Streaming chat completion chunk.
464///
465/// Each chunk contains incremental updates during streaming.
466///
467/// # Fields
468///
469/// * `id` - Response ID
470/// * `object` - Object type
471/// * `created` - Creation timestamp
472/// * `model` - Model used
473/// * `choices` - Stream choices with deltas
474/// * `usage` - Usage statistics (final chunk only)
475#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
476pub struct ChatCompletionStreamChunk {
477 pub id: String,
478 #[serde(default)]
479 pub object: Option<String>,
480 pub created: u64,
481 #[serde(default)]
482 pub model: Option<String>,
483 pub choices: Vec<StreamChoice>,
484 /// Usage statistics for the request. Only present if `stream_options.include_usage` was set to true.
485 #[serde(skip_serializing_if = "Option::is_none")]
486 pub usage: Option<Usage>,
487}
488
489/// A choice in a streaming chunk.
490///
491/// # Fields
492///
493/// * `index` - Choice index
494/// * `delta` - Content delta
495/// * `finish_reason` - Reason for finishing (if complete)
496#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
497pub struct StreamChoice {
498 /// Choice index
499 pub index: u32,
500 /// Content delta
501 pub delta: StreamDelta,
502 /// Reason for finishing
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub finish_reason: Option<String>,
505}
506
507// Streaming-specific tool call structures
508// These allow partial data since the API sends tool calls incrementally across multiple chunks
509
510/// Streaming tool call fragment.
511///
512/// During streaming, tool calls are sent incrementally across multiple chunks.
513/// The `index` field identifies which tool call each fragment belongs to.
514///
515/// # Fields
516///
517/// * `index` - Tool call index for reassembly
518/// * `id` - Tool call ID (first chunk only)
519/// * `tool_type` - Tool type (first chunk only)
520/// * `function` - Function call fragment
521#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
522pub struct StreamToolCall {
523 /// Index to identify which tool call this fragment belongs to
524 pub index: u32,
525 /// Tool call ID (only present in the first chunk)
526 #[serde(skip_serializing_if = "Option::is_none")]
527 pub id: Option<String>,
528 /// Tool type (only present in the first chunk)
529 #[serde(rename = "type")]
530 #[serde(skip_serializing_if = "Option::is_none")]
531 pub tool_type: Option<String>,
532 /// Function call data (may be partial)
533 #[serde(skip_serializing_if = "Option::is_none")]
534 pub function: Option<StreamFunctionCall>,
535}
536
537/// Streaming function call fragment.
538///
539/// # Fields
540///
541/// * `name` - Function name (first chunk only)
542/// * `arguments` - Arguments fragment (sent incrementally)
543#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
544pub struct StreamFunctionCall {
545 /// Function name (only present in the first chunk)
546 #[serde(skip_serializing_if = "Option::is_none")]
547 pub name: Option<String>,
548 /// Arguments (sent incrementally across chunks)
549 #[serde(skip_serializing_if = "Option::is_none")]
550 pub arguments: Option<String>,
551}
552
553/// Content delta in a streaming chunk.
554///
555/// Contains incremental content updates during streaming.
556///
557/// # Fields
558///
559/// * `role` - Message role (first chunk only)
560/// * `content` - Text content delta
561/// * `tool_calls` - Tool call fragments
562#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
563pub struct StreamDelta {
564 /// Message role (only present in first chunk)
565 #[serde(skip_serializing_if = "Option::is_none")]
566 pub role: Option<Role>,
567 /// Text content delta
568 #[serde(skip_serializing_if = "Option::is_none")]
569 pub content: Option<String>,
570 /// Tool call fragments
571 #[serde(skip_serializing_if = "Option::is_none")]
572 pub tool_calls: Option<Vec<StreamToolCall>>,
573}