use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub enum ThinkingMode {
Enabled { budget_tokens: u32 },
Adaptive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Effort {
Low,
Medium,
High,
Max,
}
#[derive(Debug, Clone)]
pub struct ThinkingConfig {
pub mode: ThinkingMode,
pub effort: Option<Effort>,
}
impl ThinkingConfig {
pub const DEFAULT_BUDGET_TOKENS: u32 = 10_000;
pub const MIN_BUDGET_TOKENS: u32 = 1_024;
#[must_use]
pub const fn new(budget_tokens: u32) -> Self {
Self {
mode: ThinkingMode::Enabled { budget_tokens },
effort: None,
}
}
#[must_use]
pub const fn adaptive() -> Self {
Self {
mode: ThinkingMode::Adaptive,
effort: None,
}
}
#[must_use]
pub const fn adaptive_with_effort(effort: Effort) -> Self {
Self {
mode: ThinkingMode::Adaptive,
effort: Some(effort),
}
}
#[must_use]
pub const fn with_effort(mut self, effort: Effort) -> Self {
self.effort = Some(effort);
self
}
}
impl Default for ThinkingConfig {
fn default() -> Self {
Self::new(Self::DEFAULT_BUDGET_TOKENS)
}
}
#[derive(Debug, Clone)]
pub struct ChatRequest {
pub system: String,
pub messages: Vec<Message>,
pub tools: Option<Vec<Tool>>,
pub max_tokens: u32,
pub max_tokens_explicit: bool,
pub session_id: Option<String>,
pub cached_content: Option<String>,
pub thinking: Option<ThinkingConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: Content,
}
impl Message {
#[must_use]
pub fn user(text: impl Into<String>) -> Self {
Self {
role: Role::User,
content: Content::Text(text.into()),
}
}
#[must_use]
pub const fn user_with_content(blocks: Vec<ContentBlock>) -> Self {
Self {
role: Role::User,
content: Content::Blocks(blocks),
}
}
#[must_use]
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: Content::Text(text.into()),
}
}
#[must_use]
pub fn assistant_with_tool_use(
text: Option<String>,
id: impl Into<String>,
name: impl Into<String>,
input: serde_json::Value,
) -> Self {
let mut blocks = Vec::new();
if let Some(t) = text {
blocks.push(ContentBlock::Text { text: t });
}
blocks.push(ContentBlock::ToolUse {
id: id.into(),
name: name.into(),
input,
thought_signature: None,
});
Self {
role: Role::Assistant,
content: Content::Blocks(blocks),
}
}
#[must_use]
pub fn tool_result(
tool_use_id: impl Into<String>,
content: impl Into<String>,
is_error: bool,
) -> Self {
Self {
role: Role::User,
content: Content::Blocks(vec![ContentBlock::ToolResult {
tool_use_id: tool_use_id.into(),
content: content.into(),
is_error: if is_error { Some(true) } else { None },
}]),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Content {
Text(String),
Blocks(Vec<ContentBlock>),
}
impl Content {
#[must_use]
pub fn first_text(&self) -> Option<&str> {
match self {
Self::Text(s) => Some(s),
Self::Blocks(blocks) => blocks.iter().find_map(|b| match b {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
}),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentSource {
pub media_type: String,
pub data: String,
}
impl ContentSource {
#[must_use]
pub fn new(media_type: impl Into<String>, data: impl Into<String>) -> Self {
Self {
media_type: media_type.into(),
data: data.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ContentBlock {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "thinking")]
Thinking {
thinking: String,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
#[serde(rename = "redacted_thinking")]
RedactedThinking { data: String },
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
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(rename = "image")]
Image { source: ContentSource },
#[serde(rename = "document")]
Document { source: ContentSource },
}
#[derive(Debug, Clone, Serialize)]
pub struct Tool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
#[derive(Debug, Clone)]
pub struct ChatResponse {
pub id: String,
pub content: Vec<ContentBlock>,
pub model: String,
pub stop_reason: Option<StopReason>,
pub usage: Usage,
}
impl ChatResponse {
#[must_use]
pub fn first_text(&self) -> Option<&str> {
self.content.iter().find_map(|b| match b {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
}
#[must_use]
pub fn first_thinking(&self) -> Option<&str> {
self.content.iter().find_map(|b| match b {
ContentBlock::Thinking { thinking, .. } => Some(thinking.as_str()),
_ => None,
})
}
pub fn tool_uses(&self) -> impl Iterator<Item = (&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,
})
}
#[must_use]
pub fn has_tool_use(&self) -> bool {
self.content
.iter()
.any(|b| matches!(b, ContentBlock::ToolUse { .. }))
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
ToolUse,
MaxTokens,
StopSequence,
Refusal,
ModelContextWindowExceeded,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
#[serde(default)]
pub cached_input_tokens: u32,
}
#[derive(Debug)]
pub enum ChatOutcome {
Success(ChatResponse),
RateLimited,
InvalidRequest(String),
ServerError(String),
}