async_claude/
messages.rs

1use serde::{Deserialize, Serialize};
2pub mod request;
3#[allow(unused_imports)]
4pub use request::*;
5pub mod response;
6#[allow(unused_imports)]
7pub use response::*;
8pub mod stream_response;
9#[allow(unused_imports)]
10pub use stream_response::*;
11
12#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
13pub struct Message {
14    pub role: Role,
15    pub content: MessageContent,
16}
17
18impl Message {
19    //return true if text or all blocks are empty or only contain white spaces
20    pub fn is_all_empty(&self) -> bool {
21        self.content.is_all_empty()
22    }
23
24    /// Validates that the message contains valid content for the Claude API
25    pub fn validate(&self) -> Result<(), String> {
26        self.content.validate()
27    }
28}
29
30#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
31#[serde(rename_all = "lowercase")]
32pub enum Role {
33    #[default]
34    User,
35    Assistant,
36}
37
38#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
39#[serde(untagged)]
40pub enum MessageContent {
41    Text(String),
42    Blocks(Vec<ContentBlock>),
43}
44
45impl MessageContent {
46    pub fn is_all_empty(&self) -> bool {
47        match self {
48            MessageContent::Text(s) => s.trim().is_empty(),
49            MessageContent::Blocks(blocks) => {
50                if blocks.is_empty() {
51                    return true;
52                }
53                for block in blocks {
54                    if !block.is_empty() {
55                        return false;
56                    }
57                }
58                true
59            }
60        }
61    }
62
63    /// Validates that the message content only contains valid content block types for request body
64    pub fn validate(&self) -> Result<(), String> {
65        match self {
66            MessageContent::Text(_) => Ok(()),
67            MessageContent::Blocks(blocks) => {
68                for (i, block) in blocks.iter().enumerate() {
69                    if !matches!(block, ContentBlock::Base(_) | ContentBlock::RequestOnly(_)) {
70                        return Err(format!(
71                            "Invalid content block type at index {}: {:?}. Only Text, Image, ToolUse, ToolResult, Document, Thinking, and RedactedThinking are allowed in request body.",
72                            i, block
73                        ));
74                    }
75                }
76                Ok(())
77            }
78        }
79    }
80}
81
82// Base content block types that can be used in both request body and streaming
83#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
84#[serde(tag = "type")]
85pub enum BaseContentBlock {
86    #[serde(rename = "text")]
87    Text { text: String },
88    #[serde(rename = "thinking")]
89    Thinking {
90        thinking: String,
91        #[serde(skip_serializing_if = "Option::is_none")]
92        signature: Option<String>,
93    },
94    #[serde(rename = "tool_use")]
95    ToolUse(ToolUseContentBlock),
96}
97
98#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
99pub struct ToolUseContentBlock {
100    pub id: String,
101    pub name: String,
102    pub input: serde_json::Value,
103}
104
105// Additional content block types that can only be used in request body
106#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
107#[serde(tag = "type")]
108pub enum RequestOnlyContentBlock {
109    #[serde(rename = "image")]
110    Image { source: ImageSource },
111    #[serde(rename = "document")]
112    Document {
113        #[serde(skip_serializing_if = "Option::is_none")]
114        source: Option<String>,
115        #[serde(skip_serializing_if = "Option::is_none")]
116        id: Option<String>,
117    },
118    #[serde(rename = "tool_result")]
119    ToolResult {
120        tool_use_id: String,
121        content: String,
122    },
123}
124
125// Content blocks that can be used in request body (all types)
126#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
127#[serde(untagged)]
128pub enum ContentBlock {
129    Base(BaseContentBlock),
130    RequestOnly(RequestOnlyContentBlock),
131    RedactedThinking(RedactedThinkingContentBlock),
132}
133
134#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
135#[serde(untagged)]
136pub enum ResponseContentBlock {
137    Base(BaseContentBlock),
138    RedactedThinking(RedactedThinkingContentBlock),
139}
140
141#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
142#[serde(untagged)]
143pub enum RedactedThinkingContentBlock {
144    #[serde(rename = "redacted_thinking")]
145    RedactedThinking { data: String },
146}
147
148impl ContentBlock {
149    pub fn is_empty(&self) -> bool {
150        match self {
151            ContentBlock::Base(base) => match base {
152                BaseContentBlock::Text { text } => text.trim().is_empty(),
153                BaseContentBlock::ToolUse(tool_use) => {
154                    tool_use.id.is_empty()
155                        || tool_use.name.is_empty()
156                        || !tool_use.input.is_object()
157                }
158                BaseContentBlock::Thinking { thinking, .. } => thinking.trim().is_empty(),
159            },
160            ContentBlock::RequestOnly(req_only) => match req_only {
161                RequestOnlyContentBlock::Image { source } => match source {
162                    ImageSource::Base64 { media_type, data } => {
163                        media_type.trim().is_empty() || data.trim().is_empty()
164                    }
165                },
166                RequestOnlyContentBlock::Document { source, id } => {
167                    source.is_none() || id.is_none()
168                }
169                RequestOnlyContentBlock::ToolResult {
170                    tool_use_id,
171                    content,
172                } => tool_use_id.is_empty() || content.trim().is_empty(),
173            },
174            ContentBlock::RedactedThinking(redacted_thinking) => match redacted_thinking {
175                RedactedThinkingContentBlock::RedactedThinking { data } => data.is_empty(),
176            },
177        }
178    }
179}
180
181// Delta content blocks for streaming
182// TODO: it should include a redacted_thinking delta, but the docuement dind't include it as example
183#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
184#[serde(tag = "type")]
185pub enum DeltaContentBlock {
186    #[serde(rename = "text_delta")]
187    TextDelta { text: String },
188    #[serde(rename = "input_json_delta")]
189    InputJsonDelta { partial_json: String },
190    #[serde(rename = "thinking_delta")]
191    ThinkingDelta { thinking: String },
192    #[serde(rename = "signature_delta")]
193    SignatureDelta { signature: String },
194}
195
196impl DeltaContentBlock {
197    pub fn is_empty(&self) -> bool {
198        match self {
199            DeltaContentBlock::TextDelta { text } => text.trim().is_empty(),
200            DeltaContentBlock::InputJsonDelta { partial_json } => partial_json.is_empty(),
201            DeltaContentBlock::ThinkingDelta { thinking } => thinking.is_empty(),
202            DeltaContentBlock::SignatureDelta { signature } => signature.is_empty(),
203        }
204    }
205}
206
207#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
208#[serde(tag = "type")]
209pub enum ImageSource {
210    #[serde(rename = "base64")]
211    Base64 { media_type: String, data: String },
212}
213
214#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
215#[serde(tag = "type")]
216pub enum DocumentSource {
217    #[serde(rename = "base64")]
218    Base64 { media_type: String, data: String },
219}
220
221#[derive(Debug, Deserialize, Default, Clone, PartialEq, Serialize)]
222pub struct Usage {
223    pub input_tokens: Option<u32>,
224    pub output_tokens: u32,
225}
226
227#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
228#[serde(rename_all = "snake_case")]
229pub enum StopReason {
230    EndTurn,
231    MaxTokens,
232    StopSequence,
233    ToolUse,
234}