agent_engine/runtime/openai/
reasoning.rs1use serde_json::{json, Map, Value};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum OpenAiReasoningProvider {
7 OpenRouter,
8 Groq,
9 NvidiaNim,
10 Kimi,
13 Generic,
14}
15
16pub fn thinking_level_for_budget(budget: u32) -> &'static str {
17 crate::core::models::thinking_level_for_budget(budget)
18}
19
20pub fn openai_effort_for_level(level: &str) -> &'static str {
21 match level {
22 "low" => "low",
23 "medium" | "med" => "medium",
24 "high" | "xhigh" => "high",
25 "adaptive" => "medium",
26 _ => "medium",
27 }
28}
29
30pub fn apply_openai_reasoning_params(
31 body: &mut Map<String, Value>,
32 provider: OpenAiReasoningProvider,
33 model: &str,
34 thinking_budget: u32,
35 reasoning_level: agent_core::reasoning::ReasoningLevel,
36) {
37 if provider == OpenAiReasoningProvider::Kimi {
42 crate::runtime::openai::catalog::apply_kimi_reasoning_params(body, model, reasoning_level);
43 return;
44 }
45 if thinking_budget == 0 {
49 return;
50 }
51 let level = thinking_level_for_budget(thinking_budget);
52 match provider {
53 OpenAiReasoningProvider::OpenRouter => {
54 let effort = openai_effort_for_level(level);
55 body.insert("reasoning".to_string(), json!({ "effort": effort }));
56 body.insert("include_reasoning".to_string(), json!(true));
57 }
58 OpenAiReasoningProvider::Groq => {
59 if crate::runtime::openai::catalog::infer_groq_reasoning(model)
60 == crate::runtime::openai::catalog::ReasoningSupport::GroqReasoning
61 {
62 body.insert("reasoning_format".to_string(), json!("parsed"));
63 body.insert(
64 "reasoning_effort".to_string(),
65 json!(openai_effort_for_level(level)),
66 );
67 }
68 }
69 OpenAiReasoningProvider::Kimi => unreachable!("handled above"),
70 OpenAiReasoningProvider::NvidiaNim | OpenAiReasoningProvider::Generic => {}
71 }
72}
73
74pub fn provider_for_key(provider_key: &str) -> OpenAiReasoningProvider {
75 match provider_key {
76 "openrouter" => OpenAiReasoningProvider::OpenRouter,
77 "groq" => OpenAiReasoningProvider::Groq,
78 "nvidia" => OpenAiReasoningProvider::NvidiaNim,
79 "kimi" => OpenAiReasoningProvider::Kimi,
80 _ => OpenAiReasoningProvider::Generic,
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn openrouter_adds_reasoning_and_include_reasoning() {
90 let mut body = Map::new();
91 apply_openai_reasoning_params(
92 &mut body,
93 OpenAiReasoningProvider::OpenRouter,
94 "deepseek/deepseek-r1",
95 4096,
96 agent_core::reasoning::ReasoningLevel::Medium,
97 );
98 assert_eq!(body["reasoning"]["effort"], "medium");
99 assert_eq!(body["include_reasoning"], true);
100 }
101
102 #[test]
103 fn groq_adds_reasoning_only_for_reasoning_families() {
104 let mut body = Map::new();
105 apply_openai_reasoning_params(
106 &mut body,
107 OpenAiReasoningProvider::Groq,
108 "openai/gpt-oss-120b",
109 16_384,
110 agent_core::reasoning::ReasoningLevel::Medium,
111 );
112 assert_eq!(body["reasoning_format"], "parsed");
113 assert_eq!(body["reasoning_effort"], "high");
114
115 let mut plain = Map::new();
116 apply_openai_reasoning_params(
117 &mut plain,
118 OpenAiReasoningProvider::Groq,
119 "llama-3.3-70b-versatile",
120 16_384,
121 agent_core::reasoning::ReasoningLevel::Medium,
122 );
123 assert!(plain.is_empty());
124 }
125
126 #[test]
127 fn nvidia_and_generic_do_not_emit_unsupported_extra_fields() {
128 let mut body = Map::new();
129 apply_openai_reasoning_params(
130 &mut body,
131 OpenAiReasoningProvider::NvidiaNim,
132 "moonshotai/kimi-k2-thinking",
133 4096,
134 agent_core::reasoning::ReasoningLevel::Medium,
135 );
136 assert!(body.is_empty());
137 apply_openai_reasoning_params(
138 &mut body,
139 OpenAiReasoningProvider::Generic,
140 "some/model",
141 4096,
142 agent_core::reasoning::ReasoningLevel::Medium,
143 );
144 assert!(body.is_empty());
145 }
146
147 #[test]
148 fn zero_budget_skips_all_reasoning_params() {
149 let mut body = Map::new();
150 apply_openai_reasoning_params(
151 &mut body,
152 OpenAiReasoningProvider::OpenRouter,
153 "deepseek/deepseek-r1",
154 0,
155 agent_core::reasoning::ReasoningLevel::Medium,
156 );
157 assert!(
158 body.is_empty(),
159 "OpenRouter should not inject reasoning when budget is 0"
160 );
161
162 apply_openai_reasoning_params(
163 &mut body,
164 OpenAiReasoningProvider::Groq,
165 "openai/gpt-oss-120b",
166 0,
167 agent_core::reasoning::ReasoningLevel::Medium,
168 );
169 assert!(
170 body.is_empty(),
171 "Groq should not inject reasoning when budget is 0"
172 );
173 }
174
175 #[test]
176 fn kimi_key_maps_to_kimi_provider() {
177 assert_eq!(provider_for_key("kimi"), OpenAiReasoningProvider::Kimi);
178 }
179
180 #[test]
181 fn kimi_is_level_driven_not_budget_driven() {
182 use agent_core::reasoning::ReasoningLevel;
183 let mut body = Map::new();
185 apply_openai_reasoning_params(
186 &mut body,
187 OpenAiReasoningProvider::Kimi,
188 "kimi-k3",
189 0,
190 ReasoningLevel::Max,
191 );
192 assert_eq!(body["reasoning_effort"], "max");
193
194 let mut body = Map::new();
197 apply_openai_reasoning_params(
198 &mut body,
199 OpenAiReasoningProvider::Kimi,
200 "kimi-k2.6",
201 0,
202 ReasoningLevel::Off,
203 );
204 assert_eq!(body["thinking"]["type"], "disabled");
205
206 let mut body = Map::new();
208 apply_openai_reasoning_params(
209 &mut body,
210 OpenAiReasoningProvider::Kimi,
211 "kimi-k3",
212 0,
213 ReasoningLevel::Adaptive,
214 );
215 assert!(body.is_empty());
216 }
217}