feat: Added support for modifying the reasoning effort of reasoning models

This commit is contained in:
2026-07-16 12:28:04 -06:00
parent e9c52566b8
commit e9a8c01dc4
18 changed files with 353 additions and 3 deletions
+4
View File
@@ -325,6 +325,7 @@ fn build_chat_completions_body(data: ChatCompletionsData, model: &Model) -> Resu
mut messages,
temperature,
top_p,
reasoning_effort,
functions,
stream: _,
} = data;
@@ -457,6 +458,9 @@ fn build_chat_completions_body(data: ChatCompletionsData, model: &Model) -> Resu
if let Some(v) = top_p {
body["inferenceConfig"]["topP"] = v.into();
}
if let Some(v) = reasoning_effort {
body["additionalModelRequestFields"] = json!({ "output_config": { "effort": v } });
}
if let Some(functions) = functions {
let tools: Vec<_> = functions
.iter()
+4
View File
@@ -251,6 +251,7 @@ pub fn claude_build_chat_completions_body(
mut messages,
temperature,
top_p,
reasoning_effort,
functions,
stream,
} = data;
@@ -369,6 +370,9 @@ pub fn claude_build_chat_completions_body(
if let Some(v) = top_p {
body["top_p"] = v.into();
}
if let Some(v) = reasoning_effort {
body["output_config"] = json!({ "effort": v });
}
if stream {
body["stream"] = true.into();
}
+1
View File
@@ -286,6 +286,7 @@ pub struct ChatCompletionsData {
pub messages: Vec<Message>,
pub temperature: Option<f64>,
pub top_p: Option<f64>,
pub reasoning_effort: Option<String>,
pub functions: Option<Vec<FunctionDeclaration>>,
pub stream: bool,
}
+12
View File
@@ -289,6 +289,14 @@ impl Model {
}
Ok(())
}
pub fn reasoning_levels(&self) -> &[String] {
&self.data.reasoning_levels
}
pub fn default_reasoning_effort(&self) -> Option<&str> {
self.data.default_reasoning_effort.as_deref()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -316,6 +324,10 @@ pub struct ModelData {
pub supports_vision: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub supports_function_calling: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reasoning_levels: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_reasoning_effort: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
no_stream: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
+8
View File
@@ -356,6 +356,7 @@ pub fn openai_build_chat_completions_body(data: ChatCompletionsData, model: &Mod
messages,
temperature,
top_p,
reasoning_effort,
functions,
stream,
} = data;
@@ -454,6 +455,9 @@ pub fn openai_build_chat_completions_body(data: ChatCompletionsData, model: &Mod
if let Some(v) = top_p {
body["top_p"] = v.into();
}
if let Some(v) = reasoning_effort {
body["reasoning_effort"] = v.into();
}
if stream {
body["stream"] = true.into();
}
@@ -534,6 +538,7 @@ pub fn openai_build_responses_body(data: ChatCompletionsData, model: &Model) ->
messages,
temperature,
top_p,
reasoning_effort,
functions,
stream,
} = data;
@@ -590,6 +595,9 @@ pub fn openai_build_responses_body(data: ChatCompletionsData, model: &Model) ->
if let Some(v) = top_p {
body["top_p"] = v.into();
}
if let Some(v) = reasoning_effort {
body["reasoning"] = json!({ "effort": v });
}
if stream {
body["stream"] = true.into();
}
+4
View File
@@ -334,6 +334,7 @@ pub fn gemini_build_chat_completions_body(
mut messages,
temperature,
top_p,
reasoning_effort,
functions,
stream: _,
} = data;
@@ -426,6 +427,9 @@ pub fn gemini_build_chat_completions_body(
if let Some(v) = top_p {
body["generationConfig"]["topP"] = v.into();
}
if let Some(v) = reasoning_effort {
body["generation_config"]["thinking_level"] = v.into();
}
if let Some(functions) = functions {
// Gemini doesn't support functions with parameters that have empty properties, so we need to patch it.
+14
View File
@@ -575,6 +575,10 @@ impl RoleLike for Agent {
self.config.top_p
}
fn reasoning_effort(&self) -> Option<String> {
self.config.reasoning_effort.clone()
}
fn enabled_tools(&self) -> Option<Vec<String>> {
None
}
@@ -596,6 +600,10 @@ impl RoleLike for Agent {
self.config.top_p = value;
}
fn set_reasoning_effort(&mut self, value: Option<String>) {
self.config.reasoning_effort = value;
}
fn set_enabled_tools(&mut self, value: Option<Vec<String>>) {
match value {
Some(tools) => {
@@ -637,6 +645,8 @@ pub struct AgentConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_session: Option<String>,
#[serde(default)]
pub auto_continue: bool,
@@ -732,6 +742,7 @@ impl AgentConfig {
model_id: graph.model.clone(),
temperature: graph.temperature,
top_p: graph.top_p,
reasoning_effort: graph.reasoning_effort.clone(),
description: graph.description.clone(),
global_tools: graph.global_tools.clone(),
mcp_servers: graph.mcp_servers.clone(),
@@ -766,6 +777,9 @@ impl AgentConfig {
if let Some(v) = read_env_value::<f64>(&with_prefix("top_p")) {
self.top_p = v;
}
if let Some(v) = read_env_value::<String>(&with_prefix("reasoning_effort")) {
self.reasoning_effort = v;
}
if let Ok(v) = env::var(with_prefix("global_tools"))
&& let Ok(v) = serde_json::from_str(&v)
{
+6
View File
@@ -21,6 +21,7 @@ pub struct AppConfig {
pub model_id: String,
pub temperature: Option<f64>,
pub top_p: Option<f64>,
pub reasoning_effort: Option<String>,
pub dry_run: bool,
pub stream: bool,
@@ -100,6 +101,7 @@ impl Default for AppConfig {
model_id: Default::default(),
temperature: None,
top_p: None,
reasoning_effort: None,
dry_run: false,
stream: true,
@@ -177,6 +179,7 @@ impl AppConfig {
model_id: config.model_id,
temperature: config.temperature,
top_p: config.top_p,
reasoning_effort: None,
dry_run: config.dry_run,
stream: config.stream,
@@ -426,6 +429,9 @@ impl AppConfig {
if let Some(v) = super::read_env_value::<f64>(&get_env_name("top_p")) {
self.top_p = v;
}
if let Some(v) = super::read_env_value::<String>(&get_env_name("reasoning_effort")) {
self.reasoning_effort = v;
}
if let Some(Some(v)) = super::read_env_bool(&get_env_name("dry_run")) {
self.dry_run = v;
+5
View File
@@ -253,6 +253,10 @@ impl Input {
patch_messages(&mut messages, model);
model.guard_max_input_tokens(&messages)?;
let (temperature, top_p) = (self.role().temperature(), self.role().top_p());
let reasoning_effort = self
.role()
.reasoning_effort()
.or_else(|| model.default_reasoning_effort().map(|s| s.to_string()));
let functions = if model.supports_function_calling() {
let fns = self.functions.clone();
if let Some(vec) = &fns {
@@ -268,6 +272,7 @@ impl Input {
messages,
temperature,
top_p,
reasoning_effort,
functions,
stream,
})
+29
View File
@@ -969,6 +969,16 @@ impl RequestContext {
}
}
pub fn set_reasoning_effort_on_role_like(&mut self, value: Option<String>) -> bool {
match self.role_like_mut() {
Some(role_like) => {
role_like.set_reasoning_effort(value);
true
}
None => false,
}
}
pub fn set_enabled_tools_on_role_like(&mut self, value: Option<Vec<String>>) -> bool {
match self.role_like_mut() {
Some(role_like) => {
@@ -1121,6 +1131,10 @@ impl RequestContext {
super::format_option_value(&role.temperature()),
),
("top_p", super::format_option_value(&role.top_p())),
(
"reasoning_effort",
super::format_option_value(&role.reasoning_effort()),
),
(
"enabled_tools",
super::format_option_value(&role.enabled_tools().map(|v| v.join(","))),
@@ -2009,6 +2023,12 @@ impl RequestContext {
self.update_app_config(|app| app.top_p = value);
}
}
"reasoning_effort" => {
let value: Option<String> = super::parse_value(value)?;
if !self.set_reasoning_effort_on_role_like(value.clone()) {
self.update_app_config(|app| app.reasoning_effort = value);
}
}
"enabled_tools" => {
let raw: Option<String> = super::parse_value(value)?;
let parsed: Option<Vec<String>> = raw.map(|s| super::csv_to_vec(&s));
@@ -2303,6 +2323,10 @@ impl RequestContext {
super::map_completion_values(values)
}
".macro" => super::map_completion_values(paths::list_macros()),
".reasoning" => {
let levels = self.current_model().reasoning_levels();
levels.iter().map(|v| (v.clone(), None)).collect()
}
".starter" => match &self.agent {
Some(agent) => agent
.conversation_starters()
@@ -2318,6 +2342,7 @@ impl RequestContext {
"continuation_prompt",
"temperature",
"top_p",
"reasoning_effort",
"enabled_tools",
"enabled_mcp_servers",
"inject_todo_instructions",
@@ -2507,6 +2532,10 @@ impl RequestContext {
}
"skill_instructions" => vec!["null".to_string()],
"memory" => super::complete_bool(self.should_inject_memory()),
"reasoning_effort" => {
let levels = self.current_model().reasoning_levels();
levels.to_vec()
}
_ => vec![],
};
values = candidates.into_iter().map(|v| (v, None)).collect();
+21
View File
@@ -32,7 +32,9 @@ pub trait RoleLike {
fn enabled_mcp_servers(&self) -> Option<Vec<String>>;
fn set_model(&mut self, model: Model);
fn set_temperature(&mut self, value: Option<f64>);
fn reasoning_effort(&self) -> Option<String>;
fn set_top_p(&mut self, value: Option<f64>);
fn set_reasoning_effort(&mut self, value: Option<String>);
fn set_enabled_tools(&mut self, value: Option<Vec<String>>);
fn set_enabled_mcp_servers(&mut self, value: Option<Vec<String>>);
}
@@ -51,6 +53,8 @@ pub struct Role {
temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
reasoning_effort: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
@@ -116,6 +120,9 @@ impl Role {
"model" => role.model_id = value.as_str().map(|v| v.to_string()),
"temperature" => role.temperature = value.as_f64(),
"top_p" => role.top_p = value.as_f64(),
"reasoning_effort" => {
role.reasoning_effort = value.as_str().map(|v| v.to_string())
}
"enabled_tools" => role.enabled_tools = parse_string_or_array(value),
"enabled_mcp_servers" => {
role.enabled_mcp_servers = parse_string_or_array(value)
@@ -170,6 +177,9 @@ impl Role {
if let Some(top_p) = self.top_p() {
metadata.push(format!("top_p: {top_p}"));
}
if let Some(reasoning_effort) = self.reasoning_effort() {
metadata.push(format!("reasoning_effort: {reasoning_effort}"));
}
if let Some(enabled_tools) = &self.enabled_tools {
let inline = serde_json::to_string(enabled_tools).unwrap_or_else(|_| "[]".to_string());
metadata.push(format!("enabled_tools: {inline}"));
@@ -256,6 +266,9 @@ impl Role {
enabled_tools,
enabled_mcp_servers,
);
if let Some(v) = role_like.reasoning_effort() {
self.set_reasoning_effort(Some(v));
}
}
pub fn batch_set(
@@ -410,6 +423,10 @@ impl RoleLike for Role {
self.top_p
}
fn reasoning_effort(&self) -> Option<String> {
self.reasoning_effort.clone()
}
fn enabled_tools(&self) -> Option<Vec<String>> {
self.enabled_tools.clone()
}
@@ -433,6 +450,10 @@ impl RoleLike for Role {
self.top_p = value;
}
fn set_reasoning_effort(&mut self, value: Option<String>) {
self.reasoning_effort = value;
}
fn set_enabled_tools(&mut self, value: Option<Vec<String>>) {
self.enabled_tools = value;
}
+14
View File
@@ -24,6 +24,8 @@ pub struct Session {
temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
reasoning_effort: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
@@ -401,6 +403,7 @@ impl Session {
self.model_id = role.model().id();
self.temperature = role.temperature();
self.top_p = role.top_p();
self.reasoning_effort = role.reasoning_effort();
self.enabled_tools = role.enabled_tools();
self.enabled_mcp_servers = role.enabled_mcp_servers();
self.model = role.model().clone();
@@ -792,6 +795,10 @@ impl RoleLike for Session {
self.top_p
}
fn reasoning_effort(&self) -> Option<String> {
self.reasoning_effort.clone()
}
fn enabled_tools(&self) -> Option<Vec<String>> {
self.enabled_tools.clone()
}
@@ -823,6 +830,13 @@ impl RoleLike for Session {
}
}
fn set_reasoning_effort(&mut self, value: Option<String>) {
if self.reasoning_effort != value {
self.reasoning_effort = value;
self.dirty = true;
}
}
fn set_enabled_tools(&mut self, value: Option<Vec<String>>) {
if self.enabled_tools != value {
self.enabled_tools = value;
+4
View File
@@ -329,6 +329,9 @@ fn build_inline_role(
if let Some(p) = node.top_p {
role.set_top_p(Some(p));
}
if let Some(v) = &node.reasoning_effort {
role.set_reasoning_effort(Some(v.clone()));
}
if node.tools.as_deref().unwrap_or_default().is_empty() {
role.set_enabled_tools(Some(Vec::new()));
@@ -499,6 +502,7 @@ mod tests {
model: None,
temperature: None,
top_p: None,
reasoning_effort: None,
fallback: None,
max_attempts: 1,
max_iterations: 10,
+6
View File
@@ -25,6 +25,9 @@ pub struct Graph {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_p: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
#[serde(default)]
pub global_tools: Vec<String>,
@@ -288,6 +291,9 @@ pub struct LlmNode {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_p: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback: Option<String>,
+2
View File
@@ -946,6 +946,7 @@ mod tests {
model: None,
temperature: None,
top_p: None,
reasoning_effort: None,
global_tools: Vec::new(),
mcp_servers: Vec::new(),
skills_enabled: None,
@@ -1048,6 +1049,7 @@ mod tests {
model: None,
temperature: None,
top_p: None,
reasoning_effort: None,
fallback: fallback.map(String::from),
max_attempts: 1,
max_iterations: 10,
+1
View File
@@ -362,6 +362,7 @@ pub async fn extract_entities(
messages,
temperature: Some(0.0),
top_p: None,
reasoning_effort: None,
functions: None,
stream: false,
};
+17 -3
View File
@@ -52,7 +52,7 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {"
4. Continue with the next pending item now. Call tools immediately."
};
static REPL_COMMANDS: LazyLock<[ReplCommand; 51]> = LazyLock::new(|| {
static REPL_COMMANDS: LazyLock<[ReplCommand; 52]> = LazyLock::new(|| {
[
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
ReplCommand::new(".info", "Show system info", AssertState::pass()),
@@ -259,6 +259,11 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 51]> = LazyLock::new(|| {
),
ReplCommand::new(".copy", "Copy last response", AssertState::pass()),
ReplCommand::new(".set", "Modify runtime settings", AssertState::pass()),
ReplCommand::new(
".reasoning",
"Set the reasoning effort level for the current model",
AssertState::pass(),
),
ReplCommand::new(
".delete",
"Delete roles, sessions, RAGs, or agents",
@@ -1070,6 +1075,15 @@ pub async fn run_repl_command(
println!("Usage: .set <key> <value>...")
}
},
".reasoning" => match args {
Some(level) => {
let set_args = format!("reasoning_effort {level}");
ctx.update(&set_args, abort_signal).await?;
}
None => {
println!("Usage: .reasoning <level>")
}
},
".delete" => match args {
Some(args) => {
ctx.delete(args)?;
@@ -1600,8 +1614,8 @@ mod tests {
}
#[test]
fn repl_commands_has_50_entries() {
assert_eq!(REPL_COMMANDS.len(), 50);
fn repl_commands_has_52_entries() {
assert_eq!(REPL_COMMANDS.len(), 52);
}
#[test]