anthropic-async 0.5.2

Anthropic API client for Rust with prompt caching support
Documentation
use serde::Deserialize;
use serde::Serialize;

/// Cache time-to-live for prompt caching
///
/// See the [Anthropic prompt caching documentation](https://docs.anthropic.com/en/docs/prompt-caching)
/// for details on caching behavior.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CacheTtl {
    /// 5-minute cache TTL
    #[serde(rename = "5m")]
    FiveMinutes,
    /// 1-hour cache TTL
    #[serde(rename = "1h")]
    OneHour,
}

/// Cache control configuration for content blocks
///
/// Used to enable prompt caching on specific content blocks. When mixing TTLs in a single request,
/// 1-hour cache entries must appear before 5-minute entries.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CacheControl {
    /// Cache type (currently only "ephemeral" is supported)
    #[serde(rename = "type")]
    pub kind: String,
    /// Time-to-live for the cache entry
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl: Option<CacheTtl>,
}

impl CacheControl {
    /// Creates an ephemeral cache control with 5-minute TTL
    #[must_use]
    pub fn ephemeral_5m() -> Self {
        Self {
            kind: "ephemeral".into(),
            ttl: Some(CacheTtl::FiveMinutes),
        }
    }

    /// Creates an ephemeral cache control with 1-hour TTL
    #[must_use]
    pub fn ephemeral_1h() -> Self {
        Self {
            kind: "ephemeral".into(),
            ttl: Some(CacheTtl::OneHour),
        }
    }

    /// Creates an ephemeral cache control with default TTL (5 minutes)
    #[must_use]
    pub fn ephemeral() -> Self {
        Self {
            kind: "ephemeral".into(),
            ttl: None,
        }
    }
}

/// Validate that when mixing TTLs, `OneHour` entries appear before `FiveMinutes`.
#[must_use]
pub fn validate_mixed_ttl_order(block_ttls: impl IntoIterator<Item = CacheTtl>) -> bool {
    let mut seen_5m = false;
    for ttl in block_ttls {
        match ttl {
            CacheTtl::OneHour if seen_5m => return false, // 1h after 5m → invalid
            CacheTtl::FiveMinutes => seen_5m = true,
            CacheTtl::OneHour => {}
        }
    }
    true
}

/// Metadata for requests
///
/// Used to pass additional information about the request.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct Metadata {
    /// User ID for tracking purposes
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user_id: Option<String>,
}

/// Token usage information for a request/response
///
/// Includes cache-related token counts when prompt caching is used.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct Usage {
    /// Number of input tokens processed
    pub input_tokens: Option<u64>,
    /// Number of output tokens generated
    pub output_tokens: Option<u64>,
    /// Number of tokens used to create cache entries
    pub cache_creation_input_tokens: Option<u64>,
    /// Number of tokens read from cache
    pub cache_read_input_tokens: Option<u64>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ttl_ser_de() {
        let s = serde_json::to_string(&CacheTtl::FiveMinutes).unwrap();
        assert_eq!(s, r#""5m""#);
        let t: CacheTtl = serde_json::from_str(r#""1h""#).unwrap();
        assert_eq!(t, CacheTtl::OneHour);
    }

    #[test]
    fn cache_control_ser() {
        let cc = CacheControl::ephemeral_5m();
        let s = serde_json::to_string(&cc).unwrap();
        assert!(s.contains(r#""type":"ephemeral""#));
        assert!(s.contains(r#""ttl":"5m""#));
    }

    #[test]
    fn ordering_valid() {
        assert!(validate_mixed_ttl_order([
            CacheTtl::OneHour,
            CacheTtl::FiveMinutes
        ]));
        assert!(validate_mixed_ttl_order([CacheTtl::FiveMinutes]));
        assert!(validate_mixed_ttl_order([CacheTtl::OneHour]));
        assert!(!validate_mixed_ttl_order([
            CacheTtl::FiveMinutes,
            CacheTtl::OneHour
        ]));
    }
}