pub mod anthropic;
pub mod openai;
pub mod openai_codex;
pub mod openrouter;
pub mod pricing;
pub mod provider;
pub mod unified;
pub mod usage;
pub mod vertex;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Developer,
#[serde(rename = "tool")]
Tool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageStatus {
#[serde(rename = "in_progress")]
InProgress,
Completed,
Incomplete,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputContent {
InputText {
text: String,
},
InputImage {
image_url: Option<String>,
detail: Option<String>,
},
InputFile {
file_id: Option<String>,
file_data: Option<String>,
filename: Option<String>,
file_url: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OutputContent {
OutputText {
text: String,
#[serde(default)]
annotations: Vec<Annotation>,
},
Refusal {
refusal: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Annotation {
UrlCitation {
start_index: usize,
end_index: usize,
url: String,
title: String,
},
FileCitation {
file_id: String,
filename: String,
index: usize,
},
FilePath {
file_id: String,
index: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputMessage {
#[serde(rename = "type")]
pub type_field: String,
pub role: Role,
pub content: Vec<InputContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<MessageStatus>,
}
impl InputMessage {
pub fn user_text(text: impl Into<String>) -> Self {
Self {
type_field: "message".to_string(),
role: Role::User,
content: vec![InputContent::InputText { text: text.into() }],
id: None,
status: None,
}
}
pub fn system(text: impl Into<String>) -> Self {
Self {
type_field: "message".to_string(),
role: Role::System,
content: vec![InputContent::InputText { text: text.into() }],
id: None,
status: None,
}
}
pub fn assistant_completed(id: String, text: impl Into<String>) -> Self {
Self {
type_field: "message".to_string(),
role: Role::Assistant,
content: vec![InputContent::InputText { text: text.into() }],
id: Some(id),
status: Some(MessageStatus::Completed),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputMessage {
#[serde(rename = "type")]
pub type_field: String,
pub id: String,
pub role: Role,
pub status: MessageStatus,
pub content: Vec<OutputContent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ReasoningContent {
ReasoningText {
text: String,
},
SummaryText {
text: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningItem {
#[serde(rename = "type")]
pub type_field: String,
pub id: String,
#[serde(default)]
pub content: Vec<ReasoningContent>,
#[serde(default)]
pub summary: Vec<ReasoningContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encrypted_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionCallItem {
#[serde(rename = "type")]
pub type_field: String,
pub id: String,
pub call_id: String,
pub name: String,
pub arguments: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<MessageStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionCallOutput {
#[serde(rename = "type")]
pub type_field: String,
pub id: String,
pub call_id: String,
pub output: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<MessageStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponsesOutputItem {
Message(OutputMessage),
Reasoning(ReasoningItem),
FunctionCall(FunctionCallItem),
WebSearchCall {
id: String,
status: String,
},
FileSearchCall {
id: String,
queries: Vec<String>,
status: String,
},
ImageGenerationCall {
id: String,
result: Option<String>,
status: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum InputItem {
Message(InputMessage),
OutputMessage(OutputMessage),
ReasoningItem(ReasoningItem),
FunctionCall(FunctionCallItem),
FunctionCallOutput(FunctionCallOutput),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: Role,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_content_blocks: Option<Vec<crate::llm::unified::UnifiedContentBlock>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_metadata: Option<ToolExecutionMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<MessageStatus>,
}
impl ChatMessage {
pub fn generate_id() -> String {
format!("msg_{}", Uuid::new_v4().simple())
}
pub fn to_input_items(&self) -> Vec<InputItem> {
let mut items = Vec::new();
match self.role {
Role::User | Role::System | Role::Developer => {
if let Some(ref content) = self.content {
items.push(InputItem::Message(InputMessage {
type_field: "message".to_string(),
role: self.role,
content: vec![InputContent::InputText {
text: content.clone(),
}],
id: None,
status: None,
}));
}
}
Role::Assistant => {
let msg_id = self.id.clone().unwrap_or_else(Self::generate_id);
let status = self.status.unwrap_or(MessageStatus::Completed);
if let Some(ref reasoning) = self.reasoning {
items.push(InputItem::ReasoningItem(ReasoningItem {
type_field: "reasoning".to_string(),
id: format!("reasoning_{}", Uuid::new_v4().simple()),
content: vec![ReasoningContent::ReasoningText {
text: reasoning.clone(),
}],
summary: vec![],
encrypted_content: None,
signature: None,
}));
}
if let Some(ref content) = self.content {
items.push(InputItem::Message(InputMessage {
type_field: "message".to_string(),
role: Role::Assistant,
content: vec![InputContent::InputText {
text: content.clone(),
}],
id: Some(msg_id.clone()),
status: Some(status),
}));
}
if let Some(ref tool_calls) = self.tool_calls {
for tc in tool_calls {
items.push(InputItem::FunctionCall(FunctionCallItem {
type_field: "function_call".to_string(),
id: format!("fc_{}", Uuid::new_v4().simple()),
call_id: tc.id.clone(),
name: tc.function.name.clone(),
arguments: tc.function.arguments.clone(),
status: Some(MessageStatus::Completed),
}));
}
}
}
Role::Tool => {
if let Some(ref content) = self.content {
items.push(InputItem::FunctionCallOutput(FunctionCallOutput {
type_field: "function_call_output".to_string(),
id: format!("fc_output_{}", Uuid::new_v4().simple()),
call_id: self.tool_call_id.clone().unwrap_or_default(),
output: content.clone(),
status: Some(MessageStatus::Completed),
}));
}
}
}
items
}
pub fn from_output_items(items: &[ResponsesOutputItem]) -> Vec<Self> {
let mut messages = Vec::new();
let mut current_reasoning: Option<String> = None;
for item in items {
match item {
ResponsesOutputItem::Message(OutputMessage {
id,
role,
status,
content,
..
}) => {
let text = content
.iter()
.filter_map(|c| match c {
OutputContent::OutputText { text, .. } => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
messages.push(ChatMessage {
role: *role,
name: None,
tool_call_id: None,
content: Some(text),
tool_calls: None,
reasoning: current_reasoning.take(),
raw_content_blocks: None,
tool_metadata: None,
timestamp: Some(chrono::Utc::now()),
id: Some(id.clone()),
provider_response_id: None,
status: Some(*status),
});
}
ResponsesOutputItem::Reasoning(ReasoningItem {
content, summary, ..
}) => {
let reasoning_texts: Vec<String> = content
.iter()
.filter_map(|c| match c {
ReasoningContent::ReasoningText { text } => Some(text.clone()),
_ => None,
})
.collect();
let summary_texts: Vec<String> = summary
.iter()
.filter_map(|c| match c {
ReasoningContent::SummaryText { text } => Some(text.clone()),
_ => None,
})
.collect();
let mut full_reasoning = String::new();
if !summary_texts.is_empty() {
full_reasoning.push_str("Summary:\n");
full_reasoning.push_str(&summary_texts.join("\n"));
full_reasoning.push_str("\n\n");
}
if !reasoning_texts.is_empty() {
full_reasoning.push_str(&reasoning_texts.join("\n"));
}
current_reasoning = Some(full_reasoning);
}
ResponsesOutputItem::FunctionCall(FunctionCallItem {
call_id,
name,
arguments,
id,
..
}) => {
messages.push(ChatMessage {
role: Role::Assistant,
name: None,
tool_call_id: None,
content: None,
tool_calls: Some(vec![ToolCall {
id: call_id.clone(),
type_field: "function".to_string(),
function: ToolCallFunction {
name: name.clone(),
arguments: arguments.clone(),
},
}]),
reasoning: current_reasoning.take(),
raw_content_blocks: None,
tool_metadata: None,
timestamp: Some(chrono::Utc::now()),
id: Some(id.clone()),
provider_response_id: None,
status: Some(MessageStatus::Completed),
});
}
_ => {
}
}
}
messages
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSpec {
#[serde(rename = "type")]
pub type_field: String,
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub type_field: String,
pub function: ToolCallFunction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallFunction {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamDelta {
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub tool_calls: Option<Vec<ToolCallDelta>>,
#[serde(default)]
pub finish_reason: Option<String>,
#[serde(default)]
pub reasoning: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallDelta {
#[serde(default)]
pub index: Option<usize>,
pub id: Option<String>,
pub function: Option<ToolCallFunctionDelta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallFunctionDelta {
pub name: Option<String>,
pub arguments: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolExecutionMetadata {
pub success: bool,
pub duration_ms: f64,
pub tool_name: String,
pub arguments: String,
}
pub use provider::{DynamicLlmClient, LlmClient, LlmProvider, ProviderFailureCapture};
pub use unified::{
DocumentSource, ImageSource, StopReason, UnifiedContentBlock, UnifiedMessage, UnifiedRole,
UnifiedTool, UnifiedToolCall, UnifiedUsage,
};