vtcode_core/llm/
rig_adapter.rs1use 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#[derive(Debug, Clone)]
11pub struct RigValidationSummary {
12 pub provider: Provider,
13 pub model: String,
14}
15
16#[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 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 }
53 Provider::Minimax => {
54 }
56 Provider::DeepSeek => {
57 let client = deepseek::Client::new(api_key);
58 let _ = client.completion_model(&self.model);
59 }
60 Provider::HuggingFace => {
61 }
63 Provider::OpenRouter => {
64 let client = openrouter::Client::new(api_key);
65 let _ = client.completion_model(&self.model);
66 }
67 Provider::Ollama => {
68 }
70 Provider::LlamaCpp => {
71 }
73 Provider::LmStudio => {
74 }
76 Provider::Moonshot => {
77 }
79 Provider::Mistral => {
80 }
82 Provider::ZAI => {
83 }
86 Provider::OpenCodeZen | Provider::OpenCodeGo => {
87 }
89 Provider::MiMo => {
90 }
92 Provider::Qwen => {
93 }
95 Provider::StepFun => {
96 }
98 Provider::Evolink => {
99 }
101 Provider::Poolside => {
102 }
104 }
105
106 Ok(RigValidationSummary {
107 provider: self.provider,
108 model: self.model.clone(),
109 })
110 }
111
112 #[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 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}