use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::skip_serializing_none;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MediaType {
Image,
Document,
Text,
Binary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum Part {
Text {
content: String,
#[serde(default)]
finished: bool,
},
Reasoning {
content: String,
summary: Option<String>,
signature: Option<String>,
#[serde(default)]
finished: bool,
},
FunctionCall {
id: Option<String>,
name: String,
arguments: Value,
signature: Option<String>,
#[serde(default)]
finished: bool,
},
FunctionResponse {
id: Option<String>,
name: String,
response: Value,
parts: Vec<Part>,
#[serde(default)]
finished: bool,
},
Media {
media_type: MediaType,
data: String,
mime_type: String,
#[serde(default)]
uri: Option<String>,
#[serde(default)]
finished: bool,
},
}
impl Part {
pub fn anchor_media(&self) -> String {
match self {
Part::Media { mime_type, uri, .. } => {
let uri_str = uri.as_deref().unwrap_or("unknown");
format!("File ({}) at {}:", mime_type, uri_str)
}
_ => panic!("anchor_media called on non-Media part"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "role", content = "content")]
pub enum Message {
#[serde(rename = "user")]
User(Vec<Part>),
#[serde(rename = "assistant")]
Assistant(Vec<Part>),
}
impl Message {
pub fn role(&self) -> Role {
match self {
Message::User(_) => Role::User,
Message::Assistant(_) => Role::Assistant,
}
}
pub fn parts(&self) -> &Vec<Part> {
match self {
Message::User(parts) => parts,
Message::Assistant(parts) => parts,
}
}
pub fn parts_mut(&mut self) -> &mut Vec<Part> {
match self {
Message::User(parts) => parts,
Message::Assistant(parts) => parts,
}
}
pub fn content(&self) -> Option<String> {
let parts = self.parts();
let text_parts: Vec<&str> = parts
.iter()
.filter_map(|p| match p {
Part::Text { content: text, .. } => Some(text.as_str()),
Part::Reasoning { content, .. } => Some(content.as_str()),
_ => None,
})
.collect();
if text_parts.is_empty() {
None
} else {
Some(text_parts.join("\n"))
}
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneralRequest {
pub model: String,
pub history: Vec<Message>,
pub instructions: Option<String>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum FinishReason {
Stop,
PromptTokens,
OutputTokens,
ToolCalls,
ContentFilter,
Error,
Unfinished,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Usage {
pub prompt_tokens: Option<u32>,
pub completion_tokens: Option<u32>,
}
impl std::ops::Add for Usage {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
prompt_tokens: self
.prompt_tokens
.map(|v| v + other.prompt_tokens.unwrap_or(0))
.or(other.prompt_tokens),
completion_tokens: self
.completion_tokens
.map(|v| v + other.completion_tokens.unwrap_or(0))
.or(other.completion_tokens),
}
}
}
impl std::ops::AddAssign for Usage {
fn add_assign(&mut self, other: Self) {
*self = self.clone() + other;
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub data: Vec<Message>,
pub usage: Usage,
pub finish: FinishReason,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_anchor_media() {
let part = Part::Media {
media_type: MediaType::Document,
data: "base64data".to_string(),
mime_type: "application/pdf".to_string(),
uri: Some("file:///path/to/doc.pdf".to_string()),
finished: true,
};
assert_eq!(
part.anchor_media(),
"File (application/pdf) at file:///path/to/doc.pdf:"
);
}
#[test]
fn test_anchor_media_no_uri() {
let part = Part::Media {
media_type: MediaType::Image,
data: "base64data".to_string(),
mime_type: "image/png".to_string(),
uri: None,
finished: true,
};
assert_eq!(part.anchor_media(), "File (image/png) at unknown:");
}
}