1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! Multimodal content and tool block vocabulary.
use serde::{Deserialize, Serialize};
/// A single content part in a multimodal message.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ContentPart {
/// Plain text content.
Text {
/// UTF-8 text.
text: String,
},
/// Image content by URL/source and optional inline data.
Image {
/// Provider-readable source, such as a URL or media identifier.
source: String,
/// MIME type, for example `image/png`.
mime_type: String,
/// Optional base64-encoded data.
#[serde(default, skip_serializing_if = "Option::is_none")]
data: Option<String>,
},
/// Audio content by URL/source and optional inline data.
Audio {
/// Provider-readable source, such as a URL or media identifier.
source: String,
/// MIME type, for example `audio/mpeg`.
mime_type: String,
/// Optional base64-encoded data.
#[serde(default, skip_serializing_if = "Option::is_none")]
data: Option<String>,
},
/// Video content by URL/source and optional inline data.
Video {
/// Provider-readable source, such as a URL or media identifier.
source: String,
/// MIME type, for example `video/mp4`.
mime_type: String,
/// Optional base64-encoded data.
#[serde(default, skip_serializing_if = "Option::is_none")]
data: Option<String>,
},
/// File content by URL/source and optional inline data.
File {
/// Provider-readable source, such as a URL or media identifier.
source: String,
/// MIME type, for example `application/pdf`.
mime_type: String,
/// Optional base64-encoded data.
#[serde(default, skip_serializing_if = "Option::is_none")]
data: Option<String>,
},
/// Tool-use request block emitted by a model.
ToolUse {
/// Tool-use identifier.
id: String,
/// Tool name.
name: String,
/// Tool input JSON.
input: serde_json::Map<String, serde_json::Value>,
},
/// Tool-result block returned to a model.
ToolResult {
/// Tool-use identifier this result satisfies.
#[serde(alias = "tool_use_id")]
id: String,
/// Human-readable content.
content: String,
/// Whether the tool returned an error.
#[serde(default)]
is_error: bool,
},
}
/// Tool-use block shape shared across LLM/tool/MCP boundaries.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolUseBlock {
/// Tool-use identifier.
pub id: String,
/// Tool name.
pub name: String,
/// Tool input JSON.
pub input: serde_json::Map<String, serde_json::Value>,
}
/// Tool-result block shape shared across LLM/tool/MCP boundaries.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolResultBlock {
/// Tool-use identifier this result satisfies.
pub id: String,
/// Human-readable content returned by the tool.
pub content: String,
/// Whether the result represents a tool failure.
#[serde(default)]
pub is_error: bool,
}
impl ContentPart {
/// Rough character estimate used by approximate token counters.
#[must_use]
pub fn approx_chars(&self) -> usize {
match self {
Self::Text { text } => text.len(),
Self::ToolUse { input, .. } => {
serde_json::Value::Object(input.clone()).to_string().len()
}
Self::ToolResult { content, .. } => content.len(),
Self::Image { .. } | Self::Audio { .. } | Self::Video { .. } | Self::File { .. } => 256,
}
}
}
/// Wrap text in a single text content block.
#[must_use]
pub fn text_content(text: impl Into<String>) -> Vec<ContentPart> {
vec![ContentPart::Text { text: text.into() }]
}
/// Extract concatenated text from content blocks.
#[must_use]
pub fn text_of(blocks: &[ContentPart]) -> String {
blocks
.iter()
.filter_map(|block| match block {
ContentPart::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("")
}