use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::format::FormatId;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Role {
System,
Developer,
User,
Assistant,
Tool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct InstructionBlock {
pub role: Role,
pub content: Vec<ContentBlock>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: Vec<ContentBlock>,
}
impl Message {
pub fn text(role: Role, text: impl Into<String>) -> Self {
Self {
role,
content: vec![ContentBlock::Text { text: text.into() }],
}
}
pub fn text_content(&self, separator: &str) -> Option<String> {
let parts = self
.content
.iter()
.filter_map(|block| match block {
ContentBlock::Text { text } => Some(text.as_str()),
ContentBlock::Refusal { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>();
if parts.is_empty() {
None
} else {
Some(parts.join(separator))
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text {
text: String,
},
Reasoning {
text: String,
signature: Option<String>,
},
Image {
source: ImageSource,
},
Audio {
source: MediaSource,
},
Video {
source: MediaSource,
},
File {
source: FileSource,
},
ToolCall(ToolCall),
ToolResult(ToolResult),
Refusal {
text: String,
},
Unknown {
provider: FormatId,
raw: Value,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum ImageSource {
Url {
url: String,
detail: Option<String>,
},
Base64 {
media_type: Option<String>,
data: String,
},
Raw(Value),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum FileSource {
FileId(String),
FileData {
data: String,
filename: Option<String>,
},
Raw(Value),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum MediaSource {
Url {
url: String,
media_type: Option<String>,
},
Base64 {
media_type: Option<String>,
data: String,
},
Raw(Value),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: Value,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ToolResult {
pub tool_call_id: String,
pub content: Vec<ContentBlock>,
pub is_error: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
pub description: Option<String>,
pub parameters: Value,
pub strict: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum ToolChoice {
Auto,
Required,
None,
Tool {
name: String,
},
Raw(Value),
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct SamplingParams {
pub temperature: Option<f64>,
pub top_p: Option<f64>,
pub top_k: Option<i64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct OutputParams {
pub max_output_tokens: Option<u64>,
pub response_format: Option<Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ReasoningParams {
pub effort: Option<String>,
pub raw: Option<Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ProviderExtensions {
pub fields: Map<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct PreservationMetadata {
pub requests: BTreeMap<FormatId, Value>,
pub responses: BTreeMap<FormatId, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct LlmRequest {
pub model: Option<String>,
pub instructions: Vec<InstructionBlock>,
pub messages: Vec<Message>,
pub tools: Vec<ToolDefinition>,
pub tool_choice: Option<ToolChoice>,
pub sampling: SamplingParams,
pub output: OutputParams,
pub reasoning: ReasoningParams,
pub stream: bool,
pub extensions: ProviderExtensions,
pub preservation: PreservationMetadata,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Usage {
pub input_tokens: Option<u64>,
#[serde(flatten)]
pub cache: Option<Box<InputCacheUsage>>,
pub output_tokens: Option<u64>,
pub total_tokens: Option<u64>,
pub reasoning_tokens: Option<u64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct InputCacheUsage {
pub cached_input_tokens: Option<u64>,
pub cache_creation_input_tokens: Option<u64>,
}
impl Usage {
pub fn cache_details(
cached_input_tokens: Option<u64>,
cache_creation_input_tokens: Option<u64>,
) -> Option<Box<InputCacheUsage>> {
if cached_input_tokens.is_none() && cache_creation_input_tokens.is_none() {
return None;
}
Some(Box::new(InputCacheUsage {
cached_input_tokens,
cache_creation_input_tokens,
}))
}
pub fn cached_input_tokens(&self) -> Option<u64> {
self.cache
.as_ref()
.and_then(|cache| cache.cached_input_tokens)
}
pub fn cache_creation_input_tokens(&self) -> Option<u64> {
self.cache
.as_ref()
.and_then(|cache| cache.cache_creation_input_tokens)
}
pub fn set_cached_input_tokens(&mut self, value: u64) {
self.cache
.get_or_insert_with(|| Box::new(InputCacheUsage::default()))
.cached_input_tokens = Some(value);
}
pub fn set_cache_creation_input_tokens(&mut self, value: u64) {
self.cache
.get_or_insert_with(|| Box::new(InputCacheUsage::default()))
.cache_creation_input_tokens = Some(value);
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
MaxTokens,
ToolUse,
ContentFilter,
Error,
Unknown,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ResponseOutput {
pub role: Role,
pub content: Vec<ContentBlock>,
pub stop_reason: Option<StopReason>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct AggLlmResponse {
pub id: Option<String>,
pub model: Option<String>,
pub outputs: Vec<ResponseOutput>,
pub usage: Usage,
pub extensions: ProviderExtensions,
pub preservation: PreservationMetadata,
}
impl AggLlmResponse {
pub fn first_output(&self) -> Option<&ResponseOutput> {
self.outputs.first()
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn serde_uses_python_friendly_dictionary_shapes() -> Result<(), serde_json::Error> {
let request: LlmRequest = serde_json::from_value(json!({
"model": "auto",
"messages": [{
"role": "user",
"content": [{"type": "text", "text": "hello"}]
}]
}))?;
assert_eq!(request.messages[0], Message::text(Role::User, "hello"));
let tool_call = ContentBlock::ToolCall(ToolCall {
id: "call-1".to_string(),
name: "lookup".to_string(),
arguments: json!({"query": "rust"}),
});
assert_eq!(
serde_json::to_value(tool_call)?,
json!({
"type": "tool_call",
"id": "call-1",
"name": "lookup",
"arguments": {"query": "rust"}
})
);
Ok(())
}
}