Skip to main content

gemini_cli_sdk/types/
content.rs

1//! Public content block types — identical structure to claude-cli-sdk.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6// ── ContentBlock ──────────────────────────────────────────────────────────────
7
8/// A block of content within a message.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum ContentBlock {
12    Text(TextBlock),
13    ToolUse(ToolUseBlock),
14    ToolResult(ToolResultBlock),
15    Thinking(ThinkingBlock),
16    Image(ImageBlock),
17}
18
19impl ContentBlock {
20    /// Extract text content if this is a `Text` block.
21    ///
22    /// # Examples
23    ///
24    /// ```rust
25    /// use gemini_cli_sdk::types::content::{ContentBlock, TextBlock};
26    ///
27    /// let block = ContentBlock::Text(TextBlock::new("hello"));
28    /// assert_eq!(block.as_text(), Some("hello"));
29    /// ```
30    pub fn as_text(&self) -> Option<&str> {
31        match self {
32            ContentBlock::Text(t) => Some(&t.text),
33            _ => None,
34        }
35    }
36
37    /// Returns `true` if this is a thinking/reasoning block.
38    ///
39    /// # Examples
40    ///
41    /// ```rust
42    /// use gemini_cli_sdk::types::content::{ContentBlock, ThinkingBlock};
43    ///
44    /// let block = ContentBlock::Thinking(ThinkingBlock::new("reasoning"));
45    /// assert!(block.is_thinking());
46    /// ```
47    pub fn is_thinking(&self) -> bool {
48        matches!(self, ContentBlock::Thinking(_))
49    }
50}
51
52// ── Text ──────────────────────────────────────────────────────────────────────
53
54/// A plain-text content block.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct TextBlock {
57    pub text: String,
58    #[serde(flatten)]
59    pub extra: Value,
60}
61
62impl TextBlock {
63    /// Construct a `TextBlock` with no extra fields.
64    pub fn new(text: impl Into<String>) -> Self {
65        Self {
66            text: text.into(),
67            extra: Value::Object(Default::default()),
68        }
69    }
70}
71
72// ── Tool Use ──────────────────────────────────────────────────────────────────
73
74/// A tool invocation block emitted by the model.
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub struct ToolUseBlock {
77    /// Unique identifier for this tool call, used to correlate results.
78    pub id: String,
79    /// Name of the tool being called.
80    pub name: String,
81    /// JSON input arguments for the tool.
82    #[serde(default)]
83    pub input: Value,
84    #[serde(flatten)]
85    pub extra: Value,
86}
87
88// ── Tool Result ───────────────────────────────────────────────────────────────
89
90/// The result of a tool call, sent back to the model.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct ToolResultBlock {
93    /// Must match the `id` field of the corresponding [`ToolUseBlock`].
94    pub tool_use_id: String,
95    /// One or more content items produced by the tool.
96    #[serde(default)]
97    pub content: Vec<ToolResultContent>,
98    /// Set to `true` if the tool encountered an error.
99    #[serde(default)]
100    pub is_error: bool,
101    #[serde(flatten)]
102    pub extra: Value,
103}
104
105/// Content produced by a tool — either text or an image.
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107#[serde(tag = "type", rename_all = "snake_case")]
108pub enum ToolResultContent {
109    Text { text: String },
110    Image { source: ImageSource },
111}
112
113// ── Thinking ──────────────────────────────────────────────────────────────────
114
115/// An extended-thinking / reasoning block.
116///
117/// Maps from Gemini's `agent_thought_chunk` — kept separate from text output.
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct ThinkingBlock {
120    /// The reasoning text produced before the final answer.
121    #[serde(default)]
122    pub thinking: String,
123    #[serde(flatten)]
124    pub extra: Value,
125}
126
127impl ThinkingBlock {
128    /// Construct a `ThinkingBlock` with no extra fields.
129    pub fn new(thinking: impl Into<String>) -> Self {
130        Self {
131            thinking: thinking.into(),
132            extra: Value::Object(Default::default()),
133        }
134    }
135}
136
137// ── Image ─────────────────────────────────────────────────────────────────────
138
139/// An image content block.
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub struct ImageBlock {
142    pub source: ImageSource,
143    #[serde(flatten)]
144    pub extra: Value,
145}
146
147/// The source of an image — either inline base-64 data or a remote URL.
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149#[serde(tag = "type", rename_all = "snake_case")]
150pub enum ImageSource {
151    Base64(Base64ImageSource),
152    Url(UrlImageSource),
153}
154
155/// Inline base-64 encoded image data.
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
157pub struct Base64ImageSource {
158    /// MIME type, e.g. `"image/png"`.
159    pub media_type: String,
160    /// Base-64 encoded bytes.
161    pub data: String,
162}
163
164/// A remotely-hosted image referenced by URL.
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub struct UrlImageSource {
167    pub url: String,
168}
169
170// ── UserContent ───────────────────────────────────────────────────────────────
171
172/// Content that can be sent in a user message.
173///
174/// Use the convenience constructors rather than constructing variants directly
175/// to keep call-sites free of boilerplate.
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177#[serde(tag = "type", rename_all = "snake_case")]
178pub enum UserContent {
179    Text { text: String },
180    Image { source: ImageSource },
181}
182
183impl UserContent {
184    /// Create a text user-content item.
185    ///
186    /// # Examples
187    ///
188    /// ```rust
189    /// use gemini_cli_sdk::types::content::UserContent;
190    ///
191    /// let uc = UserContent::text("Hello, Gemini!");
192    /// ```
193    pub fn text(text: impl Into<String>) -> Self {
194        UserContent::Text { text: text.into() }
195    }
196
197    /// Create an inline base-64 image user-content item.
198    ///
199    /// # Examples
200    ///
201    /// ```rust
202    /// use gemini_cli_sdk::types::content::UserContent;
203    ///
204    /// let uc = UserContent::image_base64("image/png", "iVBORw0KGgo=");
205    /// ```
206    pub fn image_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
207        UserContent::Image {
208            source: ImageSource::Base64(Base64ImageSource {
209                media_type: media_type.into(),
210                data: data.into(),
211            }),
212        }
213    }
214
215    /// Create a URL-referenced image user-content item.
216    ///
217    /// # Examples
218    ///
219    /// ```rust
220    /// use gemini_cli_sdk::types::content::UserContent;
221    ///
222    /// let uc = UserContent::image_url("https://example.com/img.png");
223    /// ```
224    pub fn image_url(url: impl Into<String>) -> Self {
225        UserContent::Image {
226            source: ImageSource::Url(UrlImageSource { url: url.into() }),
227        }
228    }
229}
230
231// ── Tests ─────────────────────────────────────────────────────────────────────
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use serde_json::json;
237
238    // ── ContentBlock serde ────────────────────────────────────────────────────
239
240    #[test]
241    fn test_content_block_text_serde_roundtrip() {
242        let block = ContentBlock::Text(TextBlock::new("hello world"));
243        let json = serde_json::to_value(&block).expect("serialize");
244        assert_eq!(json["type"], "text");
245        assert_eq!(json["text"], "hello world");
246
247        let restored: ContentBlock = serde_json::from_value(json).expect("deserialize");
248        assert_eq!(restored, block);
249    }
250
251    #[test]
252    fn test_content_block_tool_use_serde_roundtrip() {
253        let block = ContentBlock::ToolUse(ToolUseBlock {
254            id: "call_123".into(),
255            name: "read_file".into(),
256            input: json!({"path": "/tmp/foo.txt"}),
257            extra: Value::Object(Default::default()),
258        });
259        let json = serde_json::to_value(&block).expect("serialize");
260        assert_eq!(json["type"], "tool_use");
261        assert_eq!(json["id"], "call_123");
262        assert_eq!(json["name"], "read_file");
263
264        let restored: ContentBlock = serde_json::from_value(json).expect("deserialize");
265        assert_eq!(restored, block);
266    }
267
268    // ── ContentBlock helpers ──────────────────────────────────────────────────
269
270    #[test]
271    fn test_content_block_as_text_some() {
272        let block = ContentBlock::Text(TextBlock::new("greetings"));
273        assert_eq!(block.as_text(), Some("greetings"));
274    }
275
276    #[test]
277    fn test_content_block_as_text_none() {
278        let block = ContentBlock::ToolUse(ToolUseBlock {
279            id: "id".into(),
280            name: "tool".into(),
281            input: Value::Null,
282            extra: Value::Object(Default::default()),
283        });
284        assert_eq!(block.as_text(), None);
285    }
286
287    #[test]
288    fn test_content_block_is_thinking() {
289        let thinking = ContentBlock::Thinking(ThinkingBlock::new("deep thought"));
290        assert!(thinking.is_thinking());
291
292        let text = ContentBlock::Text(TextBlock::new("plain text"));
293        assert!(!text.is_thinking());
294    }
295
296    // ── Constructor helpers ───────────────────────────────────────────────────
297
298    #[test]
299    fn test_text_block_new() {
300        let tb = TextBlock::new("sample");
301        assert_eq!(tb.text, "sample");
302        assert_eq!(tb.extra, Value::Object(Default::default()));
303    }
304
305    #[test]
306    fn test_thinking_block_new() {
307        let tb = ThinkingBlock::new("pondering");
308        assert_eq!(tb.thinking, "pondering");
309        assert_eq!(tb.extra, Value::Object(Default::default()));
310    }
311
312    // ── UserContent ───────────────────────────────────────────────────────────
313
314    #[test]
315    fn test_user_content_text() {
316        let uc = UserContent::text("hi there");
317        assert_eq!(uc, UserContent::Text { text: "hi there".into() });
318
319        let json = serde_json::to_value(&uc).expect("serialize");
320        assert_eq!(json["type"], "text");
321        assert_eq!(json["text"], "hi there");
322
323        let restored: UserContent = serde_json::from_value(json).expect("deserialize");
324        assert_eq!(restored, uc);
325    }
326
327    #[test]
328    fn test_user_content_image_base64() {
329        let uc = UserContent::image_base64("image/png", "abc123==");
330        let expected = UserContent::Image {
331            source: ImageSource::Base64(Base64ImageSource {
332                media_type: "image/png".into(),
333                data: "abc123==".into(),
334            }),
335        };
336        assert_eq!(uc, expected);
337
338        let json = serde_json::to_value(&uc).expect("serialize");
339        assert_eq!(json["type"], "image");
340        assert_eq!(json["source"]["type"], "base64");
341        assert_eq!(json["source"]["media_type"], "image/png");
342        assert_eq!(json["source"]["data"], "abc123==");
343    }
344
345    #[test]
346    fn test_user_content_image_url() {
347        let uc = UserContent::image_url("https://example.com/photo.jpg");
348        let expected = UserContent::Image {
349            source: ImageSource::Url(UrlImageSource {
350                url: "https://example.com/photo.jpg".into(),
351            }),
352        };
353        assert_eq!(uc, expected);
354
355        let json = serde_json::to_value(&uc).expect("serialize");
356        assert_eq!(json["type"], "image");
357        assert_eq!(json["source"]["type"], "url");
358        assert_eq!(json["source"]["url"], "https://example.com/photo.jpg");
359    }
360}