use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use crate::utils::event_stream::AssistantMessageEventStream;
pub type Api = String;
pub type ProviderId = String;
pub type ImagesApi = String;
pub type ImagesProviderId = String;
pub type ProviderEnv = HashMap<String, String>;
pub type ProviderHeaders = HashMap<String, Option<String>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ThinkingLevel {
Minimal,
Low,
Medium,
High,
Xhigh,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelThinkingLevel {
Off,
Level(ThinkingLevel),
}
pub type ThinkingLevelMap = HashMap<String, Option<String>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CacheRetention {
None,
Short,
Long,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Transport {
Sse,
Websocket,
#[serde(rename = "websocket-cached")]
WebsocketCached,
Auto,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThinkingBudgets {
pub minimal: Option<u32>,
pub low: Option<u32>,
pub medium: Option<u32>,
pub high: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct ProviderResponse {
pub status: u16,
pub headers: HashMap<String, String>,
}
#[derive(Clone, Default)]
pub struct StreamOptions {
pub temperature: Option<f64>,
pub max_tokens: Option<u32>,
pub api_key: Option<String>,
pub transport: Option<Transport>,
pub cache_retention: Option<CacheRetention>,
pub session_id: Option<String>,
pub headers: Option<ProviderHeaders>,
pub timeout_ms: Option<u64>,
pub websocket_connect_timeout_ms: Option<u64>,
pub max_retries: Option<u32>,
pub max_retry_delay_ms: Option<u64>,
pub metadata: Option<HashMap<String, Value>>,
pub env: Option<ProviderEnv>,
pub on_payload: Option<OnPayloadCallback>,
pub on_response: Option<OnResponseCallback>,
pub signal: Option<tokio_util::sync::CancellationToken>,
}
pub type OnPayloadCallback =
Arc<dyn Fn(Value, Model) -> Pin<Box<dyn Future<Output = Option<Value>> + Send>> + Send + Sync>;
pub type OnResponseCallback =
Arc<dyn Fn(ProviderResponse, Model) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
#[derive(Clone)]
pub struct SimpleStreamOptions {
pub base: StreamOptions,
pub reasoning: Option<ThinkingLevel>,
pub thinking_budgets: Option<ThinkingBudgets>,
}
impl SimpleStreamOptions {
pub fn from_stream(options: StreamOptions) -> Self {
Self {
base: options,
reasoning: None,
thinking_budgets: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TextContent {
#[serde(rename = "type")]
pub kind: String,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_signature: Option<String>,
}
impl TextContent {
pub fn new(text: impl Into<String>) -> Self {
Self {
kind: "text".to_string(),
text: text.into(),
text_signature: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ThinkingContent {
#[serde(rename = "type")]
pub kind: String,
pub thinking: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking_signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redacted: Option<bool>,
}
impl ThinkingContent {
pub fn new(thinking: impl Into<String>) -> Self {
Self {
kind: "thinking".to_string(),
thinking: thinking.into(),
thinking_signature: None,
redacted: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ImageContent {
#[serde(rename = "type")]
pub kind: String,
pub data: String,
pub mime_type: String,
}
impl ImageContent {
pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self {
kind: "image".to_string(),
data: data.into(),
mime_type: mime_type.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ToolCall {
#[serde(rename = "type")]
pub kind: String,
pub id: String,
pub name: String,
pub arguments: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
}
impl ToolCall {
pub fn new(id: impl Into<String>, name: impl Into<String>, arguments: Value) -> Self {
Self {
kind: "toolCall".to_string(),
id: id.into(),
name: name.into(),
arguments,
thought_signature: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UsageCost {
pub input: f64,
pub output: f64,
pub cache_read: f64,
pub cache_write: f64,
pub total: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Usage {
pub input: u64,
pub output: u64,
pub cache_read: u64,
pub cache_write: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_write_1h: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<u64>,
pub total_tokens: u64,
pub cost: UsageCost,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StopReason {
Stop,
Length,
#[serde(rename = "toolUse")]
ToolUse,
Error,
Aborted,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "role", rename_all = "camelCase")]
pub enum Message {
User {
content: UserContent,
timestamp: i64,
},
Assistant(AssistantMessage),
ToolResult {
tool_call_id: String,
tool_name: String,
content: Vec<ContentBlock>,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<Value>,
is_error: bool,
timestamp: i64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UserContent {
Text(String),
Blocks(Vec<ContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ContentBlock {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image { data: String, mime_type: String },
}
fn assistant_role_default() -> String {
"assistant".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantMessage {
#[serde(skip_serializing, default = "assistant_role_default")]
pub role: String,
pub content: Vec<AssistantContentBlock>,
pub api: Api,
pub provider: ProviderId,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_id: Option<String>,
pub usage: Usage,
pub stop_reason: StopReason,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AssistantContentBlock {
Text(TextContent),
Thinking(ThinkingContent),
ToolCall(ToolCall),
}
impl AssistantMessage {
pub fn empty(model: &Model) -> Self {
Self {
role: "assistant".to_string(),
content: vec![],
api: model.api.clone(),
provider: model.provider.clone(),
model: model.id.clone(),
response_model: None,
response_id: None,
usage: Usage::default(),
stop_reason: StopReason::Stop,
error_message: None,
timestamp: chrono::Utc::now().timestamp_millis(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Tool {
pub name: String,
pub description: String,
pub parameters: Value,
}
#[derive(Debug, Clone)]
pub struct Context {
pub system_prompt: Option<String>,
pub messages: Vec<Message>,
pub tools: Option<Vec<Tool>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextSignatureV1 {
pub v: u8,
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AssistantMessageEvent {
Start {
partial: AssistantMessage,
},
TextStart {
content_index: usize,
partial: AssistantMessage,
},
TextDelta {
content_index: usize,
delta: String,
partial: AssistantMessage,
},
TextEnd {
content_index: usize,
content: String,
partial: AssistantMessage,
},
ThinkingStart {
content_index: usize,
partial: AssistantMessage,
},
ThinkingDelta {
content_index: usize,
delta: String,
partial: AssistantMessage,
},
ThinkingEnd {
content_index: usize,
content: String,
partial: AssistantMessage,
},
ToolcallStart {
content_index: usize,
partial: AssistantMessage,
},
ToolcallDelta {
content_index: usize,
delta: String,
partial: AssistantMessage,
},
ToolcallEnd {
content_index: usize,
tool_call: ToolCall,
partial: AssistantMessage,
},
Done {
reason: StopReason,
message: AssistantMessage,
},
Error {
reason: StopReason,
error: AssistantMessage,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenAICompletionsCompat {
pub supports_store: Option<bool>,
pub supports_developer_role: Option<bool>,
pub supports_reasoning_effort: Option<bool>,
pub supports_usage_in_streaming: Option<bool>,
pub max_tokens_field: Option<String>,
pub requires_tool_result_name: Option<bool>,
pub requires_assistant_after_tool_result: Option<bool>,
pub requires_thinking_as_text: Option<bool>,
pub requires_reasoning_content_on_assistant_messages: Option<bool>,
pub thinking_format: Option<String>,
pub zai_tool_stream: Option<bool>,
pub supports_strict_mode: Option<bool>,
pub cache_control_format: Option<String>,
pub send_session_affinity_headers: Option<bool>,
pub supports_long_cache_retention: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenAIResponsesCompat {
pub supports_developer_role: Option<bool>,
pub send_session_id_header: Option<bool>,
pub supports_long_cache_retention: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnthropicMessagesCompat {
pub supports_eager_tool_input_streaming: Option<bool>,
pub supports_long_cache_retention: Option<bool>,
pub send_session_affinity_headers: Option<bool>,
pub supports_cache_control_on_tools: Option<bool>,
pub supports_temperature: Option<bool>,
pub force_adaptive_thinking: Option<bool>,
pub allow_empty_signature: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct Model {
pub id: String,
pub name: String,
pub api: Api,
pub provider: ProviderId,
pub base_url: String,
pub reasoning: bool,
pub thinking_level_map: Option<ThinkingLevelMap>,
pub input: Vec<String>,
pub cost: ModelCost,
pub context_window: u32,
pub max_tokens: u32,
pub headers: Option<HashMap<String, String>>,
pub openai_completions_compat: Option<OpenAICompletionsCompat>,
pub openai_responses_compat: Option<OpenAIResponsesCompat>,
pub anthropic_compat: Option<AnthropicMessagesCompat>,
}
#[derive(Debug, Clone, Copy)]
pub struct ModelCost {
pub input: f64,
pub output: f64,
pub cache_read: f64,
pub cache_write: f64,
}
#[derive(Debug, Clone)]
pub struct ImagesModel {
pub id: String,
pub name: String,
pub api: ImagesApi,
pub provider: ImagesProviderId,
pub base_url: String,
pub input: Vec<String>,
pub output: Vec<String>,
pub cost: ModelCost,
pub headers: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone)]
pub struct ImagesContext {
pub input: Vec<ContentBlock>,
}
#[derive(Clone)]
pub struct ImagesOptions {
pub api_key: Option<String>,
pub signal: Option<tokio_util::sync::CancellationToken>,
pub env: Option<ProviderEnv>,
pub headers: Option<ProviderHeaders>,
pub timeout_ms: Option<u64>,
pub max_retries: Option<u32>,
pub on_payload: Option<OnPayloadCallback>,
pub on_response: Option<OnResponseCallback>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantImages {
pub api: ImagesApi,
pub provider: ImagesProviderId,
pub model: String,
pub output: Vec<ContentBlock>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
pub stop_reason: StopReason,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
pub timestamp: i64,
}
pub trait ProviderStreams: Send + Sync {
fn stream(&self, model: &Model, context: &Context, options: Option<StreamOptions>) -> AssistantMessageEventStream;
fn stream_simple(
&self,
model: &Model,
context: &Context,
options: Option<SimpleStreamOptions>,
) -> AssistantMessageEventStream;
}
pub trait ProviderImages: Send + Sync {
fn generate_images(
&self,
model: &ImagesModel,
context: &ImagesContext,
options: Option<ImagesOptions>,
) -> Pin<Box<dyn Future<Output = AssistantImages> + Send>>;
}
impl Message {
pub fn role(&self) -> &'static str {
match self {
Message::User { .. } => "user",
Message::Assistant(_) => "assistant",
Message::ToolResult { .. } => "toolResult",
}
}
pub fn as_assistant(&self) -> Option<&AssistantMessage> {
match self {
Message::Assistant(m) => Some(m),
_ => None,
}
}
}
impl AssistantContentBlock {
pub fn is_text(&self) -> bool {
matches!(self, AssistantContentBlock::Text(_))
}
pub fn is_thinking(&self) -> bool {
matches!(self, AssistantContentBlock::Thinking(_))
}
pub fn is_tool_call(&self) -> bool {
matches!(self, AssistantContentBlock::ToolCall(_))
}
}