1pub mod deepseek;
2mod openai;
3
4use crate::errors::Result;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11#[serde(rename_all = "lowercase")]
12pub enum Role {
13 System,
14 User,
15 Assistant,
16 Tool,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Message {
21 pub role: Role,
22 pub content: MessageContent,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub tool_call_id: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub name: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub reasoning_content: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(untagged)]
36pub enum MessageContent {
37 Text(String),
38 Parts(Vec<ContentPart>),
39}
40
41impl MessageContent {
42 pub fn as_text(&self) -> String {
43 match self {
44 MessageContent::Text(s) => s.clone(),
45 MessageContent::Parts(parts) => parts
46 .iter()
47 .filter_map(|p| match p {
48 ContentPart::Text { text } => Some(text.as_str()),
49 _ => None,
50 })
51 .collect::<Vec<_>>()
52 .join("\n"),
53 }
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(tag = "type", rename_all = "snake_case")]
59pub enum ContentPart {
60 Text {
61 text: String,
62 },
63 ToolUse {
64 id: String,
65 name: String,
66 input: serde_json::Value,
67 },
68 ToolResult {
69 tool_use_id: String,
70 content: String,
71 },
72}
73
74impl Message {
75 pub fn system(content: impl Into<String>) -> Self {
76 Self {
77 role: Role::System,
78 content: MessageContent::Text(content.into()),
79 tool_call_id: None,
80 name: None,
81 reasoning_content: None,
82 }
83 }
84
85 pub fn user(content: impl Into<String>) -> Self {
86 Self {
87 role: Role::User,
88 content: MessageContent::Text(content.into()),
89 tool_call_id: None,
90 name: None,
91 reasoning_content: None,
92 }
93 }
94
95 pub fn assistant(content: impl Into<String>) -> Self {
96 Self {
97 role: Role::Assistant,
98 content: MessageContent::Text(content.into()),
99 tool_call_id: None,
100 name: None,
101 reasoning_content: None,
102 }
103 }
104
105 pub fn tool_result(
106 tool_call_id: impl Into<String>,
107 name: impl Into<String>,
108 result: impl Into<String>,
109 ) -> Self {
110 Self {
111 role: Role::Tool,
112 content: MessageContent::Text(result.into()),
113 tool_call_id: Some(tool_call_id.into()),
114 name: Some(name.into()),
115 reasoning_content: None,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ToolDef {
124 pub name: String,
125 pub description: String,
126 pub parameters: serde_json::Value,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ToolCall {
133 pub id: String,
134 pub name: String,
135 pub arguments: serde_json::Value,
136}
137
138#[derive(Debug, Clone)]
141pub struct LlmResponse {
142 pub text: Option<String>,
144 pub tool_calls: Vec<ToolCall>,
146 pub tokens_used: u64,
148 pub input_tokens: u64,
150 pub output_tokens: u64,
152 pub reasoning_tokens: u64,
154 pub reasoning_content: Option<String>,
156 pub stop_reason: StopReason,
158}
159
160#[derive(Debug, Clone, PartialEq)]
161pub enum StopReason {
162 EndTurn,
163 ToolUse,
164 MaxTokens,
165 Stop,
166}
167
168#[async_trait]
171pub trait LlmProvider: Send + Sync {
172 async fn chat(&self, messages: &[Message], tools: &[ToolDef]) -> Result<LlmResponse>;
174
175 async fn chat_streaming(
178 &self,
179 messages: &[Message],
180 tools: &[ToolDef],
181 token_tx: &tokio::sync::mpsc::UnboundedSender<String>,
182 ) -> Result<LlmResponse> {
183 let resp = self.chat(messages, tools).await?;
184 if let Some(ref text) = resp.text {
185 if !text.is_empty() {
186 let _ = token_tx.send(text.clone());
187 }
188 }
189 Ok(resp)
190 }
191
192 fn supports_streaming(&self) -> bool {
195 false
196 }
197
198 fn name(&self) -> &str;
200
201 fn context_window(&self) -> u64;
203
204 fn default_model(&self) -> &str;
206}
207
208pub async fn with_retry<F, Fut>(
211 provider_name: &str,
212 max_attempts: u32,
213 mut f: F,
214) -> Result<LlmResponse>
215where
216 F: FnMut() -> Fut,
217 Fut: std::future::Future<Output = Result<LlmResponse>>,
218{
219 use crate::errors::RalphError;
220 let mut delay = std::time::Duration::from_millis(500);
221 for attempt in 1..=max_attempts {
222 match f().await {
223 Ok(r) => return Ok(r),
224 Err(RalphError::LlmRateLimit { .. }) | Err(RalphError::LlmApi { .. })
225 if attempt < max_attempts =>
226 {
227 tokio::time::sleep(delay).await;
228 delay *= 2;
229 }
230 Err(e) => return Err(e),
231 }
232 }
233 Err(RalphError::LlmRateLimit {
234 provider: provider_name.to_string(),
235 attempts: max_attempts,
236 })
237}