use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
use crate::{LlmError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ImageSource {
Url { url: String },
Base64 { media_type: String, data: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Part {
Text {
text: String,
},
Image {
source: ImageSource,
},
Reasoning {
text: String,
#[serde(default)]
raw: Value,
},
ToolCall {
id: String,
name: String,
arguments: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
ToolResult {
call_id: String,
content: String,
is_error: bool,
},
Other {
raw: Value,
},
}
impl Part {
pub fn text(text: impl Into<String>) -> Self {
Part::Text { text: text.into() }
}
pub fn image_url(url: impl Into<String>) -> Self {
Part::Image {
source: ImageSource::Url { url: url.into() },
}
}
pub fn image_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
Part::Image {
source: ImageSource::Base64 {
media_type: media_type.into(),
data: data.into(),
},
}
}
pub fn tool_result(
call_id: impl Into<String>,
content: impl Into<String>,
is_error: bool,
) -> Self {
Part::ToolResult {
call_id: call_id.into(),
content: content.into(),
is_error,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub parts: Vec<Part>,
}
impl Message {
pub fn new(role: Role, parts: Vec<Part>) -> Self {
Self { role, parts }
}
pub fn user(text: impl Into<String>) -> Self {
Self::text(Role::User, text)
}
pub fn user_with_image(text: impl Into<String>, image: Part) -> Self {
Self::new(Role::User, vec![Part::text(text), image])
}
pub fn assistant(text: impl Into<String>) -> Self {
Self::text(Role::Assistant, text)
}
pub fn text(role: Role, text: impl Into<String>) -> Self {
Self::new(role, vec![Part::text(text)])
}
pub fn tool_result(
call_id: impl Into<String>,
content: impl Into<String>,
is_error: bool,
) -> Self {
Self::new(
Role::Tool,
vec![Part::tool_result(call_id, content, is_error)],
)
}
pub fn text_content(&self) -> String {
self.parts
.iter()
.filter_map(|p| match p {
Part::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("")
}
pub(crate) fn has_images(&self) -> bool {
self.parts.iter().any(|p| matches!(p, Part::Image { .. }))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
pub description: String,
pub parameters: Value,
}
impl Tool {
pub fn new(name: impl Into<String>, description: impl Into<String>, parameters: Value) -> Self {
Self {
name: name.into(),
description: description.into(),
parameters,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: Value,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ResponseFormat {
pub name: String,
pub schema: Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReasoningEffort {
Low,
Medium,
High,
}
impl ReasoningEffort {
pub(crate) fn as_str(self) -> &'static str {
match self {
ReasoningEffort::Low => "low",
ReasoningEffort::Medium => "medium",
ReasoningEffort::High => "high",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ChatRequest {
pub model: String,
pub system: Option<String>,
pub messages: Vec<Message>,
pub tools: Vec<Tool>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub response_format: Option<ResponseFormat>,
pub reasoning: Option<ReasoningEffort>,
}
pub const DEFAULT_MAX_TOKENS: u32 = 4096;
impl ChatRequest {
pub fn new(model: impl Into<String>) -> Self {
Self {
model: model.into(),
..Default::default()
}
}
pub fn system(mut self, system: impl Into<String>) -> Self {
self.system = Some(system.into());
self
}
pub fn message(mut self, message: Message) -> Self {
self.messages.push(message);
self
}
pub fn tool(mut self, tool: Tool) -> Self {
self.tools.push(tool);
self
}
pub fn max_tokens(mut self, max_tokens: u32) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
pub fn json_schema(mut self, name: impl Into<String>, schema: Value) -> Self {
self.response_format = Some(ResponseFormat {
name: name.into(),
schema,
});
self
}
pub fn reasoning(mut self, effort: ReasoningEffort) -> Self {
self.reasoning = Some(effort);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StopReason {
EndTurn,
ToolUse,
MaxTokens,
Paused,
Refusal,
Other(String),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
}
impl Usage {
pub fn cost(&self, input_per_million: f64, output_per_million: f64) -> f64 {
(f64::from(self.input_tokens) * input_per_million
+ f64::from(self.output_tokens) * output_per_million)
/ 1_000_000.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChatResponse {
pub model: String,
pub message: Message,
pub stop_reason: StopReason,
pub usage: Usage,
}
impl ChatResponse {
pub fn text(&self) -> String {
self.message.text_content()
}
pub fn reasoning(&self) -> String {
self.message
.parts
.iter()
.filter_map(|p| match p {
Part::Reasoning { text, .. } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("")
}
pub fn tool_calls(&self) -> Vec<ToolCall> {
self.message
.parts
.iter()
.filter_map(|p| match p {
Part::ToolCall {
id,
name,
arguments,
..
} => Some(ToolCall {
id: id.clone(),
name: name.clone(),
arguments: arguments.clone(),
}),
_ => None,
})
.collect()
}
pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
let text = self.text();
serde_json::from_str(&text).map_err(|source| LlmError::Deserialize {
source,
content: crate::error::truncate(&text),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelInfo {
pub id: String,
pub display_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Embeddings {
pub vectors: Vec<Vec<f32>>,
pub usage: Usage,
}