adk-anthropic 0.6.0

Dedicated Anthropic API client for ADK-Rust
Documentation
use serde::{Deserialize, Serialize};

use crate::types::{ContentBlock, MessageRole, Model, StopReason, Usage};

/// Container information returned in message responses.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContainerInfo {
    /// Container identifier.
    pub id: String,
    /// Expiration timestamp.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<String>,
}

/// A message generated by the assistant.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Message {
    /// Unique object identifier.
    ///
    /// The format and length of IDs may change over time.
    pub id: String,

    /// Container information for code execution tool reuse.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub container: Option<ContainerInfo>,

    /// Content generated by the model.
    ///
    /// This is an array of content blocks, each of which has a `type` that determines
    /// its shape.
    pub content: Vec<ContentBlock>,

    /// The model that generated the message.
    pub model: Model,

    /// Conversational role of the generated message.
    ///
    /// This will always be "assistant".
    pub role: MessageRole,

    /// The reason that generation stopped.
    ///
    /// This may be null in streaming responses until completion.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stop_reason: Option<StopReason>,

    /// Which custom stop sequence was generated, if any.
    ///
    /// This value will be a non-null string if one of your custom stop sequences was
    /// generated.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stop_sequence: Option<String>,

    /// Object type, which is always "message".
    pub r#type: String,

    /// Billing and rate-limit usage information.
    pub usage: Usage,
}

impl Message {
    /// Create a new `Message` with the given parameters.
    pub fn new(id: String, content: Vec<ContentBlock>, model: Model, usage: Usage) -> Self {
        Self {
            id,
            container: None,
            content,
            model,
            role: MessageRole::Assistant,
            stop_reason: None,
            stop_sequence: None,
            r#type: "message".to_string(),
            usage,
        }
    }

    /// Set the stop reason.
    pub fn with_stop_reason(mut self, stop_reason: StopReason) -> Self {
        self.stop_reason = Some(stop_reason);
        self
    }

    /// Set the stop sequence.
    pub fn with_stop_sequence(mut self, stop_sequence: String) -> Self {
        self.stop_sequence = Some(stop_sequence);
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::TextBlock;
    use serde_json::{json, to_value};

    #[test]
    fn message_serialization() {
        let text_block = TextBlock::new("Hello, I'm Claude.".to_string());
        let content = vec![ContentBlock::Text(text_block)];
        let model = Model::Known(crate::types::KnownModel::ClaudeSonnet46);
        let usage = Usage::new(50, 100);

        let message = Message::new("msg_012345".to_string(), content, model, usage);

        let json = to_value(&message).unwrap();
        assert_eq!(
            json,
            json!({
                "id": "msg_012345",
                "content": [
                    {
                        "text": "Hello, I'm Claude.",
                        "type": "text"
                    }
                ],
                "model": "claude-sonnet-4-6",
                "role": "assistant",
                "type": "message",
                "usage": {
                    "input_tokens": 50,
                    "output_tokens": 100
                }
            })
        );
    }

    #[test]
    fn message_with_stop_reason() {
        let text_block = TextBlock::new("Hello, I'm Claude.".to_string());
        let content = vec![ContentBlock::Text(text_block)];
        let model = Model::Known(crate::types::KnownModel::ClaudeSonnet46);
        let usage = Usage::new(50, 100);

        let message = Message::new("msg_012345".to_string(), content, model, usage)
            .with_stop_reason(StopReason::EndTurn);

        let json = to_value(&message).unwrap();
        assert_eq!(
            json,
            json!({
                "id": "msg_012345",
                "content": [
                    {
                        "text": "Hello, I'm Claude.",
                        "type": "text"
                    }
                ],
                "model": "claude-sonnet-4-6",
                "role": "assistant",
                "stop_reason": "end_turn",
                "type": "message",
                "usage": {
                    "input_tokens": 50,
                    "output_tokens": 100
                }
            })
        );
    }

    #[test]
    fn message_deserialization() {
        let json = json!({
            "id": "msg_012345",
            "content": [
                {
                    "text": "Hello, I'm Claude.",
                    "type": "text"
                }
            ],
            "model": "claude-sonnet-4-6",
            "role": "assistant",
            "stop_reason": "end_turn",
            "stop_sequence": "###",
            "type": "message",
            "usage": {
                "input_tokens": 50,
                "output_tokens": 100,
                "server_tool_use": {
                    "web_search_requests": 5
                }
            }
        });

        let message: Message = serde_json::from_value(json).unwrap();
        assert_eq!(message.id, "msg_012345");
        assert_eq!(message.role, MessageRole::Assistant);
        assert_eq!(message.r#type, "message");
        assert_eq!(message.stop_reason, Some(StopReason::EndTurn));
        assert_eq!(message.stop_sequence, Some("###".to_string()));

        assert_eq!(message.content.len(), 1);
        match &message.content[0] {
            ContentBlock::Text(text_block) => {
                assert_eq!(text_block.text, "Hello, I'm Claude.");
            }
            _ => panic!("Expected Text variant"),
        }

        match message.model {
            Model::Known(model) => {
                assert_eq!(model, crate::types::KnownModel::ClaudeSonnet46);
            }
            _ => panic!("Expected Known model variant"),
        }

        assert_eq!(message.usage.input_tokens, 50);
        assert_eq!(message.usage.output_tokens, 100);
        // We'll verify server_tool_use in a future PR when ServerToolUsage is properly implemented
    }
}