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" | "kimi-code" => OpenAiReasoningProvider::Kimi,
82 _ => OpenAiReasoningProvider::Generic,
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn openrouter_adds_reasoning_and_include_reasoning() {
92 let mut body = Map::new();
93 apply_openai_reasoning_params(
94 &mut body,
95 OpenAiReasoningProvider::OpenRouter,
96 "deepseek/deepseek-r1",
97 4096,
98 agent_core::reasoning::ReasoningLevel::Medium,
99 );
100 assert_eq!(body["reasoning"]["effort"], "medium");
101 assert_eq!(body["include_reasoning"], true);
102 }
103
104 #[test]
105 fn groq_adds_reasoning_only_for_reasoning_families() {
106 let mut body = Map::new();
107 apply_openai_reasoning_params(
108 &mut body,
109 OpenAiReasoningProvider::Groq,
110 "openai/gpt-oss-120b",
111 16_384,
112 agent_core::reasoning::ReasoningLevel::Medium,
113 );
114 assert_eq!(body["reasoning_format"], "parsed");
115 assert_eq!(body["reasoning_effort"], "high");
116
117 let mut plain = Map::new();
118 apply_openai_reasoning_params(
119 &mut plain,
120 OpenAiReasoningProvider::Groq,
121 "llama-3.3-70b-versatile",
122 16_384,
123 agent_core::reasoning::ReasoningLevel::Medium,
124 );
125 assert!(plain.is_empty());
126 }
127
128 #[test]
129 fn nvidia_and_generic_do_not_emit_unsupported_extra_fields() {
130 let mut body = Map::new();
131 apply_openai_reasoning_params(
132 &mut body,
133 OpenAiReasoningProvider::NvidiaNim,
134 "moonshotai/kimi-k2-thinking",
135 4096,
136 agent_core::reasoning::ReasoningLevel::Medium,
137 );
138 assert!(body.is_empty());
139 apply_openai_reasoning_params(
140 &mut body,
141 OpenAiReasoningProvider::Generic,
142 "some/model",
143 4096,
144 agent_core::reasoning::ReasoningLevel::Medium,
145 );
146 assert!(body.is_empty());
147 }
148
149 #[test]
150 fn zero_budget_skips_all_reasoning_params() {
151 let mut body = Map::new();
152 apply_openai_reasoning_params(
153 &mut body,
154 OpenAiReasoningProvider::OpenRouter,
155 "deepseek/deepseek-r1",
156 0,
157 agent_core::reasoning::ReasoningLevel::Medium,
158 );
159 assert!(
160 body.is_empty(),
161 "OpenRouter should not inject reasoning when budget is 0"
162 );
163
164 apply_openai_reasoning_params(
165 &mut body,
166 OpenAiReasoningProvider::Groq,
167 "openai/gpt-oss-120b",
168 0,
169 agent_core::reasoning::ReasoningLevel::Medium,
170 );
171 assert!(
172 body.is_empty(),
173 "Groq should not inject reasoning when budget is 0"
174 );
175 }
176
177 #[test]
178 fn kimi_key_maps_to_kimi_provider() {
179 assert_eq!(provider_for_key("kimi"), OpenAiReasoningProvider::Kimi);
180 }
181
182 #[test]
183 fn kimi_is_level_driven_not_budget_driven() {
184 use agent_core::reasoning::ReasoningLevel;
185 let mut body = Map::new();
187 apply_openai_reasoning_params(
188 &mut body,
189 OpenAiReasoningProvider::Kimi,
190 "kimi-k3",
191 0,
192 ReasoningLevel::Max,
193 );
194 assert_eq!(body["reasoning_effort"], "max");
195
196 let mut body = Map::new();
199 apply_openai_reasoning_params(
200 &mut body,
201 OpenAiReasoningProvider::Kimi,
202 "kimi-k2.6",
203 0,
204 ReasoningLevel::Off,
205 );
206 assert_eq!(body["thinking"]["type"], "disabled");
207
208 let mut body = Map::new();
210 apply_openai_reasoning_params(
211 &mut body,
212 OpenAiReasoningProvider::Kimi,
213 "kimi-k3",
214 0,
215 ReasoningLevel::Adaptive,
216 );
217 assert!(body.is_empty());
218 }
219}