Skip to main content

vtcode_core/llm/
rig_adapter.rs

1use crate::config::models::Provider;
2use crate::config::types::ReasoningEffortLevel;
3use anyhow::Result;
4use rig::client::CompletionClient;
5use rig::providers::gemini::completion::gemini_api_types::ThinkingConfig;
6use rig::providers::{anthropic, deepseek, gemini, openai, openrouter};
7use serde_json::{Value, json};
8
9/// Result of validating a provider/model combination through rig-core.
10#[derive(Debug, Clone)]
11pub struct RigValidationSummary {
12    pub provider: Provider,
13    pub model: String,
14}
15
16/// Internal bridge for Rig-backed provider/model capability checks.
17#[derive(Debug, Clone)]
18pub struct RigProviderCapabilities {
19    provider: Provider,
20    model: String,
21}
22
23impl RigProviderCapabilities {
24    #[must_use]
25    pub fn new(provider: Provider, model: impl Into<String>) -> Self {
26        Self {
27            provider,
28            model: model.into(),
29        }
30    }
31
32    /// Attempt to construct a rig-core client for the given provider and
33    /// instantiate the requested model. This performs a lightweight validation
34    /// without issuing a network request, ensuring that downstream calls can
35    /// reuse the rig client configuration paths.
36    pub fn validate_model(&self, api_key: &str) -> Result<RigValidationSummary> {
37        match self.provider {
38            Provider::Gemini => {
39                let client = gemini::Client::new(api_key);
40                let _ = client.completion_model(&self.model);
41            }
42            Provider::OpenAI => {
43                let client = openai::Client::new(api_key);
44                let _ = client.completion_model(&self.model);
45            }
46            Provider::Anthropic => {
47                let client = anthropic::Client::new(api_key);
48                let _ = client.completion_model(&self.model);
49            }
50            Provider::Copilot => {
51                // Copilot is authenticated through the official CLI, not rig.
52            }
53            Provider::Minimax => {
54                // MiniMax uses an Anthropic-compatible API; rig has no direct client.
55            }
56            Provider::DeepSeek => {
57                let client = deepseek::Client::new(api_key);
58                let _ = client.completion_model(&self.model);
59            }
60            Provider::HuggingFace => {
61                // Hugging Face exposes an OpenAI-compatible router; rig does not ship a dedicated client.
62            }
63            Provider::OpenRouter => {
64                let client = openrouter::Client::new(api_key);
65                let _ = client.completion_model(&self.model);
66            }
67            Provider::Ollama => {
68                // Rig does not provide an Ollama integration; validation is skipped.
69            }
70            Provider::LlamaCpp => {
71                // llama.cpp provides an OpenAI-compatible API; rig has no direct client.
72            }
73            Provider::LmStudio => {
74                // LM Studio provides an OpenAI-compatible API; rig has no direct client.
75            }
76            Provider::Moonshot => {
77                // Moonshot does not have a rig client integration yet.
78            }
79            Provider::Mistral => {
80                // Mistral exposes an OpenAI-compatible API; rig has no dedicated client.
81            }
82            Provider::ZAI => {
83                // The rig crate does not yet expose a dedicated Z.AI client.
84                // Skip instantiation while still marking the provider as verified.
85            }
86            Provider::OpenCodeZen | Provider::OpenCodeGo => {
87                // OpenCode Zen/Go are API gateways; rig has no direct client.
88            }
89            Provider::MiMo => {
90                // Xiaomi MiMo is an OpenAI-compatible API; rig has no dedicated client.
91            }
92            Provider::Qwen => {
93                // Alibaba Cloud Qwen uses an OpenAI-compatible API; rig has no dedicated client.
94            }
95            Provider::StepFun => {
96                // StepFun uses an OpenAI-compatible API; rig has no dedicated client.
97            }
98            Provider::Evolink => {
99                // Evolink is an OpenAI-compatible gateway; rig has no dedicated client.
100            }
101            Provider::Poolside => {
102                // Poolside uses an OpenAI-compatible API; rig has no dedicated client.
103            }
104        }
105
106        Ok(RigValidationSummary {
107            provider: self.provider,
108            model: self.model.clone(),
109        })
110    }
111
112    /// Convert a VT Code reasoning effort level to provider-specific parameters
113    /// using rig-core data structures. The resulting JSON payload can be merged
114    /// into provider requests when supported.
115    #[must_use]
116    pub fn reasoning_parameters(&self, effort: ReasoningEffortLevel) -> Option<Value> {
117        match self.provider {
118            Provider::OpenAI => {
119                let mut reasoning = openai::responses_api::Reasoning::new();
120                let mapped = match effort {
121                    ReasoningEffortLevel::None => return None,
122                    ReasoningEffortLevel::Minimal => {
123                        let effort = if is_gpt5_codex_model(&self.model) {
124                            "low"
125                        } else {
126                            "minimal"
127                        };
128                        return Some(json!({ "effort": effort }));
129                    }
130                    ReasoningEffortLevel::Low => openai::responses_api::ReasoningEffort::Low,
131                    ReasoningEffortLevel::Medium => openai::responses_api::ReasoningEffort::Medium,
132                    ReasoningEffortLevel::High => openai::responses_api::ReasoningEffort::High,
133                    ReasoningEffortLevel::XHigh => return Some(json!({ "effort": "xhigh" })),
134                    ReasoningEffortLevel::Max => return Some(json!({ "effort": "xhigh" })),
135                };
136                reasoning = reasoning.with_effort(mapped);
137                serde_json::to_value(reasoning).ok()
138            }
139            Provider::Gemini => {
140                let include_thoughts = matches!(
141                    effort,
142                    ReasoningEffortLevel::High
143                        | ReasoningEffortLevel::XHigh
144                        | ReasoningEffortLevel::Max
145                );
146                let budget = match effort {
147                    ReasoningEffortLevel::None => return None,
148                    ReasoningEffortLevel::Minimal => 16,
149                    ReasoningEffortLevel::Low => 64,
150                    ReasoningEffortLevel::Medium => 128,
151                    ReasoningEffortLevel::High
152                    | ReasoningEffortLevel::XHigh
153                    | ReasoningEffortLevel::Max => 256,
154                };
155                let config = ThinkingConfig {
156                    thinking_budget: budget,
157                    include_thoughts: Some(include_thoughts),
158                };
159                serde_json::to_value(config)
160                    .ok()
161                    .map(|value| json!({ "thinking_config": value }))
162            }
163            Provider::HuggingFace => match effort {
164                ReasoningEffortLevel::None => None,
165                ReasoningEffortLevel::Minimal => Some(json!({ "reasoning_effort": "minimal" })),
166                ReasoningEffortLevel::Low => Some(json!({ "reasoning_effort": "low" })),
167                ReasoningEffortLevel::Medium => Some(json!({ "reasoning_effort": "medium" })),
168                ReasoningEffortLevel::High
169                | ReasoningEffortLevel::XHigh
170                | ReasoningEffortLevel::Max => Some(json!({ "reasoning_effort": "high" })),
171            },
172            // DeepSeek only accepts `high` and `max` for reasoning_effort.
173            // Per DeepSeek docs: low/medium → high, xhigh → max.
174            Provider::DeepSeek => match effort {
175                ReasoningEffortLevel::None => None,
176                ReasoningEffortLevel::Minimal
177                | ReasoningEffortLevel::Low
178                | ReasoningEffortLevel::Medium
179                | ReasoningEffortLevel::High => {
180                    Some(json!({"thinking": {"type": "enabled"}, "reasoning_effort": "high"}))
181                }
182                ReasoningEffortLevel::XHigh | ReasoningEffortLevel::Max => {
183                    Some(json!({"thinking": {"type": "enabled"}, "reasoning_effort": "max"}))
184                }
185            },
186            Provider::Minimax => None,
187            Provider::Ollama => None,
188            Provider::LlamaCpp => None,
189            Provider::ZAI => match effort {
190                ReasoningEffortLevel::None => None,
191                ReasoningEffortLevel::Minimal => Some(json!({
192                    "thinking": { "type": "enabled" },
193                    "thinking_effort": "minimal"
194                })),
195                ReasoningEffortLevel::Low => Some(json!({
196                    "thinking": { "type": "enabled" },
197                    "thinking_effort": "low"
198                })),
199                ReasoningEffortLevel::Medium => Some(json!({
200                    "thinking": { "type": "enabled" },
201                    "thinking_effort": "medium"
202                })),
203                ReasoningEffortLevel::High
204                | ReasoningEffortLevel::XHigh
205                | ReasoningEffortLevel::Max => Some(json!({
206                    "thinking": { "type": "enabled" },
207                    "thinking_effort": "high"
208                })),
209            },
210            Provider::StepFun => match effort {
211                ReasoningEffortLevel::None => None,
212                ReasoningEffortLevel::Minimal | ReasoningEffortLevel::Low => {
213                    Some(json!({ "reasoning_effort": "low" }))
214                }
215                ReasoningEffortLevel::Medium => Some(json!({ "reasoning_effort": "medium" })),
216                ReasoningEffortLevel::High
217                | ReasoningEffortLevel::XHigh
218                | ReasoningEffortLevel::Max => Some(json!({ "reasoning_effort": "high" })),
219            },
220            Provider::Evolink => match effort {
221                ReasoningEffortLevel::None => None,
222                ReasoningEffortLevel::Minimal | ReasoningEffortLevel::Low => {
223                    Some(json!({ "reasoning_effort": "low" }))
224                }
225                ReasoningEffortLevel::Medium => Some(json!({ "reasoning_effort": "medium" })),
226                ReasoningEffortLevel::High
227                | ReasoningEffortLevel::XHigh
228                | ReasoningEffortLevel::Max => Some(json!({ "reasoning_effort": "high" })),
229            },
230            _ => None,
231        }
232    }
233}
234
235fn is_gpt5_codex_model(model: &str) -> bool {
236    model == "gpt-5-codex" || (model.starts_with("gpt-5.") && model.contains("codex"))
237}
238
239#[cfg(test)]
240mod tests {
241    use super::RigProviderCapabilities;
242    use crate::config::models::Provider;
243    use crate::config::types::ReasoningEffortLevel;
244
245    #[test]
246    fn rig_capabilities_validate_rig_backed_and_noop_providers() {
247        let openai = RigProviderCapabilities::new(Provider::OpenAI, "gpt-5")
248            .validate_model("test-key")
249            .expect("openai validation");
250        assert_eq!(openai.provider, Provider::OpenAI);
251        assert_eq!(openai.model, "gpt-5");
252
253        let deepseek = RigProviderCapabilities::new(Provider::DeepSeek, "deepseek-chat")
254            .validate_model("test-key")
255            .expect("no-op validation");
256        assert_eq!(deepseek.provider, Provider::DeepSeek);
257        assert_eq!(deepseek.model, "deepseek-chat");
258    }
259
260    #[test]
261    fn rig_capabilities_generate_reasoning_payload_for_supported_provider() {
262        let payload = RigProviderCapabilities::new(Provider::ZAI, "glm-5")
263            .reasoning_parameters(ReasoningEffortLevel::Medium)
264            .expect("reasoning payload");
265
266        assert_eq!(payload["thinking"]["type"], "enabled");
267        assert_eq!(payload["thinking_effort"], "medium");
268    }
269
270    #[test]
271    fn rig_capabilities_skip_reasoning_payload_for_unsupported_provider() {
272        assert!(
273            RigProviderCapabilities::new(Provider::Ollama, "qwen")
274                .reasoning_parameters(ReasoningEffortLevel::High)
275                .is_none()
276        );
277    }
278}