use crate::agent::agent_loop::types::{ThinkingBudgets, ThinkingLevel};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EffortWire {
NestedEffort,
TopLevelEffort,
AnthropicBudget,
GeminiBudget,
GenericLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisableWire {
ThinkingToggle,
ChatTemplateKwargs,
OllamaThink,
GeminiZeroBudget,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReasoningProfile {
pub effort: EffortWire,
pub disable: DisableWire,
}
pub fn reasoning_profile(provider: Option<&str>) -> ReasoningProfile {
match provider {
Some("anthropic") => ReasoningProfile {
effort: EffortWire::AnthropicBudget,
disable: DisableWire::None,
},
Some("deepseek") => ReasoningProfile {
effort: EffortWire::TopLevelEffort,
disable: DisableWire::ThinkingToggle,
},
Some("glm") => ReasoningProfile {
effort: EffortWire::NestedEffort,
disable: DisableWire::ThinkingToggle,
},
Some("openai") => ReasoningProfile {
effort: EffortWire::NestedEffort,
disable: DisableWire::None,
},
Some("custom") | Some("openrouter") => ReasoningProfile {
effort: EffortWire::NestedEffort,
disable: DisableWire::ChatTemplateKwargs,
},
Some("opencode") => ReasoningProfile {
effort: EffortWire::GenericLevel,
disable: DisableWire::ChatTemplateKwargs,
},
Some("gemini") => ReasoningProfile {
effort: EffortWire::GeminiBudget,
disable: DisableWire::GeminiZeroBudget,
},
Some("ollama") => ReasoningProfile {
effort: EffortWire::GenericLevel,
disable: DisableWire::OllamaThink,
},
_ => ReasoningProfile {
effort: EffortWire::GenericLevel,
disable: DisableWire::None,
},
}
}
fn thinking_level_to_openai_effort(level: ThinkingLevel) -> Option<&'static str> {
match level {
ThinkingLevel::Off => None,
ThinkingLevel::Minimal | ThinkingLevel::Low => Some("low"),
ThinkingLevel::Medium => Some("medium"),
ThinkingLevel::High | ThinkingLevel::Xhigh => Some("high"),
}
}
fn thinking_level_to_deepseek_effort(level: ThinkingLevel) -> Option<&'static str> {
match level {
ThinkingLevel::Off => None,
ThinkingLevel::Minimal | ThinkingLevel::Low => Some("low"),
ThinkingLevel::Medium => Some("medium"),
ThinkingLevel::High => Some("high"),
ThinkingLevel::Xhigh => Some("max"),
}
}
fn budget_for_level(level: ThinkingLevel, budgets: Option<&ThinkingBudgets>) -> u32 {
match level {
ThinkingLevel::Off => 0,
ThinkingLevel::Minimal => budgets.and_then(|b| b.minimal).unwrap_or(1024),
ThinkingLevel::Low => budgets.and_then(|b| b.low).unwrap_or(2048),
ThinkingLevel::Medium => budgets.and_then(|b| b.medium).unwrap_or(4096),
ThinkingLevel::High | ThinkingLevel::Xhigh => budgets.and_then(|b| b.high).unwrap_or(16384),
}
}
impl ReasoningProfile {
pub fn effort_params(
&self,
level: ThinkingLevel,
budgets: Option<&ThinkingBudgets>,
) -> Option<serde_json::Value> {
match self.effort {
EffortWire::NestedEffort => thinking_level_to_openai_effort(level)
.map(|e| serde_json::json!({ "reasoning": { "effort": e } })),
EffortWire::TopLevelEffort => thinking_level_to_deepseek_effort(level)
.map(|e| serde_json::json!({ "reasoning_effort": e })),
EffortWire::AnthropicBudget => {
let b = budget_for_level(level, budgets);
(b > 0).then(
|| serde_json::json!({ "thinking": { "type": "enabled", "budget_tokens": b } }),
)
}
EffortWire::GeminiBudget => {
let b = budget_for_level(level, budgets);
(b > 0).then(|| serde_json::json!({ "thinking_config": { "thinking_budget": b } }))
}
EffortWire::GenericLevel => Some(
serde_json::json!({ "reasoning_level": serde_json::to_value(level).unwrap_or(serde_json::Value::Null) }),
),
}
}
pub fn disable_params(&self) -> Option<serde_json::Value> {
match self.disable {
DisableWire::ThinkingToggle => {
Some(serde_json::json!({ "thinking": { "type": "disabled" } }))
}
DisableWire::ChatTemplateKwargs => {
Some(serde_json::json!({ "chat_template_kwargs": { "thinking": false } }))
}
DisableWire::OllamaThink => Some(serde_json::json!({ "think": false })),
DisableWire::GeminiZeroBudget => {
Some(serde_json::json!({ "thinking_config": { "thinking_budget": 0 } }))
}
DisableWire::None => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_table_all_known_providers() {
let cases: &[(&str, EffortWire, DisableWire)] = &[
("anthropic", EffortWire::AnthropicBudget, DisableWire::None),
(
"deepseek",
EffortWire::TopLevelEffort,
DisableWire::ThinkingToggle,
),
("glm", EffortWire::NestedEffort, DisableWire::ThinkingToggle),
("openai", EffortWire::NestedEffort, DisableWire::None),
(
"custom",
EffortWire::NestedEffort,
DisableWire::ChatTemplateKwargs,
),
(
"openrouter",
EffortWire::NestedEffort,
DisableWire::ChatTemplateKwargs,
),
(
"opencode",
EffortWire::GenericLevel,
DisableWire::ChatTemplateKwargs,
),
(
"gemini",
EffortWire::GeminiBudget,
DisableWire::GeminiZeroBudget,
),
("ollama", EffortWire::GenericLevel, DisableWire::OllamaThink),
];
for &(name, effort, disable) in cases {
let p = reasoning_profile(Some(name));
assert_eq!(
(p.effort, p.disable),
(effort, disable),
"profile mismatch for {name}"
);
}
}
#[test]
fn profile_table_none_and_unknown() {
let none = reasoning_profile(None);
assert_eq!(
(none.effort, none.disable),
(EffortWire::GenericLevel, DisableWire::None)
);
let unknown = reasoning_profile(Some("bogus"));
assert_eq!(
(unknown.effort, unknown.disable),
(EffortWire::GenericLevel, DisableWire::None)
);
}
#[test]
fn disable_params_all_variants() {
assert_eq!(
ReasoningProfile {
effort: EffortWire::NestedEffort,
disable: DisableWire::ThinkingToggle
}
.disable_params(),
Some(serde_json::json!({ "thinking": { "type": "disabled" } }))
);
assert_eq!(
ReasoningProfile {
effort: EffortWire::NestedEffort,
disable: DisableWire::ChatTemplateKwargs
}
.disable_params(),
Some(serde_json::json!({ "chat_template_kwargs": { "thinking": false } }))
);
assert_eq!(
ReasoningProfile {
effort: EffortWire::NestedEffort,
disable: DisableWire::OllamaThink
}
.disable_params(),
Some(serde_json::json!({ "think": false }))
);
assert_eq!(
ReasoningProfile {
effort: EffortWire::NestedEffort,
disable: DisableWire::GeminiZeroBudget
}
.disable_params(),
Some(serde_json::json!({ "thinking_config": { "thinking_budget": 0 } }))
);
assert_eq!(
ReasoningProfile {
effort: EffortWire::NestedEffort,
disable: DisableWire::None
}
.disable_params(),
None
);
}
#[test]
fn effort_nested_high_off() {
let p = ReasoningProfile {
effort: EffortWire::NestedEffort,
disable: DisableWire::None,
};
assert_eq!(
p.effort_params(ThinkingLevel::High, None),
Some(serde_json::json!({ "reasoning": { "effort": "high" } }))
);
assert_eq!(p.effort_params(ThinkingLevel::Off, None), None);
}
#[test]
fn effort_top_level_xhigh_high_off() {
let p = ReasoningProfile {
effort: EffortWire::TopLevelEffort,
disable: DisableWire::None,
};
assert_eq!(
p.effort_params(ThinkingLevel::Xhigh, None),
Some(serde_json::json!({ "reasoning_effort": "max" }))
);
assert_eq!(
p.effort_params(ThinkingLevel::High, None),
Some(serde_json::json!({ "reasoning_effort": "high" }))
);
assert_eq!(p.effort_params(ThinkingLevel::Off, None), None);
}
#[test]
fn effort_generic_level() {
let p = ReasoningProfile {
effort: EffortWire::GenericLevel,
disable: DisableWire::None,
};
let v = p
.effort_params(ThinkingLevel::Medium, None)
.expect("generic level should produce value");
assert_eq!(
v["reasoning_level"],
serde_json::to_value(ThinkingLevel::Medium).unwrap()
);
}
#[test]
fn effort_anthropic_budget_positive_and_zero() {
let p = ReasoningProfile {
effort: EffortWire::AnthropicBudget,
disable: DisableWire::None,
};
let v = p
.effort_params(ThinkingLevel::Medium, None)
.expect("medium should produce budget");
assert_eq!(v["thinking"]["type"], "enabled");
assert_eq!(
v["thinking"]["budget_tokens"],
budget_for_level(ThinkingLevel::Medium, None)
);
assert_eq!(p.effort_params(ThinkingLevel::Off, None), None);
}
#[test]
fn effort_gemini_budget_positive_and_zero() {
let p = ReasoningProfile {
effort: EffortWire::GeminiBudget,
disable: DisableWire::None,
};
let v = p
.effort_params(ThinkingLevel::Low, None)
.expect("low should produce budget");
assert_eq!(
v["thinking_config"]["thinking_budget"],
budget_for_level(ThinkingLevel::Low, None)
);
assert_eq!(p.effort_params(ThinkingLevel::Off, None), None);
}
#[test]
fn thinking_level_to_deepseek_effort_all_variants() {
assert_eq!(thinking_level_to_deepseek_effort(ThinkingLevel::Off), None);
assert_eq!(
thinking_level_to_deepseek_effort(ThinkingLevel::Minimal),
Some("low")
);
assert_eq!(
thinking_level_to_deepseek_effort(ThinkingLevel::Low),
Some("low")
);
assert_eq!(
thinking_level_to_deepseek_effort(ThinkingLevel::Medium),
Some("medium")
);
assert_eq!(
thinking_level_to_deepseek_effort(ThinkingLevel::High),
Some("high")
);
assert_eq!(
thinking_level_to_deepseek_effort(ThinkingLevel::Xhigh),
Some("max")
);
}
}