xz-provider 0.5.0

LLM 服务提供者抽象层 — 统一的 LLM 服务提供者接口
Documentation
use serde::{Deserialize, Serialize};

/// 输出配置 — 控制 Anthropic 模型输出形态和资源分配
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
    /// 推理投入程度(Anthropic effort level)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub effort: Option<EffortLevel>,
}

/// 推理投入程度(Anthropic-specific,不等同于 OpenAI reasoning_effort)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum EffortLevel {
    Low,
    Medium,
    High,
    #[serde(rename = "xhigh", alias = "x_high")]
    XHigh,
    Max,
}

/// 文档引用配置(per-document citation toggle)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CitationConfig {
    /// 是否允许模型引用当前文档
    pub enabled: bool,
}

/// 引用来源位置(Anthropic Citations 功能)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Citation {
    #[serde(rename = "char_location")]
    CharLocation { cited_text: String, start_index: u32, end_index: u32, document_index: u32 },
    #[serde(rename = "page_location")]
    PageLocation {
        cited_text: String,
        start_page_number: u32,
        end_page_number: u32,
        document_index: u32,
    },
    #[serde(rename = "content_block_location")]
    ContentBlockLocation {
        cited_text: String,
        start_block_index: u32,
        end_block_index: u32,
        document_index: u32,
    },
    #[serde(rename = "search_result_location")]
    SearchResultLocation {
        cited_text: String,
        start_index: u32,
        end_index: u32,
        search_result_index: u32,
    },
    #[serde(rename = "web_search_result_location")]
    WebSearchResultLocation {
        cited_text: String,
        encrypted_index: String,
        title: String,
        url: String,
    },
}

/// 思考内容块(含签名)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThinkingBlock {
    /// 思考链文本
    pub thinking: String,
    /// 签名(用于后续对话连续性验证)
    pub signature: String,
}

