pub mod anthropic;
pub mod fallback;
pub mod openai_compatible;
pub mod registry;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::borrow::Cow;
#[derive(Debug, Clone)]
pub struct ToolSpec {
pub name: Cow<'static, str>,
pub description: Cow<'static, str>,
pub input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub input: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ContentPart {
Text(String),
Image {
media_type: String,
data_base64: String,
},
ImageRef {
media_type: String,
sha256: String,
},
ToolUse {
id: String,
name: String,
input: Value,
},
ToolResult {
tool_use_id: String,
content: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum UserInputKind {
Text,
Voice,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: Role,
pub parts: Vec<ContentPart>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_kind: Option<UserInputKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
}
impl ChatMessage {
pub fn user(text: impl Into<String>) -> Self {
Self {
role: Role::User,
parts: vec![ContentPart::Text(text.into())],
input_kind: Some(UserInputKind::Text),
user_id: None,
}
}
pub fn user_voice(text: impl Into<String>) -> Self {
Self {
role: Role::User,
parts: vec![ContentPart::Text(text.into())],
input_kind: Some(UserInputKind::Voice),
user_id: None,
}
}
pub fn user_with_images(
text: impl Into<String>,
images: impl IntoIterator<Item = (String, String)>,
) -> Self {
let mut parts: Vec<ContentPart> = images
.into_iter()
.map(|(media_type, data_base64)| ContentPart::Image {
media_type,
data_base64,
})
.collect();
let text = text.into();
if !text.is_empty() {
parts.push(ContentPart::Text(text));
}
Self {
role: Role::User,
parts,
input_kind: Some(UserInputKind::Text),
user_id: None,
}
}
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
parts: vec![ContentPart::Text(text.into())],
input_kind: None,
user_id: None,
}
}
pub fn assistant_with_tools(text: Option<String>, tool_calls: Vec<ToolCall>) -> Self {
let mut parts = Vec::new();
if let Some(t) = text.filter(|s| !s.is_empty()) {
parts.push(ContentPart::Text(t));
}
for call in tool_calls {
parts.push(ContentPart::ToolUse {
id: call.id,
name: call.name,
input: call.input,
});
}
Self {
role: Role::Assistant,
parts,
input_kind: None,
user_id: None,
}
}
pub fn tool_results_with_images(
results: Vec<(String, String)>,
images: Vec<(String, String)>,
) -> Self {
let mut parts: Vec<ContentPart> = results
.into_iter()
.map(|(id, content)| ContentPart::ToolResult {
tool_use_id: id,
content,
})
.collect();
parts.extend(
images
.into_iter()
.map(|(media_type, data_base64)| ContentPart::Image {
media_type,
data_base64,
}),
);
Self {
role: Role::User,
parts,
input_kind: None,
user_id: None,
}
}
pub fn text(&self) -> Option<String> {
let texts: Vec<&str> = self
.parts
.iter()
.filter_map(|p| {
if let ContentPart::Text(t) = p {
Some(t.as_str())
} else {
None
}
})
.collect();
if texts.is_empty() {
None
} else {
Some(texts.join(""))
}
}
}
#[derive(Debug, Clone)]
pub struct ChatResponse {
pub text: Option<String>,
pub tool_calls: Vec<ToolCall>,
pub stop_reason: Option<String>,
}
impl ChatResponse {
#[allow(dead_code)]
pub fn text_only(text: String) -> Self {
Self {
text: Some(text),
tool_calls: vec![],
stop_reason: None,
}
}
pub fn has_tool_calls(&self) -> bool {
!self.tool_calls.is_empty()
}
#[allow(dead_code)]
pub fn text_or_empty(&self) -> &str {
self.text.as_deref().unwrap_or("")
}
}
#[async_trait]
pub trait Provider: Send + Sync {
fn name(&self) -> &str;
async fn chat(
&self,
system: Option<&str>,
messages: &[ChatMessage],
tools: Option<&[ToolSpec]>,
) -> anyhow::Result<ChatResponse>;
}