camel_component_llm/provider/
mod.rs1pub mod mock;
7#[cfg(any(feature = "openai", feature = "ollama", feature = "all-providers"))]
8pub mod siumai_adapter;
9
10use async_trait::async_trait;
11use futures::stream::BoxStream;
12
13use crate::error::LlmError;
14
15#[async_trait]
20pub trait LlmProvider: Send + Sync {
21 fn id(&self) -> &str;
23
24 fn default_model(&self) -> &str;
26
27 fn chat_stream(&self, req: ChatRequest) -> BoxStream<'static, Result<ChatEvent, LlmError>>;
29
30 async fn embed(&self, req: EmbedRequest) -> Result<EmbedResponse, LlmError>;
32
33 fn supports_embed(&self) -> bool {
35 true
36 }
37}
38
39#[derive(Debug, Clone)]
41#[non_exhaustive]
42pub enum ChatEvent {
43 Delta {
45 text: String,
47 },
48 Finished {
50 usage: Option<LlmUsage>,
52 model: Option<String>,
54 finish_reason: Option<FinishReason>,
56 metadata: serde_json::Map<String, serde_json::Value>,
58 },
59 ToolCall {
61 id: String,
63 name: String,
65 arguments: String,
67 },
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub struct LlmUsage {
73 pub prompt_tokens: u32,
75 pub completion_tokens: u32,
77 pub total_tokens: u32,
79}
80
81#[derive(Debug, Clone)]
83pub enum FinishReason {
84 Stop,
86 Length,
88 ToolCall,
90 ContentFilter,
92 Other(String),
94}
95
96#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
98pub struct ToolDefinition {
99 pub name: String,
101 pub description: String,
103 pub parameters: serde_json::Map<String, serde_json::Value>,
105}
106
107#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
109pub enum ToolChoice {
110 Auto,
112 None,
114 Specific(String),
116}
117
118#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
120pub struct EmittedToolCall {
121 pub id: String,
123 pub name: String,
125 pub arguments: String,
127}
128
129#[derive(Debug, Clone)]
131pub struct ChatRequest {
132 pub model: String,
134 pub messages: Vec<ChatMessage>,
136 pub temperature: Option<f64>,
138 pub max_tokens: Option<u32>,
140 pub stop: Option<Vec<String>>,
142 pub system_prompt: Option<String>,
144 pub tools: Vec<ToolDefinition>,
146 pub tool_choice: Option<ToolChoice>,
148 pub extra: serde_json::Map<String, serde_json::Value>,
150}
151
152impl ChatRequest {
153 pub fn new(model: impl Into<String>, messages: Vec<ChatMessage>) -> Self {
155 Self {
156 model: model.into(),
157 messages,
158 temperature: None,
159 max_tokens: None,
160 stop: None,
161 system_prompt: None,
162 tools: Vec::new(),
163 tool_choice: None,
164 extra: serde_json::Map::new(),
165 }
166 }
167}
168
169#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
171pub struct ChatMessage {
172 pub role: ChatRole,
174 pub content: String,
176 pub tool_calls: Option<Vec<EmittedToolCall>>,
178}
179
180impl ChatMessage {
181 pub fn user(content: impl Into<String>) -> Self {
183 Self {
184 role: ChatRole::User,
185 content: content.into(),
186 tool_calls: None,
187 }
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
193#[non_exhaustive]
194pub enum ChatRole {
195 System,
197 User,
199 Assistant,
201 Tool {
203 tool_call_id: String,
205 },
206}
207
208#[derive(Debug, Clone)]
210pub struct EmbedRequest {
211 pub model: String,
213 pub inputs: Vec<String>,
215 pub extra: serde_json::Map<String, serde_json::Value>,
217}
218
219impl EmbedRequest {
220 pub fn new(model: impl Into<String>, inputs: Vec<String>) -> Self {
222 Self {
223 model: model.into(),
224 inputs,
225 extra: serde_json::Map::new(),
226 }
227 }
228}
229
230#[derive(Debug, Clone)]
232pub struct EmbedResponse {
233 pub embeddings: Vec<Vec<f32>>,
235 pub usage: Option<LlmUsage>,
237 pub model: String,
239 pub metadata: serde_json::Map<String, serde_json::Value>,
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn chat_request_builder() {
249 let req = ChatRequest::new("gpt-4o", vec![ChatMessage::user("hello")]);
250 assert_eq!(req.model, "gpt-4o");
251 assert_eq!(req.messages.len(), 1);
252 assert_eq!(req.messages[0].role, ChatRole::User);
253 }
254
255 #[test]
256 fn chat_request_accepts_tools() {
257 let tool = ToolDefinition {
258 name: "get_weather".into(),
259 description: "Get weather for a city".into(),
260 parameters: serde_json::Map::new(),
261 };
262 let req = ChatRequest {
263 model: "gpt-4o".into(),
264 messages: vec![ChatMessage::user("what's the weather?")],
265 temperature: None,
266 max_tokens: None,
267 stop: None,
268 system_prompt: None,
269 extra: serde_json::Map::new(),
270 tools: vec![tool],
271 tool_choice: Some(ToolChoice::Auto),
272 };
273 assert_eq!(req.tools.len(), 1);
274 assert_eq!(req.tools[0].name, "get_weather");
275 assert_eq!(req.tool_choice, Some(ToolChoice::Auto));
276 }
277
278 #[test]
279 fn tool_message_carries_tool_call_id() {
280 let msg = ChatMessage {
281 role: ChatRole::Tool {
282 tool_call_id: "call_123".into(),
283 },
284 content: "weather result".into(),
285 tool_calls: None,
286 };
287 match &msg.role {
288 ChatRole::Tool { tool_call_id } => assert_eq!(tool_call_id, "call_123"),
289 _ => panic!("expected Tool role"),
290 }
291 }
292
293 #[test]
294 fn assistant_message_carries_prior_tool_calls() {
295 let tool_call = EmittedToolCall {
296 id: "call_123".into(),
297 name: "get_weather".into(),
298 arguments: r#"{"city":"London"}"#.into(),
299 };
300 let msg = ChatMessage {
301 role: ChatRole::Assistant,
302 content: "I'll check the weather".into(),
303 tool_calls: Some(vec![tool_call]),
304 };
305 assert_eq!(msg.tool_calls.as_ref().unwrap().len(), 1);
306 assert_eq!(msg.tool_calls.as_ref().unwrap()[0].id, "call_123");
307 }
308}