/// 加密思考块(API 返回已删节的加密内容)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedactedThinkingBlock {
    /// 加密数据
    pub data: String,
}

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

    // ── EffortLevel ───────────────────────────────────────────

    #[test]
    fn effort_level_low_round_trip() {
        let value = EffortLevel::Low;
        let json = serde_json::to_string(&value).unwrap();
        assert_eq!(json, r#""low""#);
        let deserialized: EffortLevel = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, EffortLevel::Low);
    }

    #[test]
    fn effort_level_medium_round_trip() {
        let value = EffortLevel::Medium;
        let json = serde_json::to_string(&value).unwrap();
        assert_eq!(json, r#""medium""#);
        let deserialized: EffortLevel = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, EffortLevel::Medium);
    }

    #[test]
    fn effort_level_high_round_trip() {
        let value = EffortLevel::High;
        let json = serde_json::to_string(&value).unwrap();
        assert_eq!(json, r#""high""#);
        let deserialized: EffortLevel = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, EffortLevel::High);
    }

    #[test]
    fn effort_level_xhigh_round_trip() {
        let value = EffortLevel::XHigh;
        let json = serde_json::to_string(&value).unwrap();
        assert_eq!(json, r#""xhigh""#);
        let deserialized: EffortLevel = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, EffortLevel::XHigh);
    }

    #[test]
    fn effort_level_xhigh_deserialize_x_high_alias() {
        // Backward compat: "x_high" should still deserialize to XHigh
        let deserialized: EffortLevel = serde_json::from_str(r#""x_high""#).unwrap();
        assert_eq!(deserialized, EffortLevel::XHigh);
    }

    #[test]
    fn effort_level_max_round_trip() {
        let value = EffortLevel::Max;
        let json = serde_json::to_string(&value).unwrap();
        assert_eq!(json, r#""max""#);
        let deserialized: EffortLevel = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, EffortLevel::Max);
    }

    // ── OutputConfig ──────────────────────────────────────────

    #[test]
    fn output_config_with_effort() {
        let config = OutputConfig { effort: Some(EffortLevel::High) };
        let json = serde_json::to_string(&config).unwrap();
        assert_eq!(json, r#"{"effort":"high"}"#);
        let deserialized: OutputConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.effort, Some(EffortLevel::High));
    }

    #[test]
    fn output_config_without_effort() {
        let config = OutputConfig { effort: None };
        let json = serde_json::to_string(&config).unwrap();
        assert_eq!(json, r#"{}"#);
        let deserialized: OutputConfig = serde_json::from_str(&json).unwrap();
        assert!(deserialized.effort.is_none());
    }

    // ── CitationConfig ────────────────────────────────────────

    #[test]
    fn citation_config_true() {
        let config = CitationConfig { enabled: true };
        let json = serde_json::to_string(&config).unwrap();
        assert_eq!(json, r#"{"enabled":true}"#);
        let deserialized: CitationConfig = serde_json::from_str(&json).unwrap();
        assert!(deserialized.enabled);
    }

    #[test]
    fn citation_config_false() {
        let config = CitationConfig { enabled: false };
        let json = serde_json::to_string(&config).unwrap();
        assert_eq!(json, r#"{"enabled":false}"#);
        let deserialized: CitationConfig = serde_json::from_str(&json).unwrap();
        assert!(!deserialized.enabled);
    }

    // ── Citation ──────────────────────────────────────────────

    #[test]
    fn citation_char_location_round_trip() {
        let value = Citation::CharLocation {
            cited_text: "foo".into(),
            start_index: 10,
            end_index: 13,
            document_index: 0,
        };
        let json = serde_json::to_string(&value).unwrap();
        let expected = r#"{"type":"char_location","cited_text":"foo","start_index":10,"end_index":13,"document_index":0}"#;
        assert_eq!(json, expected);
        let deserialized: Citation = serde_json::from_str(&json).unwrap();
        assert!(matches!(deserialized, Citation::CharLocation { .. }));
    }

    #[test]
    fn citation_page_location_round_trip() {
        let value = Citation::PageLocation {
            cited_text: "bar".into(),
            start_page_number: 1,
            end_page_number: 2,
            document_index: 0,
        };
        let json = serde_json::to_string(&value).unwrap();
        let expected = r#"{"type":"page_location","cited_text":"bar","start_page_number":1,"end_page_number":2,"document_index":0}"#;
        assert_eq!(json, expected);
        let deserialized: Citation = serde_json::from_str(&json).unwrap();
        assert!(matches!(deserialized, Citation::PageLocation { .. }));
    }

    #[test]
    fn citation_content_block_location_round_trip() {
        let value = Citation::ContentBlockLocation {
            cited_text: "baz".into(),
            start_block_index: 0,
            end_block_index: 1,
            document_index: 0,
        };
        let json = serde_json::to_string(&value).unwrap();
        let expected = r#"{"type":"content_block_location","cited_text":"baz","start_block_index":0,"end_block_index":1,"document_index":0}"#;
        assert_eq!(json, expected);
        let deserialized: Citation = serde_json::from_str(&json).unwrap();
        assert!(matches!(deserialized, Citation::ContentBlockLocation { .. }));
    }

    #[test]
    fn citation_search_result_location_round_trip() {
        let value = Citation::SearchResultLocation {
            cited_text: "qux".into(),
            start_index: 0,
            end_index: 3,
            search_result_index: 1,
        };
        let json = serde_json::to_string(&value).unwrap();
        let expected = r#"{"type":"search_result_location","cited_text":"qux","start_index":0,"end_index":3,"search_result_index":1}"#;
        assert_eq!(json, expected);
        let deserialized: Citation = serde_json::from_str(&json).unwrap();
        assert!(matches!(deserialized, Citation::SearchResultLocation { .. }));
    }

    #[test]
    fn citation_web_search_result_location_round_trip() {
        let value = Citation::WebSearchResultLocation {
            cited_text: "hello".into(),
            encrypted_index: "abc123".into(),
            title: "Example".into(),
            url: "https://example.com".into(),
        };
        let json = serde_json::to_string(&value).unwrap();
        let expected = r#"{"type":"web_search_result_location","cited_text":"hello","encrypted_index":"abc123","title":"Example","url":"https://example.com"}"#;
        assert_eq!(json, expected);
        let deserialized: Citation = serde_json::from_str(&json).unwrap();
        assert!(matches!(deserialized, Citation::WebSearchResultLocation { .. }));
    }

    // ── ThinkingBlock ─────────────────────────────────────────

    #[test]
    fn thinking_block_round_trip() {
        let value = ThinkingBlock {
            thinking: "Let me reason about this...".into(),
            signature: "sig_abc123".into(),
        };
        let json = serde_json::to_string(&value).unwrap();
        let expected = r#"{"thinking":"Let me reason about this...","signature":"sig_abc123"}"#;
        assert_eq!(json, expected);
        let deserialized: ThinkingBlock = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.thinking, "Let me reason about this...");
        assert_eq!(deserialized.signature, "sig_abc123");
    }

    // ── RedactedThinkingBlock ─────────────────────────────────

    #[test]
    fn redacted_thinking_block_round_trip() {
        let value = RedactedThinkingBlock { data: "encrypted_data_here".into() };
        let json = serde_json::to_string(&value).unwrap();
        let expected = r#"{"data":"encrypted_data_here"}"#;
        assert_eq!(json, expected);
        let deserialized: RedactedThinkingBlock = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.data, "encrypted_data_here");
    }
}