Skip to main content

synaps_cli/runtime/openai/
reasoning.rs

1//! Provider-aware thinking/reasoning request helpers.
2
3use 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    // Don't inject reasoning params when thinking is disabled.
34    // Without this guard, non-reasoning models (e.g. llama-3.3) get
35    // unsupported fields that cause request failures.
36    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}