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 enum ToolChoice {
Auto,
Tool(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseFormat {
pub name: String,
pub schema: serde_json::Value,
pub strict: bool,
}
impl ResponseFormat {
#[must_use]
pub fn new(name: impl Into<String>, schema: serde_json::Value) -> Self {
Self {
name: name.into(),
schema,
strict: true,
}
}
#[must_use]
pub const fn with_strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
}
#[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>,
pub tool_choice: Option<ToolChoice>,
pub response_format: Option<ResponseFormat>,
}
impl ChatRequest {
pub const DEFAULT_MAX_TOKENS: u32 = 4096;
#[must_use]
pub fn new(system: impl Into<String>, messages: Vec<Message>) -> Self {
Self {
system: system.into(),
messages,
tools: None,
max_tokens: Self::DEFAULT_MAX_TOKENS,
max_tokens_explicit: false,
session_id: None,
cached_content: None,
thinking: None,
tool_choice: None,
response_format: None,
}
}
#[must_use]
pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
self.tools = Some(tools);
self
}
#[must_use]
pub const fn with_max_tokens(mut self, max_tokens: u32) -> Self {
self.max_tokens = max_tokens;
self.max_tokens_explicit = true;
self
}
#[must_use]
pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
#[must_use]
pub const fn with_thinking(mut self, thinking: ThinkingConfig) -> Self {
self.thinking = Some(thinking);
self
}
#[must_use]
pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
self.tool_choice = Some(tool_choice);
self
}
#[must_use]
pub fn with_response_format(mut self, response_format: ResponseFormat) -> Self {
self.response_format = Some(response_format);
self
}
}
#[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 const fn assistant_with_content(blocks: Vec<ContentBlock>) -> Self {
Self {
role: Role::Assistant,
content: Content::Blocks(blocks),
}
}
#[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")]
#[non_exhaustive]
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, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
pub display_name: String,
pub tier: super::types::ToolTier,
}
#[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, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum StopReason {
EndTurn,
ToolUse,
MaxTokens,
StopSequence,
Refusal,
ModelContextWindowExceeded,
#[serde(other)]
Unknown,
}
impl StopReason {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::EndTurn => "end_turn",
Self::ToolUse => "tool_use",
Self::MaxTokens => "max_tokens",
Self::StopSequence => "stop_sequence",
Self::Refusal => "refusal",
Self::ModelContextWindowExceeded => "model_context_window_exceeded",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
#[serde(default)]
pub cached_input_tokens: u32,
#[serde(default)]
pub cache_creation_input_tokens: u32,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ChatOutcome {
Success(ChatResponse),
RateLimited,
InvalidRequest(String),
ServerError(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chat_request_new_defaults_then_setters() {
let req = ChatRequest::new("sys", vec![Message::user("hi")]);
assert_eq!(req.system, "sys");
assert_eq!(req.messages.len(), 1);
assert_eq!(req.max_tokens, ChatRequest::DEFAULT_MAX_TOKENS);
assert!(!req.max_tokens_explicit);
assert!(req.tools.is_none());
assert!(req.tool_choice.is_none());
assert!(req.response_format.is_none());
let req = req
.with_max_tokens(1234)
.with_tool_choice(ToolChoice::Auto)
.with_response_format(ResponseFormat::new(
"r",
serde_json::json!({"type": "object"}),
))
.with_session_id("s-1");
assert_eq!(req.max_tokens, 1234);
assert!(req.max_tokens_explicit);
assert!(matches!(req.tool_choice, Some(ToolChoice::Auto)));
assert!(req.response_format.is_some());
assert_eq!(req.session_id.as_deref(), Some("s-1"));
}
#[test]
fn stop_reason_known_values_round_trip() -> Result<(), serde_json::Error> {
for (json, expected) in [
("\"end_turn\"", StopReason::EndTurn),
("\"tool_use\"", StopReason::ToolUse),
("\"max_tokens\"", StopReason::MaxTokens),
("\"stop_sequence\"", StopReason::StopSequence),
("\"refusal\"", StopReason::Refusal),
(
"\"model_context_window_exceeded\"",
StopReason::ModelContextWindowExceeded,
),
] {
let parsed: StopReason = serde_json::from_str(json)?;
assert_eq!(parsed, expected);
assert_eq!(serde_json::to_string(&parsed)?, json);
}
Ok(())
}
#[test]
fn stop_reason_unknown_value_deserializes_to_unknown() -> Result<(), serde_json::Error> {
let parsed: StopReason = serde_json::from_str("\"some_future_reason\"")?;
assert_eq!(parsed, StopReason::Unknown);
assert_eq!(parsed.as_str(), "unknown");
Ok(())
}
#[test]
fn stop_reason_unknown_serializes_to_unknown() -> Result<(), serde_json::Error> {
assert_eq!(serde_json::to_string(&StopReason::Unknown)?, "\"unknown\"");
Ok(())
}
}