pub mod prompt;
pub mod message;
pub mod model_selector;
pub use prompt::*;
pub use message::*;
pub use model_selector::*;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub type MessageId = Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MessageRole {
User,
Assistant,
System,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageContent {
Text(String),
Code {
language: String,
code: String
},
Terminal {
command: String,
output: String
},
Image {
url: String,
alt: String,
width: Option<u32>,
height: Option<u32>,
},
File {
path: String,
name: String,
size: u64,
mime_type: Option<String>,
},
Thinking(String),
Error(String),
Tool {
name: String,
input: String,
output: String
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub id: MessageId,
pub role: MessageRole,
pub content: Vec<MessageContent>,
pub timestamp: DateTime<Utc>,
pub is_streaming: bool,
pub metadata: Option<serde_json::Value>,
}
impl Message {
pub fn new_user(text: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
role: MessageRole::User,
content: vec![MessageContent::Text(text.into())],
timestamp: Utc::now(),
is_streaming: false,
metadata: None,
}
}
pub fn new_assistant(content: Vec<MessageContent>) -> Self {
Self {
id: Uuid::new_v4(),
role: MessageRole::Assistant,
content,
timestamp: Utc::now(),
is_streaming: false,
metadata: None,
}
}
pub fn new_system(text: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
role: MessageRole::System,
content: vec![MessageContent::Text(text.into())],
timestamp: Utc::now(),
is_streaming: false,
metadata: None,
}
}
pub fn get_text(&self) -> String {
self.content
.iter()
.filter_map(|content| match content {
MessageContent::Text(text) => Some(text.clone()),
MessageContent::Thinking(text) => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn has_code(&self) -> bool {
self.content.iter().any(|content| matches!(content, MessageContent::Code { .. }))
}
pub fn has_attachments(&self) -> bool {
self.content.iter().any(|content| {
matches!(content, MessageContent::Image { .. } | MessageContent::File { .. })
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
pub id: String,
pub name: String,
pub description: Option<String>,
pub provider: String,
pub context_length: Option<u32>,
pub capabilities: Vec<ModelCapability>,
pub pricing: Option<PricingInfo>,
}
impl ModelInfo {
pub fn default_models_from_llm_link() -> Vec<Self> {
use llm_link::models::ModelsConfig;
let mut models = Vec::new();
let models_config = ModelsConfig::load_with_fallback();
let provider_ids = models_config.get_all_providers();
for provider_id in provider_ids {
let provider_models = models_config.get_models_for_provider(&provider_id);
let provider_name = Self::format_provider_name(&provider_id);
for model_info in provider_models {
let model = ModelInfo {
id: model_info.id.clone(),
name: model_info.name.clone(),
description: Some(model_info.description.clone()), provider: provider_name.clone(),
context_length: None, capabilities: Self::infer_capabilities_from_provider(&provider_id),
pricing: None, };
models.push(model);
}
}
models
}
fn format_provider_name(provider_id: &str) -> String {
match provider_id {
"openai" => "OpenAI".to_string(),
"anthropic" => "Anthropic".to_string(),
"zhipu" => "Zhipu".to_string(),
"aliyun" => "Aliyun".to_string(),
"volcengine" => "Volcengine".to_string(),
"tencent" => "Tencent".to_string(),
"longcat" => "Longcat".to_string(),
"moonshot" => "Moonshot".to_string(),
"ollama" => "Ollama".to_string(),
_ => {
let mut chars = provider_id.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
}
}
fn infer_capabilities_from_provider(provider: &str) -> Vec<ModelCapability> {
let mut capabilities = vec![
ModelCapability::TextGeneration,
ModelCapability::CodeGeneration,
];
match provider {
"openai" | "anthropic" | "zhipu" | "moonshot" => {
capabilities.push(ModelCapability::FunctionCalling);
}
_ => {}
}
capabilities
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ModelCapability {
TextGeneration,
CodeGeneration,
ImageGeneration,
ImageAnalysis,
FunctionCalling,
DocumentAnalysis,
WebSearch,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PricingInfo {
pub input_price: f64,
pub output_price: f64,
pub currency: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderInfo {
pub id: String,
pub name: String,
pub models: Vec<ModelInfo>,
pub endpoint: Option<String>,
pub requires_auth: bool,
}
impl ProviderInfo {
pub fn all_from_llm_link() -> Vec<Self> {
use llm_link::models::ModelsConfig;
let mut providers = Vec::new();
let models_config = ModelsConfig::load_with_fallback();
let provider_ids = models_config.get_all_providers();
for provider_id in provider_ids {
let provider_models = models_config.get_models_for_provider(&provider_id);
let models: Vec<ModelInfo> = provider_models
.into_iter()
.map(|model_info| {
let provider_name = ModelInfo::format_provider_name(&provider_id);
ModelInfo {
id: model_info.id.clone(),
name: model_info.name.clone(),
description: Some(model_info.description.clone()),
provider: provider_name.clone(),
context_length: None,
capabilities: ModelInfo::infer_capabilities_from_provider(&provider_id),
pricing: None,
}
})
.collect();
let provider_name = ModelInfo::format_provider_name(&provider_id);
let provider_info = ProviderInfo {
id: provider_id.clone(),
name: provider_name,
models,
endpoint: None, requires_auth: true, };
providers.push(provider_info);
}
providers
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attachment {
pub id: String,
pub name: String,
pub path: String,
pub size: u64,
pub mime_type: String,
pub attachment_type: AttachmentType,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AttachmentType {
Image,
Document,
Code,
Audio,
Video,
Other,
}
impl Attachment {
pub fn new(
name: impl Into<String>,
path: impl Into<String>,
size: u64,
mime_type: impl Into<String>,
) -> Self {
let mime_type = mime_type.into();
let attachment_type = AttachmentType::from_mime_type(&mime_type);
Self {
id: Uuid::new_v4().to_string(),
name: name.into(),
path: path.into(),
size,
mime_type,
attachment_type,
}
}
}
impl AttachmentType {
pub fn from_mime_type(mime_type: &str) -> Self {
match mime_type.split('/').next() {
Some("image") => Self::Image,
Some("audio") => Self::Audio,
Some("video") => Self::Video,
Some("text") => Self::Code,
Some("application") => {
if mime_type.contains("pdf") || mime_type.contains("document") {
Self::Document
} else {
Self::Other
}
}
_ => Self::Other,
}
}
}