synaps_cli/runtime/openai/
reasoning.rs1use serde_json::{Map, Value, json};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum OpenAiReasoningProvider {
7 OpenRouter,
8 Groq,
9 NvidiaNim,
10 Generic,
11}
12
13pub fn thinking_level_for_budget(budget: u32) -> &'static str {
14 crate::core::models::thinking_level_for_budget(budget)
15}
16
17pub fn openai_effort_for_level(level: &str) -> &'static str {
18 match level {
19 "low" => "low",
20 "medium" | "med" => "medium",
21 "high" | "xhigh" => "high",
22 "adaptive" => "medium",
23 _ => "medium",
24 }
25}
26
27pub fn apply_openai_reasoning_params(
28 body: &mut Map<String, Value>,
29 provider: OpenAiReasoningProvider,
30 model: &str,
31 thinking_budget: u32,
32) {
33 if thinking_budget == 0 {
37 return;
38 }
39 let level = thinking_level_for_budget(thinking_budget);
40 match provider {
41 OpenAiReasoningProvider::OpenRouter => {
42 let effort = openai_effort_for_level(level);
43 body.insert("reasoning".to_string(), json!({ "effort": effort }));
44 body.insert("include_reasoning".to_string(), json!(true));
45 }
46 OpenAiReasoningProvider::Groq => {
47 if crate::runtime::openai::catalog::infer_groq_reasoning(model)
48 == crate::runtime::openai::catalog::ReasoningSupport::GroqReasoning
49 {
50 body.insert("reasoning_format".to_string(), json!("parsed"));
51 body.insert("reasoning_effort".to_string(), json!(openai_effort_for_level(level)));
52 }
53 }
54 OpenAiReasoningProvider::NvidiaNim | OpenAiReasoningProvider::Generic => {}
55 }
56}
57
58pub fn provider_for_key(provider_key: &str) -> OpenAiReasoningProvider {
59 match provider_key {
60 "openrouter" => OpenAiReasoningProvider::OpenRouter,
61 "groq" => OpenAiReasoningProvider::Groq,
62 "nvidia" => OpenAiReasoningProvider::NvidiaNim,
63 _ => OpenAiReasoningProvider::Generic,
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn openrouter_adds_reasoning_and_include_reasoning() {
73 let mut body = Map::new();
74 apply_openai_reasoning_params(&mut body, OpenAiReasoningProvider::OpenRouter, "deepseek/deepseek-r1", 4096);
75 assert_eq!(body["reasoning"]["effort"], "medium");
76 assert_eq!(body["include_reasoning"], true);
77 }
78
79 #[test]
80 fn groq_adds_reasoning_only_for_reasoning_families() {
81 let mut body = Map::new();
82 apply_openai_reasoning_params(&mut body, OpenAiReasoningProvider::Groq, "openai/gpt-oss-120b", 16_384);
83 assert_eq!(body["reasoning_format"], "parsed");
84 assert_eq!(body["reasoning_effort"], "high");
85
86 let mut plain = Map::new();
87 apply_openai_reasoning_params(&mut plain, OpenAiReasoningProvider::Groq, "llama-3.3-70b-versatile", 16_384);
88 assert!(plain.is_empty());
89 }
90
91 #[test]
92 fn nvidia_and_generic_do_not_emit_unsupported_extra_fields() {
93 let mut body = Map::new();
94 apply_openai_reasoning_params(&mut body, OpenAiReasoningProvider::NvidiaNim, "moonshotai/kimi-k2-thinking", 4096);
95 assert!(body.is_empty());
96 apply_openai_reasoning_params(&mut body, OpenAiReasoningProvider::Generic, "some/model", 4096);
97 assert!(body.is_empty());
98 }
99
100 #[test]
101 fn zero_budget_skips_all_reasoning_params() {
102 let mut body = Map::new();
103 apply_openai_reasoning_params(&mut body, OpenAiReasoningProvider::OpenRouter, "deepseek/deepseek-r1", 0);
104 assert!(body.is_empty(), "OpenRouter should not inject reasoning when budget is 0");
105
106 apply_openai_reasoning_params(&mut body, OpenAiReasoningProvider::Groq, "openai/gpt-oss-120b", 0);
107 assert!(body.is_empty(), "Groq should not inject reasoning when budget is 0");
108 }
109}