xz-provider 0.5.0

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

/// 工具定义 —— 告诉 LLM "你可以调用哪些工具"
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
    pub name: String,
    pub description: String,
    /// JSON Schema 描述的参数结构
    pub parameters: Value,
    /// 强制 JSON 输出合规(OpenAI strict mode)
    /// Provider 实现层对不支持 strict 的 provider 静默忽略
    #[serde(skip_serializing_if = "Option::is_none")]
    pub strict: Option<bool>,
}

/// LLM 返回的工具调用
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "ToolCallWire")]
pub struct ToolCall {
    pub id: String,
    pub function_name: String,
    pub arguments: Value,
}

impl Serialize for ToolCall {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let wire = ToolCallWire {
            id: self.id.clone(),
            tool_type: "function".to_string(),
            function: ToolCallFunctionWire {
                name: self.function_name.clone(),
                arguments: serde_json::to_string(&self.arguments).unwrap_or_default(),
            },
        };
        wire.serialize(serializer)
    }
}

#[derive(Serialize, Deserialize)]
struct ToolCallWire {
    id: String,
    #[serde(rename = "type")]
    tool_type: String,
    function: ToolCallFunctionWire,
}

#[derive(Serialize, Deserialize)]
struct ToolCallFunctionWire {
    name: String,
    arguments: String,
}

impl From<ToolCallWire> for ToolCall {
    fn from(wire: ToolCallWire) -> Self {
        ToolCall {
            id: wire.id,
            function_name: wire.function.name,
            arguments: serde_json::from_str(&wire.function.arguments).unwrap_or(Value::Null),
        }
    }
}

/// 工具调用结果 —— 执行完工具后塞回 messages
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
    /// 对应 ToolCall.id
    pub tool_call_id: String,
    /// 工具返回内容
    pub content: String,
    /// 工具执行是否失败
    #[serde(default)]
    pub is_error: bool,
}

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

    fn test_tool_definition() -> ToolDefinition {
        ToolDefinition {
            name: "search".to_string(),
            description: "Search the web".to_string(),
            parameters: serde_json::json!({
                "type": "object",
                "properties": {
                    "query": { "type": "string" }
                },
                "required": ["query"]
            }),
            strict: None,
        }
    }

    #[test]
    fn test_tool_definition_with_strict_true() {
        let mut td = test_tool_definition();
        td.strict = Some(true);

        let json = serde_json::to_string(&td).unwrap();
        assert!(
            json.contains(r#""strict":true"#),
            "Expected 'strict': true in JSON output, got: {json}"
        );

        let deserialized: ToolDefinition = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.strict, Some(true));
        assert_eq!(deserialized.name, "search");
        assert_eq!(deserialized.description, "Search the web");
    }

    #[test]
    fn test_tool_definition_without_strict() {
        let td = test_tool_definition();
        assert!(td.strict.is_none());

        let json = serde_json::to_string(&td).unwrap();
        assert!(!json.contains("strict"), "Expected no 'strict' key in JSON output, got: {json}");

        let deserialized: ToolDefinition = serde_json::from_str(&json).unwrap();
        assert!(deserialized.strict.is_none());
        assert_eq!(deserialized.name, "search");
        assert_eq!(deserialized.description, "Search the web");
    }

    #[test]
    fn test_tool_definition_round_trip() {
        let td = test_tool_definition();
        let json = serde_json::to_string(&td).unwrap();
        let deserialized: ToolDefinition = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.name, td.name);
        assert_eq!(deserialized.description, td.description);
        assert_eq!(deserialized.parameters, td.parameters);
        assert_eq!(deserialized.strict, td.strict);
    }
}