use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MessageRequest {
pub model: String,
pub messages: Vec<Message>,
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemPrompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
}
#[derive(Debug, Clone)]
pub struct PrimaryTurnRequest {
pub model: String,
pub messages: Vec<Message>,
pub max_tokens: u32,
pub system: Option<SystemPrompt>,
pub tools: Option<Vec<Tool>>,
pub tool_choice: Option<serde_json::Value>,
pub reasoning_effort: Option<String>,
}
#[must_use]
pub fn prepare_primary_turn_request(input: PrimaryTurnRequest) -> MessageRequest {
MessageRequest {
model: input.model,
messages: input.messages,
max_tokens: input.max_tokens,
system: input.system,
tools: input.tools,
tool_choice: input.tool_choice,
metadata: None,
thinking: None,
reasoning_effort: input.reasoning_effort,
stream: Some(true),
temperature: None,
top_p: None,
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum SystemPrompt {
Text(String),
Blocks(Vec<SystemBlock>),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct SystemBlock {
#[serde(rename = "type")]
pub block_type: String,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ImageUrlContent {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Message {
pub role: String,
pub content: Vec<ContentBlock>,
}
pub const INTERRUPTED_ASSISTANT_ROLE: &str = "assistant_interrupted";
pub const INTERRUPTED_ASSISTANT_CONTEXT_PREFIX: &str = "[The following assistant output was interrupted before completion and may be incomplete or wrong]\n";
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct OpaqueReasoningState {
pub provider: String,
pub api: String,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub encrypted_content: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
pub enum ContentBlock {
#[serde(rename = "text")]
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "image_url")]
ImageUrl { image_url: ImageUrlContent },
#[serde(rename = "thinking")]
Thinking {
thinking: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
state: Option<OpaqueReasoningState>,
},
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
caller: Option<ToolCaller>,
#[serde(skip_serializing_if = "Option::is_none", default)]
thought_signature: Option<String>,
},
#[serde(rename = "tool_result")]
ToolResult {
tool_use_id: String,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
content_blocks: Option<Vec<serde_json::Value>>,
},
#[serde(rename = "server_tool_use")]
ServerToolUse {
id: String,
name: String,
input: serde_json::Value,
},
#[serde(rename = "tool_search_tool_result")]
ToolSearchToolResult {
tool_use_id: String,
content: serde_json::Value,
},
#[serde(rename = "code_execution_tool_result")]
CodeExecutionToolResult {
tool_use_id: String,
content: serde_json::Value,
},
}
impl ContentBlock {
#[must_use]
pub fn thinking(thinking: impl Into<String>) -> Self {
Self::Thinking {
thinking: thinking.into(),
signature: None,
state: None,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct CacheControl {
#[serde(rename = "type")]
pub cache_type: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ToolCaller {
#[serde(rename = "type")]
pub caller_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Tool {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub tool_type: Option<String>,
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_callers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub defer_loading: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_examples: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn primary_turn() -> PrimaryTurnRequest {
PrimaryTurnRequest {
model: "deepseek-v4-flash".to_string(),
messages: vec![Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: "inspect the request".to_string(),
cache_control: None,
}],
}],
max_tokens: 4096,
system: Some(SystemPrompt::Text("system".to_string())),
tools: Some(vec![Tool {
tool_type: None,
name: "read_file".to_string(),
description: "Read a file".to_string(),
input_schema: json!({"zeta": 1, "alpha": 2, "type": "object"}),
allowed_callers: None,
defer_loading: None,
input_examples: None,
strict: None,
cache_control: None,
}]),
tool_choice: Some(json!({"type": "auto"})),
reasoning_effort: Some("high".to_string()),
}
}
#[test]
fn primary_turn_preparation_has_stable_serialized_bytes() {
let first = prepare_primary_turn_request(primary_turn());
let second = prepare_primary_turn_request(primary_turn());
let first_bytes = serde_json::to_vec(&first).expect("serialize first request");
let second_bytes = serde_json::to_vec(&second).expect("serialize second request");
assert_eq!(first_bytes, second_bytes);
assert_eq!(
first_bytes,
br#"{"model":"deepseek-v4-flash","messages":[{"role":"user","content":[{"type":"text","text":"inspect the request"}]}],"max_tokens":4096,"system":"system","tools":[{"name":"read_file","description":"Read a file","input_schema":{"zeta":1,"alpha":2,"type":"object"}}],"tool_choice":{"type":"auto"},"reasoning_effort":"high","stream":true}"#
);
}
#[test]
fn primary_turn_preparation_owns_shared_defaults() {
let request = prepare_primary_turn_request(primary_turn());
assert_eq!(request.stream, Some(true));
assert!(request.metadata.is_none());
assert!(request.thinking.is_none());
assert!(request.temperature.is_none());
assert!(request.top_p.is_none());
}
}