use crate::proxy::{LlmMessage, LlmRole, LlmUsage};
use serde::{Deserialize, Serialize};
pub mod models {
pub const OPUS_4_6: &str = "claude-opus-4-6";
pub const SONNET_4_6: &str = "claude-sonnet-4-6";
pub const HAIKU_4_5: &str = "claude-haiku-4-5";
pub const OPUS_4_5: &str = "claude-opus-4-5";
pub const SONNET_4_5: &str = "claude-sonnet-4-5";
}
#[derive(Debug, Clone, Serialize)]
pub struct MessagesRequest {
pub model: String,
pub messages: Vec<Message>,
pub max_tokens: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_k: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequences: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<ThinkingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_config: Option<OutputConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
pub stream: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemContent {
Text(String),
Blocks(Vec<SystemBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: MessageRole,
pub content: MessageContent,
}
impl Message {
pub fn user(text: impl Into<String>) -> Self {
Self {
role: MessageRole::User,
content: MessageContent::Text(text.into()),
}
}
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: MessageRole::Assistant,
content: MessageContent::Text(text.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Blocks(Vec<ContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
Image {
source: ImageSource,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
Thinking {
thinking: String,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
RedactedThinking { data: String },
ToolUse {
id: String,
name: String,
input: serde_json::Value,
},
ToolResult {
tool_use_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<ToolResultContent>,
#[serde(skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
},
}
impl ContentBlock {
pub fn as_text(&self) -> Option<&str> {
match self {
ContentBlock::Text { text, .. } => Some(text),
_ => None,
}
}
pub fn as_thinking(&self) -> Option<&str> {
match self {
ContentBlock::Thinking { thinking, .. } => Some(thinking),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ImageSource {
Base64 {
media_type: String, data: String,
},
Url {
url: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Text(String),
Blocks(Vec<ContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ThinkingConfig {
Adaptive,
Enabled { budget_tokens: usize },
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolChoice {
Auto {
#[serde(skip_serializing_if = "Option::is_none")]
disable_parallel_tool_use: Option<bool>,
},
Any {
#[serde(skip_serializing_if = "Option::is_none")]
disable_parallel_tool_use: Option<bool>,
},
Tool {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
disable_parallel_tool_use: Option<bool>,
},
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<Effort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<OutputFormat>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Effort {
Low,
Medium,
High,
Max,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OutputFormat {
JsonSchema { schema: serde_json::Value },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheControl {
#[serde(rename = "type")]
pub control_type: String, #[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>, }
impl CacheControl {
pub fn ephemeral() -> Self {
Self {
control_type: "ephemeral".to_string(),
ttl: None,
}
}
pub fn ephemeral_1h() -> Self {
Self {
control_type: "ephemeral".to_string(),
ttl: Some("1h".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MessagesResponse {
pub id: String,
#[serde(rename = "type")]
pub response_type: String, pub role: String, pub content: Vec<ContentBlock>,
pub model: String,
pub stop_reason: Option<StopReason>,
pub stop_sequence: Option<String>,
pub usage: Usage,
}
impl MessagesResponse {
pub fn text(&self) -> Option<&str> {
self.content.iter().find_map(|b| b.as_text())
}
pub fn thinking(&self) -> Option<String> {
let parts: Vec<&str> = self
.content
.iter()
.filter_map(|b| b.as_thinking())
.collect();
if parts.is_empty() {
None
} else {
Some(parts.join("\n"))
}
}
pub fn has_tool_use(&self) -> bool {
self.stop_reason == Some(StopReason::ToolUse)
}
pub fn tool_calls(&self) -> Vec<(&str, &str, &serde_json::Value)> {
self.content
.iter()
.filter_map(|b| match b {
ContentBlock::ToolUse { id, name, input } => {
Some((id.as_str(), name.as_str(), input))
}
_ => None,
})
.collect()
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
MaxTokens,
StopSequence,
ToolUse,
PauseTurn,
Refusal,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Usage {
pub input_tokens: usize,
pub output_tokens: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_creation_input_tokens: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<usize>,
}
impl From<LlmMessage> for Message {
fn from(msg: LlmMessage) -> Self {
Self {
role: match msg.role {
LlmRole::System => MessageRole::User, LlmRole::User => MessageRole::User,
LlmRole::Assistant => MessageRole::Assistant,
},
content: MessageContent::Text(msg.content),
}
}
}
impl From<Usage> for LlmUsage {
fn from(u: Usage) -> Self {
Self {
prompt_tokens: u.input_tokens,
completion_tokens: u.output_tokens,
total_tokens: u.input_tokens + u.output_tokens,
}
}
}