struct_llm/
provider.rs

1//! Provider-specific adapters for different LLM APIs
2
3/// Supported LLM API providers
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Provider {
6    /// OpenAI API (chat.openai.com)
7    OpenAI,
8
9    /// Anthropic API (claude.ai)
10    Anthropic,
11
12    /// Local or generic OpenAI-compatible API
13    Local,
14}
15
16impl Provider {
17    /// Returns the expected format for tool definitions
18    pub fn tool_format(&self) -> ToolFormat {
19        match self {
20            Provider::OpenAI | Provider::Local => ToolFormat::OpenAI,
21            Provider::Anthropic => ToolFormat::Anthropic,
22        }
23    }
24}
25
26/// Different tool definition formats used by providers
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ToolFormat {
29    /// OpenAI format: { "type": "function", "function": { "name": "...", "parameters": {...} } }
30    OpenAI,
31
32    /// Anthropic format: { "name": "...", "description": "...", "input_schema": {...} }
33    Anthropic,
34}
35
36/// Helper to build request bodies with tools for different providers
37pub fn build_request_with_tools(
38    messages: &[crate::Message],
39    tools: &[crate::ToolDefinition],
40    provider: Provider,
41) -> serde_json::Value {
42    match provider {
43        Provider::OpenAI | Provider::Local => build_openai_request(messages, tools),
44        Provider::Anthropic => build_anthropic_request(messages, tools),
45    }
46}
47
48/// Build a request that enforces a specific tool call (like pydantic AI / luagent pattern)
49///
50/// This is the recommended approach for structured outputs - it guarantees the LLM
51/// will call the specified tool, ensuring you always get back your structured data.
52///
53/// # Example
54///
55/// ```ignore
56/// use struct_llm::{build_enforced_tool_request, Provider, StructuredOutput};
57///
58/// let tool = MyOutput::tool_definition();
59/// let request = build_enforced_tool_request(
60///     &messages,
61///     &tool,
62///     Provider::OpenAI
63/// );
64/// // The LLM will be forced to call MyOutput's tool
65/// ```
66pub fn build_enforced_tool_request(
67    messages: &[crate::Message],
68    tool: &crate::ToolDefinition,
69    provider: Provider,
70) -> serde_json::Value {
71    match provider {
72        Provider::OpenAI | Provider::Local => {
73            let mut request = build_openai_request(messages, &[tool.clone()]);
74            // Force this specific tool to be called
75            request["tool_choice"] = serde_json::json!({
76                "type": "function",
77                "function": {
78                    "name": tool.name
79                }
80            });
81            request
82        }
83        Provider::Anthropic => {
84            let mut request = build_anthropic_request(messages, &[tool.clone()]);
85            // Force this specific tool to be called
86            request["tool_choice"] = serde_json::json!({
87                "type": "tool",
88                "name": tool.name
89            });
90            request
91        }
92    }
93}
94
95fn build_openai_request(
96    messages: &[crate::Message],
97    tools: &[crate::ToolDefinition],
98) -> serde_json::Value {
99    let mut request = serde_json::json!({
100        "messages": messages,
101    });
102
103    if !tools.is_empty() {
104        let formatted_tools: Vec<_> = tools
105            .iter()
106            .map(|tool| {
107                serde_json::json!({
108                    "type": "function",
109                    "function": {
110                        "name": tool.name,
111                        "description": tool.description,
112                        "parameters": tool.parameters,
113                    }
114                })
115            })
116            .collect();
117
118        request["tools"] = serde_json::json!(formatted_tools);
119    }
120
121    request
122}
123
124fn build_anthropic_request(
125    messages: &[crate::Message],
126    tools: &[crate::ToolDefinition],
127) -> serde_json::Value {
128    let mut request = serde_json::json!({
129        "messages": messages,
130    });
131
132    if !tools.is_empty() {
133        let formatted_tools: Vec<_> = tools
134            .iter()
135            .map(|tool| {
136                serde_json::json!({
137                    "name": tool.name,
138                    "description": tool.description,
139                    "input_schema": tool.parameters,
140                })
141            })
142            .collect();
143
144        request["tools"] = serde_json::json!(formatted_tools);
145    }
146
147    request
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_provider_tool_format() {
156        assert_eq!(Provider::OpenAI.tool_format(), ToolFormat::OpenAI);
157        assert_eq!(Provider::Anthropic.tool_format(), ToolFormat::Anthropic);
158        assert_eq!(Provider::Local.tool_format(), ToolFormat::OpenAI);
159    }
160}