1use rig::providers::gemini::completion::gemini_api_types::ThinkingConfig;
2use serde_json::{Value, json};
3use vtcode_config::models::Provider;
4use vtcode_config::types::ReasoningEffortLevel;
5
6#[derive(Debug, Clone)]
8pub struct RigProviderCapabilities {
9 provider: Provider,
10 model: compact_str::CompactString,
11}
12
13impl RigProviderCapabilities {
14 #[must_use]
15 pub fn new(provider: Provider, model: impl Into<String>) -> Self {
16 Self { provider, model: model.into().into() }
17 }
18
19 pub fn reasoning_parameters(
23 &self,
24 effort: ReasoningEffortLevel,
25 ) -> Result<Option<Value>, crate::provider::LLMError> {
26 let supported = crate::provider::catalog_reasoning_efforts(self.provider.as_ref(), &self.model)
27 .or_else(|| (self.provider == Provider::Anthropic).then_some(crate::provider::GENERIC_REASONING_EFFORTS))
28 .unwrap_or(&[]);
29 self.reasoning_parameters_for_supported_efforts(effort, supported)
30 }
31
32 pub fn reasoning_parameters_for_supported_efforts(
39 &self,
40 effort: ReasoningEffortLevel,
41 supported: &[&str],
42 ) -> Result<Option<Value>, crate::provider::LLMError> {
43 if effort == ReasoningEffortLevel::None {
44 return Ok(None);
45 }
46 let _mapping =
47 crate::reasoning_effort::ReasoningEffortMapper::map(effort, supported, false).map_err(|error| {
48 crate::provider::LLMError::InvalidRequest { message: error.to_string(), metadata: None }
49 })?;
50 let payload = match self.provider {
51 Provider::OpenAI => Some(json!({ "effort": effort.as_str() })),
52 Provider::Gemini => {
53 let budget = match effort {
54 ReasoningEffortLevel::Minimal => 16,
55 ReasoningEffortLevel::Low => 64,
56 ReasoningEffortLevel::Medium => 128,
57 ReasoningEffortLevel::High => 256,
58 _ => {
59 return Err(crate::provider::LLMError::InvalidRequest {
60 message: format!("Gemini does not serialize reasoning effort `{effort}`"),
61 metadata: None,
62 });
63 }
64 };
65 let config = ThinkingConfig {
66 thinking_budget: Some(budget),
67 thinking_level: None,
68 include_thoughts: Some(effort == ReasoningEffortLevel::High),
69 };
70 serde_json::to_value(config)
71 .ok()
72 .map(|value| json!({ "thinking_config": value }))
73 }
74 Provider::DeepSeek | Provider::ZAI => Some(json!({
75 "thinking": { "type": "enabled" }, "reasoning_effort": effort.as_str()
76 })),
77 Provider::HuggingFace | Provider::Meta | Provider::Evolink => {
78 Some(json!({ "reasoning_effort": effort.as_str() }))
79 }
80 Provider::StepFun => Some(json!({ "reasoning": { "effort": effort.as_str() } })),
82 Provider::OpenRouter => Some(json!({ "effort": effort.as_str() })),
86 Provider::Anthropic => None,
87 _ => None,
88 };
89 Ok(payload)
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn rig_serialization_preserves_exact_catalog_effort() {
99 for (provider, model, key) in [
100 (Provider::OpenAI, "gpt-6-astra", "effort"),
101 (Provider::ZAI, "glm-5.3", "reasoning_effort"),
102 ] {
103 for effort in [
104 ReasoningEffortLevel::Low,
105 ReasoningEffortLevel::High,
106 ReasoningEffortLevel::Max,
107 ] {
108 let payload = RigProviderCapabilities::new(provider, model)
109 .reasoning_parameters(effort)
110 .expect("reasoning serialization should not fail")
111 .expect("catalog-supported effort");
112 assert_eq!(payload[key], effort.as_str());
113 }
114 }
115 }
116
117 #[test]
118 fn rig_serialization_rejects_unsupported_effort_without_aliasing() {
119 for (provider, model, effort) in [
120 (Provider::OpenAI, "gpt-6-astra", ReasoningEffortLevel::Minimal),
121 (Provider::ZAI, "glm-5.3", ReasoningEffortLevel::XHigh),
122 (Provider::Gemini, "gemini-3.7-flash", ReasoningEffortLevel::Max),
123 (Provider::OpenAI, "unknown-model", ReasoningEffortLevel::High),
124 (Provider::OpenRouter, "meta/muse-spark-1.2", ReasoningEffortLevel::High),
125 ] {
126 assert!(
127 RigProviderCapabilities::new(provider, model)
128 .reasoning_parameters(effort)
129 .is_err()
130 );
131 }
132 }
133
134 #[test]
135 fn rig_serialization_accepts_explicit_custom_capability_levels() {
136 let payload = RigProviderCapabilities::new(Provider::OpenAI, "custom-route")
137 .reasoning_parameters_for_supported_efforts(ReasoningEffortLevel::High, &["low", "medium", "high"])
138 .expect("explicit provider capability should validate")
139 .expect("OpenRouter should serialize reasoning effort");
140 assert_eq!(payload["effort"], "high");
141 }
142
143 #[test]
144 fn anthropic_custom_routes_use_generic_efforts_without_silent_drops() {
145 let payload = RigProviderCapabilities::new(Provider::Anthropic, "custom-route")
146 .reasoning_parameters(ReasoningEffortLevel::High)
147 .expect("generic custom effort should validate");
148 assert!(payload.is_none(), "Anthropic encodes this effort in output_config");
149 assert!(
150 RigProviderCapabilities::new(Provider::Anthropic, "custom-route")
151 .reasoning_parameters(ReasoningEffortLevel::Max)
152 .is_err()
153 );
154 }
155
156 #[test]
157 fn native_providers_do_not_fail_rig_serialization() {
158 for (provider, model) in [
159 (Provider::MergeGateway, "openai/gpt-5.5"),
160 (Provider::OllamaCloud, "gpt-oss:120b-cloud"),
161 ] {
162 let payload = RigProviderCapabilities::new(provider, model)
163 .reasoning_parameters_for_supported_efforts(ReasoningEffortLevel::High, &["low", "medium", "high"])
164 .expect("native provider should not error on supported effort");
165 assert!(payload.is_none(), "native provider handles reasoning effort on its own");
166 }
167 }
168
169 #[test]
170 fn all_providers_handle_reasoning_parameters_gracefully() {
171 let supported_efforts = &["low", "medium", "high", "xhigh", "max"];
172 for (provider, model) in [
173 (Provider::OpenAI, "gpt-5.1-mini"),
174 (Provider::Anthropic, "claude-sonnet-5"),
175 (Provider::Gemini, "gemini-3.7-flash"),
176 (Provider::DeepSeek, "deepseek-v4.1-flash"),
177 (Provider::ZAI, "glm-5.3-flash"),
178 (Provider::Meta, "muse-spark-1.3"),
179 (Provider::OpenRouter, "openrouter/auto"),
180 (Provider::Ollama, "gpt-oss:20b"),
181 (Provider::OllamaCloud, "gpt-oss:20b-cloud"),
182 (Provider::LmStudio, "gpt-oss-20b"),
183 (Provider::LlamaCpp, "gpt-oss-20b"),
184 (Provider::Moonshot, "kimi-k3"),
185 (Provider::Minimax, "minimax-m2.7"),
186 (Provider::MiMo, "mimo-v2.6-pro"),
187 (Provider::Mistral, "ministral-3-3b-2512"),
188 (Provider::HuggingFace, "openai/gpt-oss-20b:huggingface"),
189 (Provider::OpenCodeZen, "gpt-5.6-sol"),
190 (Provider::OpenCodeGo, "glm-5.2"),
191 (Provider::Qwen, "deepseek-flash"),
192 (Provider::StepFun, "step-3.7-flash"),
193 (Provider::Evolink, "deepseek-flash"),
194 (Provider::Poolside, "laguna-xs2"),
195 (Provider::XAI, "grok-4-6"),
196 (Provider::NVIDIA, "nvidia/nemotron-3-nano-30b-a3b"),
197 (Provider::MergeGateway, "openai/gpt-5.5"),
198 (Provider::Vercel, "claude-sonnet-5"),
199 ] {
200 let result = RigProviderCapabilities::new(provider, model)
201 .reasoning_parameters_for_supported_efforts(ReasoningEffortLevel::High, supported_efforts);
202 assert!(result.is_ok(), "provider {provider:?} with model {model} should not error on supported effort");
203 }
204 }
205}