pub mod deepseek;
mod openai;
use crate::errors::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: MessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
impl MessageContent {
pub fn as_text(&self) -> String {
match self {
MessageContent::Text(s) => s.clone(),
MessageContent::Parts(parts) => parts
.iter()
.filter_map(|p| match p {
ContentPart::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text {
text: String,
},
ToolUse {
id: String,
name: String,
input: serde_json::Value,
},
ToolResult {
tool_use_id: String,
content: String,
},
}
impl Message {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: Role::System,
content: MessageContent::Text(content.into()),
tool_call_id: None,
name: None,
reasoning_content: None,
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: MessageContent::Text(content.into()),
tool_call_id: None,
name: None,
reasoning_content: None,
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: MessageContent::Text(content.into()),
tool_call_id: None,
name: None,
reasoning_content: None,
}
}
pub fn tool_result(
tool_call_id: impl Into<String>,
name: impl Into<String>,
result: impl Into<String>,
) -> Self {
Self {
role: Role::Tool,
content: MessageContent::Text(result.into()),
tool_call_id: Some(tool_call_id.into()),
name: Some(name.into()),
reasoning_content: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDef {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone)]
pub struct LlmResponse {
pub text: Option<String>,
pub tool_calls: Vec<ToolCall>,
pub tokens_used: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub reasoning_tokens: u64,
pub reasoning_content: Option<String>,
pub stop_reason: StopReason,
}
#[derive(Debug, Clone, PartialEq)]
pub enum StopReason {
EndTurn,
ToolUse,
MaxTokens,
Stop,
}
#[async_trait]
pub trait LlmProvider: Send + Sync {
async fn chat(&self, messages: &[Message], tools: &[ToolDef]) -> Result<LlmResponse>;
async fn chat_streaming(
&self,
messages: &[Message],
tools: &[ToolDef],
token_tx: &tokio::sync::mpsc::UnboundedSender<String>,
) -> Result<LlmResponse> {
let resp = self.chat(messages, tools).await?;
if let Some(ref text) = resp.text {
if !text.is_empty() {
let _ = token_tx.send(text.clone());
}
}
Ok(resp)
}
fn supports_streaming(&self) -> bool {
false
}
fn name(&self) -> &str;
fn context_window(&self) -> u64;
fn default_model(&self) -> &str;
}
pub async fn with_retry<F, Fut>(
provider_name: &str,
max_attempts: u32,
mut f: F,
) -> Result<LlmResponse>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<LlmResponse>>,
{
use crate::errors::RalphError;
let mut delay = std::time::Duration::from_millis(500);
for attempt in 1..=max_attempts {
match f().await {
Ok(r) => return Ok(r),
Err(RalphError::LlmRateLimit { .. }) | Err(RalphError::LlmApi { .. })
if attempt < max_attempts =>
{
tokio::time::sleep(delay).await;
delay *= 2;
}
Err(e) => return Err(e),
}
}
Err(RalphError::LlmRateLimit {
provider: provider_name.to_string(),
attempts: max_attempts,
})
}