vtcode_core/llm/
provider.rs

1//! Universal LLM provider abstraction with API-specific role handling
2//!
3//! This module provides a unified interface for different LLM providers (OpenAI, Anthropic, Gemini)
4//! while properly handling their specific requirements for message roles and tool calling.
5//!
6//! ## Message Role Mapping
7//!
8//! Different LLM providers have varying support for message roles, especially for tool calling:
9//!
10//! ### OpenAI API
11//! - **Full Support**: `system`, `user`, `assistant`, `tool`
12//! - **Tool Messages**: Must include `tool_call_id` to reference the original tool call
13//! - **Tool Calls**: Only `assistant` messages can contain `tool_calls`
14//!
15//! ### Anthropic API
16//! - **Standard Roles**: `user`, `assistant`
17//! - **System Messages**: Can be hoisted to system parameter or treated as user messages
18//! - **Tool Responses**: Converted to `user` messages (no separate tool role)
19//! - **Tool Choice**: Supports `auto`, `any`, `tool`, `none` modes
20//!
21//! ### Gemini API
22//! - **Conversation Roles**: Only `user` and `model` (not `assistant`)
23//! - **System Messages**: Handled separately as `systemInstruction` parameter
24//! - **Tool Responses**: Converted to `user` messages with `functionResponse` format
25//! - **Function Calls**: Uses `functionCall` in `model` messages
26//!
27//! ## Best Practices
28//!
29//! 1. Always use `MessageRole::tool_response()` constructor for tool responses
30//! 2. Validate messages using `validate_for_provider()` before sending
31//! 3. Use appropriate role mapping methods for each provider
32//! 4. Handle provider-specific constraints (e.g., Gemini's system instruction requirement)
33//!
34//! ## Example Usage
35//!
36//! ```rust
37//! use vtcode_core::llm::provider::{Message, MessageRole};
38//!
39//! // Create a proper tool response message
40//! let tool_response = Message::tool_response(
41//!     "call_123".to_string(),
42//!     "Tool execution completed successfully".to_string()
43//! );
44//!
45//! // Validate for specific provider
46//! tool_response.validate_for_provider("openai").unwrap();
47//! ```
48
49use async_stream::try_stream;
50use async_trait::async_trait;
51use serde::{Deserialize, Serialize};
52use serde_json::{Value, json};
53use std::pin::Pin;
54
55/// Universal LLM request structure
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct LLMRequest {
58    pub messages: Vec<Message>,
59    pub system_prompt: Option<String>,
60    pub tools: Option<Vec<ToolDefinition>>,
61    pub model: String,
62    pub max_tokens: Option<u32>,
63    pub temperature: Option<f32>,
64    pub stream: bool,
65
66    /// Tool choice configuration based on official API docs
67    /// Supports: "auto" (default), "none", "any", or specific tool selection
68    pub tool_choice: Option<ToolChoice>,
69
70    /// Whether to enable parallel tool calls (OpenAI specific)
71    pub parallel_tool_calls: Option<bool>,
72
73    /// Parallel tool use configuration following Anthropic best practices
74    pub parallel_tool_config: Option<ParallelToolConfig>,
75
76    /// Reasoning effort level for models that support it (low, medium, high)
77    /// Applies to: Claude, GPT-5, Gemini, Qwen3, DeepSeek with reasoning capability
78    pub reasoning_effort: Option<String>,
79}
80
81/// Tool choice configuration that works across different providers
82/// Based on OpenAI, Anthropic, and Gemini API specifications
83/// Follows Anthropic's tool use best practices for optimal performance
84#[derive(Debug, Clone, Serialize, Deserialize)]
85#[serde(untagged)]
86pub enum ToolChoice {
87    /// Let the model decide whether to call tools ("auto")
88    /// Default behavior - allows model to use tools when appropriate
89    Auto,
90
91    /// Force the model to not call any tools ("none")
92    /// Useful for pure conversational responses without tool usage
93    None,
94
95    /// Force the model to call at least one tool ("any")
96    /// Ensures tool usage even when model might prefer direct response
97    Any,
98
99    /// Force the model to call a specific tool
100    /// Useful for directing model to use particular functionality
101    Specific(SpecificToolChoice),
102}
103
104/// Specific tool choice for forcing a particular function call
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct SpecificToolChoice {
107    #[serde(rename = "type")]
108    pub tool_type: String, // "function"
109
110    pub function: SpecificFunctionChoice,
111}
112
113/// Specific function choice details
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SpecificFunctionChoice {
116    pub name: String,
117}
118
119impl ToolChoice {
120    /// Create auto tool choice (default behavior)
121    pub fn auto() -> Self {
122        Self::Auto
123    }
124
125    /// Create none tool choice (disable tool calling)
126    pub fn none() -> Self {
127        Self::None
128    }
129
130    /// Create any tool choice (force at least one tool call)
131    pub fn any() -> Self {
132        Self::Any
133    }
134
135    /// Create specific function tool choice
136    pub fn function(name: String) -> Self {
137        Self::Specific(SpecificToolChoice {
138            tool_type: "function".to_string(),
139            function: SpecificFunctionChoice { name },
140        })
141    }
142
143    /// Check if this tool choice allows parallel tool use
144    /// Based on Anthropic's parallel tool use guidelines
145    pub fn allows_parallel_tools(&self) -> bool {
146        match self {
147            // Auto allows parallel tools by default
148            Self::Auto => true,
149            // Any forces at least one tool, may allow parallel
150            Self::Any => true,
151            // Specific forces one particular tool, typically no parallel
152            Self::Specific(_) => false,
153            // None disables tools entirely
154            Self::None => false,
155        }
156    }
157
158    /// Get human-readable description of tool choice behavior
159    pub fn description(&self) -> &'static str {
160        match self {
161            Self::Auto => "Model decides when to use tools (allows parallel)",
162            Self::None => "No tools will be used",
163            Self::Any => "At least one tool must be used (allows parallel)",
164            Self::Specific(_) => "Specific tool must be used (no parallel)",
165        }
166    }
167
168    /// Convert to provider-specific format
169    pub fn to_provider_format(&self, provider: &str) -> Value {
170        match (self, provider) {
171            (Self::Auto, "openai") | (Self::Auto, "deepseek") => json!("auto"),
172            (Self::None, "openai") | (Self::None, "deepseek") => json!("none"),
173            (Self::Any, "openai") | (Self::Any, "deepseek") => json!("required"),
174            (Self::Specific(choice), "openai") | (Self::Specific(choice), "deepseek") => {
175                json!(choice)
176            }
177
178            (Self::Auto, "anthropic") => json!({"type": "auto"}),
179            (Self::None, "anthropic") => json!({"type": "none"}),
180            (Self::Any, "anthropic") => json!({"type": "any"}),
181            (Self::Specific(choice), "anthropic") => {
182                json!({"type": "tool", "name": choice.function.name})
183            }
184
185            (Self::Auto, "gemini") => json!({"mode": "auto"}),
186            (Self::None, "gemini") => json!({"mode": "none"}),
187            (Self::Any, "gemini") => json!({"mode": "any"}),
188            (Self::Specific(choice), "gemini") => {
189                json!({"mode": "any", "allowed_function_names": [choice.function.name]})
190            }
191
192            // Generic follows OpenAI format
193            _ => match self {
194                Self::Auto => json!("auto"),
195                Self::None => json!("none"),
196                Self::Any => json!("required"),
197                Self::Specific(choice) => json!(choice),
198            },
199        }
200    }
201}
202
203/// Configuration for parallel tool use behavior
204/// Based on Anthropic's parallel tool use guidelines
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct ParallelToolConfig {
207    /// Whether to disable parallel tool use
208    /// When true, forces sequential tool execution
209    pub disable_parallel_tool_use: bool,
210
211    /// Maximum number of tools to execute in parallel
212    /// None means no limit (provider default)
213    pub max_parallel_tools: Option<usize>,
214
215    /// Whether to encourage parallel tool use in prompts
216    pub encourage_parallel: bool,
217}
218
219impl Default for ParallelToolConfig {
220    fn default() -> Self {
221        Self {
222            disable_parallel_tool_use: false,
223            max_parallel_tools: Some(5), // Reasonable default
224            encourage_parallel: true,
225        }
226    }
227}
228
229impl ParallelToolConfig {
230    /// Create configuration optimized for Anthropic models
231    pub fn anthropic_optimized() -> Self {
232        Self {
233            disable_parallel_tool_use: false,
234            max_parallel_tools: None, // Let Anthropic decide
235            encourage_parallel: true,
236        }
237    }
238
239    /// Create configuration for sequential tool use
240    pub fn sequential_only() -> Self {
241        Self {
242            disable_parallel_tool_use: true,
243            max_parallel_tools: Some(1),
244            encourage_parallel: false,
245        }
246    }
247}
248
249/// Universal message structure
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
251pub struct Message {
252    pub role: MessageRole,
253    pub content: String,
254    pub tool_calls: Option<Vec<ToolCall>>,
255    pub tool_call_id: Option<String>,
256}
257
258impl Message {
259    /// Create a user message
260    pub fn user(content: String) -> Self {
261        Self {
262            role: MessageRole::User,
263            content,
264            tool_calls: None,
265            tool_call_id: None,
266        }
267    }
268
269    /// Create an assistant message
270    pub fn assistant(content: String) -> Self {
271        Self {
272            role: MessageRole::Assistant,
273            content,
274            tool_calls: None,
275            tool_call_id: None,
276        }
277    }
278
279    /// Create an assistant message with tool calls
280    /// Based on OpenAI Cookbook patterns for function calling
281    pub fn assistant_with_tools(content: String, tool_calls: Vec<ToolCall>) -> Self {
282        Self {
283            role: MessageRole::Assistant,
284            content,
285            tool_calls: Some(tool_calls),
286            tool_call_id: None,
287        }
288    }
289
290    /// Create a system message
291    pub fn system(content: String) -> Self {
292        Self {
293            role: MessageRole::System,
294            content,
295            tool_calls: None,
296            tool_call_id: None,
297        }
298    }
299
300    /// Create a tool response message
301    /// This follows the exact pattern from OpenAI Cookbook:
302    /// ```json
303    /// {
304    ///   "role": "tool",
305    ///   "tool_call_id": "call_123",
306    ///   "content": "Function result"
307    /// }
308    /// ```
309    pub fn tool_response(tool_call_id: String, content: String) -> Self {
310        Self {
311            role: MessageRole::Tool,
312            content,
313            tool_calls: None,
314            tool_call_id: Some(tool_call_id),
315        }
316    }
317
318    /// Create a tool response message with function name (for compatibility)
319    /// Some providers might need the function name in addition to tool_call_id
320    pub fn tool_response_with_name(
321        tool_call_id: String,
322        _function_name: String,
323        content: String,
324    ) -> Self {
325        // We can store the function name in the content metadata or handle it provider-specifically
326        Self::tool_response(tool_call_id, content)
327    }
328
329    /// Validate this message for a specific provider
330    /// Based on official API documentation constraints
331    pub fn validate_for_provider(&self, provider: &str) -> Result<(), String> {
332        // Check role-specific constraints
333        self.role
334            .validate_for_provider(provider, self.tool_call_id.is_some())?;
335
336        // Check tool call constraints
337        if let Some(tool_calls) = &self.tool_calls {
338            if !self.role.can_make_tool_calls() {
339                return Err(format!("Role {:?} cannot make tool calls", self.role));
340            }
341
342            if tool_calls.is_empty() {
343                return Err("Tool calls array should not be empty".to_string());
344            }
345
346            // Validate each tool call
347            for tool_call in tool_calls {
348                tool_call.validate()?;
349            }
350        }
351
352        // Provider-specific validations based on official docs
353        match provider {
354            "openai" | "openrouter" => {
355                if self.role == MessageRole::Tool && self.tool_call_id.is_none() {
356                    return Err(format!(
357                        "{} requires tool_call_id for tool messages",
358                        provider
359                    ));
360                }
361            }
362            "gemini" => {
363                if self.role == MessageRole::Tool && self.tool_call_id.is_none() {
364                    return Err(
365                        "Gemini tool responses need tool_call_id for function name mapping"
366                            .to_string(),
367                    );
368                }
369                // Gemini has additional constraints on content structure
370                if self.role == MessageRole::System && !self.content.is_empty() {
371                    // System messages should be handled as systemInstruction, not in contents
372                }
373            }
374            "anthropic" => {
375                // Anthropic is more flexible with tool message format
376                // Tool messages are converted to user messages anyway
377            }
378            _ => {} // Generic validation already done above
379        }
380
381        Ok(())
382    }
383
384    /// Check if this message has tool calls
385    pub fn has_tool_calls(&self) -> bool {
386        self.tool_calls
387            .as_ref()
388            .map_or(false, |calls| !calls.is_empty())
389    }
390
391    /// Get the tool calls if present
392    pub fn get_tool_calls(&self) -> Option<&[ToolCall]> {
393        self.tool_calls.as_deref()
394    }
395
396    /// Check if this is a tool response message
397    pub fn is_tool_response(&self) -> bool {
398        self.role == MessageRole::Tool
399    }
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
403pub enum MessageRole {
404    System,
405    User,
406    Assistant,
407    Tool,
408}
409
410impl MessageRole {
411    /// Get the role string for Gemini API
412    /// Note: Gemini API has specific constraints on message roles
413    /// - Only accepts "user" and "model" roles in conversations
414    /// - System messages are handled separately as system instructions
415    /// - Tool responses are sent as "user" role with function response format
416    pub fn as_gemini_str(&self) -> &'static str {
417        match self {
418            MessageRole::System => "system", // Handled as systemInstruction, not in contents
419            MessageRole::User => "user",
420            MessageRole::Assistant => "model", // Gemini uses "model" instead of "assistant"
421            MessageRole::Tool => "user", // Tool responses are sent as user messages with functionResponse
422        }
423    }
424
425    /// Get the role string for OpenAI API
426    /// OpenAI supports all standard role types including:
427    /// - system, user, assistant, tool
428    /// - function (legacy, now replaced by tool)
429    pub fn as_openai_str(&self) -> &'static str {
430        match self {
431            MessageRole::System => "system",
432            MessageRole::User => "user",
433            MessageRole::Assistant => "assistant",
434            MessageRole::Tool => "tool", // Full support for tool role with tool_call_id
435        }
436    }
437
438    /// Get the role string for Anthropic API
439    /// Anthropic has specific handling for tool messages:
440    /// - Supports user, assistant roles normally
441    /// - Tool responses are treated as user messages
442    /// - System messages can be handled as system parameter or hoisted
443    pub fn as_anthropic_str(&self) -> &'static str {
444        match self {
445            MessageRole::System => "system", // Can be hoisted to system parameter
446            MessageRole::User => "user",
447            MessageRole::Assistant => "assistant",
448            MessageRole::Tool => "user", // Anthropic treats tool responses as user messages
449        }
450    }
451
452    /// Get the role string for generic OpenAI-compatible providers
453    /// Most providers follow OpenAI's role conventions
454    pub fn as_generic_str(&self) -> &'static str {
455        match self {
456            MessageRole::System => "system",
457            MessageRole::User => "user",
458            MessageRole::Assistant => "assistant",
459            MessageRole::Tool => "tool",
460        }
461    }
462
463    /// Check if this role supports tool calls
464    /// Only Assistant role can initiate tool calls in most APIs
465    pub fn can_make_tool_calls(&self) -> bool {
466        matches!(self, MessageRole::Assistant)
467    }
468
469    /// Check if this role represents a tool response
470    pub fn is_tool_response(&self) -> bool {
471        matches!(self, MessageRole::Tool)
472    }
473
474    /// Validate message role constraints for a given provider
475    /// Based on official API documentation requirements
476    pub fn validate_for_provider(
477        &self,
478        provider: &str,
479        has_tool_call_id: bool,
480    ) -> Result<(), String> {
481        match (self, provider) {
482            (MessageRole::Tool, provider)
483                if matches!(provider, "openai" | "openrouter" | "xai" | "deepseek")
484                    && !has_tool_call_id =>
485            {
486                Err(format!("{} tool messages must have tool_call_id", provider))
487            }
488            (MessageRole::Tool, "gemini") if !has_tool_call_id => {
489                Err("Gemini tool messages need tool_call_id for function mapping".to_string())
490            }
491            _ => Ok(()),
492        }
493    }
494}
495
496/// Universal tool definition that matches OpenAI/Anthropic/Gemini specifications
497/// Based on official API documentation from Context7
498#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct ToolDefinition {
500    /// The type of tool (always "function" for function calling)
501    #[serde(rename = "type")]
502    pub tool_type: String,
503
504    /// Function definition containing name, description, and parameters
505    pub function: FunctionDefinition,
506}
507
508/// Function definition within a tool
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct FunctionDefinition {
511    /// The name of the function to be called
512    pub name: String,
513
514    /// A description of what the function does
515    pub description: String,
516
517    /// The parameters the function accepts, described as a JSON Schema object
518    pub parameters: Value,
519}
520
521impl ToolDefinition {
522    /// Create a new tool definition with function type
523    pub fn function(name: String, description: String, parameters: Value) -> Self {
524        Self {
525            tool_type: "function".to_string(),
526            function: FunctionDefinition {
527                name,
528                description,
529                parameters,
530            },
531        }
532    }
533
534    /// Get the function name for easy access
535    pub fn function_name(&self) -> &str {
536        &self.function.name
537    }
538
539    /// Validate that this tool definition is properly formed
540    pub fn validate(&self) -> Result<(), String> {
541        if self.tool_type != "function" {
542            return Err(format!(
543                "Only 'function' type is supported, got: {}",
544                self.tool_type
545            ));
546        }
547
548        if self.function.name.is_empty() {
549            return Err("Function name cannot be empty".to_string());
550        }
551
552        if self.function.description.is_empty() {
553            return Err("Function description cannot be empty".to_string());
554        }
555
556        // Validate that parameters is a proper JSON Schema object
557        if !self.function.parameters.is_object() {
558            return Err("Function parameters must be a JSON object".to_string());
559        }
560
561        Ok(())
562    }
563}
564
565/// Universal tool call that matches the exact structure from OpenAI API
566/// Based on OpenAI Cookbook examples and official documentation
567#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
568pub struct ToolCall {
569    /// Unique identifier for this tool call (e.g., "call_123")
570    pub id: String,
571
572    /// The type of tool call (always "function" for function calling)
573    #[serde(rename = "type")]
574    pub call_type: String,
575
576    /// Function call details
577    pub function: FunctionCall,
578}
579
580/// Function call within a tool call
581#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
582pub struct FunctionCall {
583    /// The name of the function to call
584    pub name: String,
585
586    /// The arguments to pass to the function, as a JSON string
587    pub arguments: String,
588}
589
590impl ToolCall {
591    /// Create a new function tool call
592    pub fn function(id: String, name: String, arguments: String) -> Self {
593        Self {
594            id,
595            call_type: "function".to_string(),
596            function: FunctionCall { name, arguments },
597        }
598    }
599
600    /// Parse the arguments as JSON Value
601    pub fn parsed_arguments(&self) -> Result<Value, serde_json::Error> {
602        serde_json::from_str(&self.function.arguments)
603    }
604
605    /// Validate that this tool call is properly formed
606    pub fn validate(&self) -> Result<(), String> {
607        if self.call_type != "function" {
608            return Err(format!(
609                "Only 'function' type is supported, got: {}",
610                self.call_type
611            ));
612        }
613
614        if self.id.is_empty() {
615            return Err("Tool call ID cannot be empty".to_string());
616        }
617
618        if self.function.name.is_empty() {
619            return Err("Function name cannot be empty".to_string());
620        }
621
622        // Validate that arguments is valid JSON
623        if let Err(e) = self.parsed_arguments() {
624            return Err(format!("Invalid JSON in function arguments: {}", e));
625        }
626
627        Ok(())
628    }
629}
630
631/// Universal LLM response
632#[derive(Debug, Clone)]
633pub struct LLMResponse {
634    pub content: Option<String>,
635    pub tool_calls: Option<Vec<ToolCall>>,
636    pub usage: Option<Usage>,
637    pub finish_reason: FinishReason,
638    pub reasoning: Option<String>,
639}
640
641#[derive(Debug, Clone)]
642pub struct Usage {
643    pub prompt_tokens: u32,
644    pub completion_tokens: u32,
645    pub total_tokens: u32,
646    pub cached_prompt_tokens: Option<u32>,
647    pub cache_creation_tokens: Option<u32>,
648    pub cache_read_tokens: Option<u32>,
649}
650
651#[derive(Debug, Clone, PartialEq, Eq)]
652pub enum FinishReason {
653    Stop,
654    Length,
655    ToolCalls,
656    ContentFilter,
657    Error(String),
658}
659
660#[derive(Debug, Clone)]
661pub enum LLMStreamEvent {
662    Token { delta: String },
663    Reasoning { delta: String },
664    Completed { response: LLMResponse },
665}
666
667pub type LLMStream = Pin<Box<dyn futures::Stream<Item = Result<LLMStreamEvent, LLMError>> + Send>>;
668
669/// Universal LLM provider trait
670#[async_trait]
671pub trait LLMProvider: Send + Sync {
672    /// Provider name (e.g., "gemini", "openai", "anthropic")
673    fn name(&self) -> &str;
674
675    /// Whether the provider has native streaming support
676    fn supports_streaming(&self) -> bool {
677        false
678    }
679
680    /// Whether the provider surfaces structured reasoning traces for the given model
681    fn supports_reasoning(&self, _model: &str) -> bool {
682        false
683    }
684
685    /// Whether the provider accepts configurable reasoning effort for the model
686    fn supports_reasoning_effort(&self, _model: &str) -> bool {
687        false
688    }
689
690    /// Generate completion
691    async fn generate(&self, request: LLMRequest) -> Result<LLMResponse, LLMError>;
692
693    /// Stream completion (optional)
694    async fn stream(&self, request: LLMRequest) -> Result<LLMStream, LLMError> {
695        // Default implementation falls back to non-streaming
696        let response = self.generate(request).await?;
697        let stream = try_stream! {
698            yield LLMStreamEvent::Completed { response };
699        };
700        Ok(Box::pin(stream))
701    }
702
703    /// Get supported models
704    fn supported_models(&self) -> Vec<String>;
705
706    /// Validate request for this provider
707    fn validate_request(&self, request: &LLMRequest) -> Result<(), LLMError>;
708}
709
710#[derive(Debug, thiserror::Error)]
711pub enum LLMError {
712    #[error("Authentication failed: {0}")]
713    Authentication(String),
714    #[error("Rate limit exceeded")]
715    RateLimit,
716    #[error("Invalid request: {0}")]
717    InvalidRequest(String),
718    #[error("Network error: {0}")]
719    Network(String),
720    #[error("Provider error: {0}")]
721    Provider(String),
722}
723
724// Implement conversion from provider::LLMError to llm::types::LLMError
725impl From<LLMError> for crate::llm::types::LLMError {
726    fn from(err: LLMError) -> crate::llm::types::LLMError {
727        match err {
728            LLMError::Authentication(msg) => crate::llm::types::LLMError::ApiError(msg),
729            LLMError::RateLimit => crate::llm::types::LLMError::RateLimit,
730            LLMError::InvalidRequest(msg) => crate::llm::types::LLMError::InvalidRequest(msg),
731            LLMError::Network(msg) => crate::llm::types::LLMError::NetworkError(msg),
732            LLMError::Provider(msg) => crate::llm::types::LLMError::ApiError(msg),
733        }
734    }
735}