From e9a8c01dc4f471195e2c26e4cdd4227a9373a475 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 16 Jul 2026 12:28:04 -0600 Subject: [PATCH] feat: Added support for modifying the reasoning effort of reasoning models --- models.yaml | 201 ++++++++++++++++++++++++++++++++++ src/client/bedrock.rs | 4 + src/client/claude.rs | 4 + src/client/common.rs | 1 + src/client/model.rs | 12 ++ src/client/openai.rs | 8 ++ src/client/vertexai.rs | 4 + src/config/agent.rs | 14 +++ src/config/app_config.rs | 6 + src/config/input.rs | 5 + src/config/request_context.rs | 29 +++++ src/config/role.rs | 21 ++++ src/config/session.rs | 14 +++ src/graph/llm.rs | 4 + src/graph/types.rs | 6 + src/graph/validator.rs | 2 + src/rag/graph.rs | 1 + src/repl/mod.rs | 20 +++- 18 files changed, 353 insertions(+), 3 deletions(-) diff --git a/models.yaml b/models.yaml index 7ac3eac..5822602 100644 --- a/models.yaml +++ b/models.yaml @@ -10,6 +10,8 @@ output_price: 30 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh, max] + default_reasoning_effort: medium - name: gpt-5.6-terra max_input_tokens: 1050000 max_output_tokens: 128000 @@ -17,6 +19,8 @@ output_price: 30 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh, max] + default_reasoning_effort: medium - name: gpt-5.6-luna max_input_tokens: 1050000 max_output_tokens: 128000 @@ -24,6 +28,8 @@ output_price: 30 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh, max] + default_reasoning_effort: medium - name: gpt-5.5 max_input_tokens: 1050000 max_output_tokens: 128000 @@ -31,6 +37,8 @@ output_price: 30 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh] + default_reasoning_effort: medium - name: gpt-5.5-pro max_input_tokens: 1050000 max_output_tokens: 128000 @@ -38,6 +46,8 @@ output_price: 180 supports_vision: true supports_function_calling: true + reasoning_levels: [medium, high, xhigh] + default_reasoning_effort: high - name: gpt-5.4 max_input_tokens: 1050000 max_output_tokens: 128000 @@ -45,6 +55,8 @@ output_price: 15 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh] + default_reasoning_effort: none - name: gpt-5.4-pro max_input_tokens: 1050000 max_output_tokens: 128000 @@ -52,6 +64,8 @@ output_price: 180 supports_vision: true supports_function_calling: true + reasoning_levels: [medium, high, xhigh] + default_reasoning_effort: medium - name: gpt-5.4-mini max_input_tokens: 400000 max_output_tokens: 128000 @@ -59,6 +73,8 @@ output_price: 4.5 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh] + default_reasoning_effort: none - name: gpt-5.4-nano max_input_tokens: 400000 max_output_tokens: 128000 @@ -66,6 +82,8 @@ output_price: 1.25 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh] + default_reasoning_effort: none - name: gpt-5.3-codex max_input_tokens: 400000 max_output_tokens: 128000 @@ -73,6 +91,8 @@ output_price: 14 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh] + default_reasoning_effort: medium - name: chat-latest max_input_tokens: 400000 max_output_tokens: 128000 @@ -87,6 +107,17 @@ output_price: 14 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh] + default_reasoning_effort: none + - name: gpt-5.2-pro + max_input_tokens: 400000 + max_output_tokens: 128000 + input_price: 21 + output_price: 168 + supports_vision: true + supports_function_calling: true + reasoning_levels: [medium, high, xhigh] + default_reasoning_effort: medium - name: gpt-5.1 max_input_tokens: 400000 max_output_tokens: 128000 @@ -94,6 +125,8 @@ output_price: 10 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high] + default_reasoning_effort: none - name: gpt-5.1-chat-latest max_input_tokens: 400000 max_output_tokens: 128000 @@ -101,6 +134,8 @@ output_price: 10 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high] + default_reasoning_effort: none - name: gpt-5 max_input_tokens: 400000 max_output_tokens: 128000 @@ -108,6 +143,8 @@ output_price: 10 supports_vision: true supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: medium - name: gpt-5-chat-latest max_input_tokens: 400000 max_output_tokens: 128000 @@ -115,6 +152,8 @@ output_price: 10 supports_vision: true supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: medium - name: gpt-5-mini max_input_tokens: 400000 max_output_tokens: 128000 @@ -172,6 +211,8 @@ supports_vision: true supports_function_calling: true system_prompt_prefix: Formatting re-enabled + reasoning_levels: [low, medium, high] + default_reasoning_effort: medium patch: body: max_tokens: null @@ -285,18 +326,24 @@ input_price: 0.2 output_price: 1.5 supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: medium - name: gemini-3-flash-preview max_input_tokens: 1048576 max_output_tokens: 65536 input_price: 0.2 output_price: 1.5 supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: high - name: gemini-3.1-flash-lite max_input_tokens: 1048576 max_output_tokens: 65536 input_price: 0.2 output_price: 1.5 supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: minimal - name: gemini-3.1-pro-preview max_input_tokens: 1048576 max_output_tokens: 65535 @@ -304,6 +351,8 @@ output_price: 2.5 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high] + default_reasoning_effort: high - name: gemini-2.5-flash max_input_tokens: 1048576 max_output_tokens: 65536 @@ -311,6 +360,8 @@ output_price: 0 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high] + default_reasoning_effort: low - name: gemini-2.5-pro max_input_tokens: 1048576 max_output_tokens: 65536 @@ -318,6 +369,8 @@ output_price: 0 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high] + default_reasoning_effort: high - name: gemini-2.5-flash-lite max_input_tokens: 1000000 max_output_tokens: 64000 @@ -329,10 +382,14 @@ max_input_tokens: 1048576 supports_vision: true supports_function_calling: true + reasoning_levels: [low, high] + default_reasoning_level: high - name: gemini-3-flash-preview max_input_tokens: 1048576 supports_vision: true supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_level: high - name: gemma-3-27b-it max_input_tokens: 131072 max_output_tokens: 8192 @@ -358,6 +415,8 @@ output_price: 50 supports_function_calling: true supports_vision: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: claude-opus-4-8 max_input_tokens: 1000000 max_output_tokens: 128000 @@ -366,6 +425,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: claude-opus-4-7 max_input_tokens: 1000000 max_output_tokens: 128000 @@ -374,6 +435,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: claude-opus-4-6 max_input_tokens: 200000 max_output_tokens: 8192 @@ -382,6 +445,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, max] + default_reasoning_effort: high - name: claude-opus-4-6:thinking real_name: claude-opus-4-6 max_input_tokens: 200000 @@ -406,6 +471,8 @@ output_price: 15 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: claude-sonnet-4-6 max_input_tokens: 200000 max_output_tokens: 8192 @@ -414,6 +481,8 @@ output_price: 15 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, max] + default_reasoning_effort: high - name: claude-sonnet-4-6:thinking real_name: claude-sonnet-4-6 max_input_tokens: 200000 @@ -856,18 +925,24 @@ input_price: 0.2 output_price: 1.5 supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: medium - name: gemini-3-flash-preview max_input_tokens: 1048576 max_output_tokens: 65536 input_price: 0.2 output_price: 1.5 supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: high - name: gemini-3.1-flash-lite max_input_tokens: 1048576 max_output_tokens: 65536 input_price: 0.2 output_price: 1.5 supports_function_calling: true + reasoning_levels: [minimal, high] + default_reasoning_effort: minimal - name: gemini-3.1-pro-preview max_input_tokens: 1048576 max_output_tokens: 65536 @@ -875,6 +950,8 @@ output_price: 12 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high] + default_reasoning_effort: high - name: gemini-2.5-flash max_input_tokens: 1048576 max_output_tokens: 65535 @@ -882,6 +959,8 @@ output_price: 2.5 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high] + default_reasoning_effort: medium - name: gemini-2.5-pro max_input_tokens: 1048576 max_output_tokens: 65536 @@ -889,6 +968,8 @@ output_price: 10 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high] + default_reasoning_effort: high - name: gemini-2.5-flash-lite max_input_tokens: 1048576 max_output_tokens: 65536 @@ -900,10 +981,14 @@ max_input_tokens: 1048576 supports_vision: true supports_function_calling: true + reasoning_levels: [low, high] + default_reasoning_effort: high - name: gemini-3-flash-preview max_input_tokens: 1048576 supports_vision: true supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: high - name: claude-fable-5 max_input_tokens: 1000000 max_output_tokens: 128000 @@ -912,6 +997,8 @@ output_price: 50 supports_function_calling: true supports_vision: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: claude-opus-4-8 max_input_tokens: 1000000 max_output_tokens: 128000 @@ -920,6 +1007,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: claude-opus-4-7 max_input_tokens: 1000000 max_output_tokens: 128000 @@ -928,6 +1017,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: claude-opus-4-6 max_input_tokens: 200000 max_output_tokens: 8192 @@ -959,6 +1050,8 @@ output_price: 15 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: claude-sonnet-4-6 max_input_tokens: 200000 max_output_tokens: 8192 @@ -967,6 +1060,8 @@ output_price: 15 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, max] + default_reasoning_effort: high - name: claude-sonnet-4-6:thinking real_name: claude-sonnet-4-6 max_input_tokens: 200000 @@ -1099,6 +1194,8 @@ output_price: 50 supports_function_calling: true supports_vision: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: us.anthropic.claude-opus-4-8 max_input_tokens: 1000000 max_output_tokens: 128000 @@ -1107,6 +1204,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: us.anthropic.claude-opus-4-7 max_input_tokens: 1000000 max_output_tokens: 128000 @@ -1115,6 +1214,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: us.anthropic.claude-opus-4-6-v1 max_input_tokens: 200000 max_output_tokens: 8192 @@ -1123,6 +1224,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, max] + default_reasoning_effort: high - name: us.anthropic.claude-opus-4-6-v1:thinking real_name: us.anthropic.claude-opus-4-6-v1 max_input_tokens: 200000 @@ -1148,6 +1251,8 @@ output_price: 15 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: us.anthropic.claude-sonnet-4-6 max_input_tokens: 200000 max_output_tokens: 8192 @@ -1156,6 +1261,8 @@ output_price: 15 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, max] + default_reasoning_effort: high - name: us.anthropic.claude-sonnet-4-6:thinking real_name: us.anthropic.claude-sonnet-4-6 max_input_tokens: 200000 @@ -1672,6 +1779,8 @@ output_price: 30 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh, max] + default_reasoning_effort: medium - name: openai/gpt-5.6-terra max_input_tokens: 1050000 max_output_tokens: 128000 @@ -1679,6 +1788,8 @@ output_price: 30 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh, max] + default_reasoning_effort: medium - name: openai/gpt-5.6-luna max_input_tokens: 1050000 max_output_tokens: 128000 @@ -1686,6 +1797,8 @@ output_price: 30 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh, max] + default_reasoning_effort: medium - name: openai/gpt-5.5 max_input_tokens: 1050000 max_output_tokens: 128000 @@ -1693,6 +1806,8 @@ output_price: 30 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh] + default_reasoning_effort: medium - name: openai/gpt-5.5-pro max_input_tokens: 1050000 max_output_tokens: 128000 @@ -1700,6 +1815,8 @@ output_price: 180 supports_vision: true supports_function_calling: true + reasoning_levels: [medium, high, xhigh] + default_reasoning_effort: high - name: openai/gpt-5.4 max_input_tokens: 1050000 max_output_tokens: 128000 @@ -1707,6 +1824,8 @@ output_price: 15 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh] + default_reasoning_effort: none - name: openai/gpt-5.4-pro max_input_tokens: 1050000 max_output_tokens: 128000 @@ -1714,6 +1833,8 @@ output_price: 180 supports_vision: true supports_function_calling: true + reasoning_levels: [medium, high, xhigh] + default_reasoning_effort: medium - name: openai/gpt-5.4-mini max_input_tokens: 400000 max_output_tokens: 128000 @@ -1721,6 +1842,8 @@ output_price: 4.5 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh] + default_reasoning_effort: none - name: openai/gpt-5.4-nano max_input_tokens: 400000 max_output_tokens: 128000 @@ -1728,6 +1851,8 @@ output_price: 1.25 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh] + default_reasoning_effort: none - name: openai/gpt-5.3-codex max_input_tokens: 400000 max_output_tokens: 128000 @@ -1735,6 +1860,8 @@ output_price: 14 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh] + default_reasoning_effort: medium - name: openai/gpt-5.2 max_input_tokens: 400000 max_output_tokens: 128000 @@ -1742,6 +1869,17 @@ output_price: 14 supports_vision: true supports_function_calling: true + reasoning_levels: [none, low, medium, high, xhigh] + default_reasoning_effort: none + - name: openai/gpt-5.2-pro + max_input_tokens: 400000 + max_output_tokens: 128000 + input_price: 21 + output_price: 168 + supports_vision: true + supports_function_calling: true + reasoning_levels: [medium, high, xhigh] + default_reasoning_effort: medium - name: openai/gpt-5 max_input_tokens: 400000 max_output_tokens: 128000 @@ -1749,6 +1887,8 @@ output_price: 10 supports_vision: true supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: medium - name: openai/gpt-5-mini max_input_tokens: 400000 max_output_tokens: 128000 @@ -1786,18 +1926,67 @@ input_price: 0.04 output_price: 0.16 supports_function_calling: true + - name: google/gemini-3.5-flash + max_input_tokens: 1048576 + max_output_tokens: 65536 + input_price: 0.2 + output_price: 1.5 + supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: medium + - name: google/gemini-3-flash-preview + max_input_tokens: 1048576 + max_output_tokens: 65536 + input_price: 0.2 + output_price: 1.5 + supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: high + - name: google/gemini-3.1-flash-lite + max_input_tokens: 1048576 + max_output_tokens: 65536 + input_price: 0.2 + output_price: 1.5 + supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_effort: minimal + - name: google/gemini-3.1-pro-preview + max_input_tokens: 1048576 + max_output_tokens: 65535 + input_price: 0.3 + output_price: 2.5 + supports_vision: true + supports_function_calling: true + reasoning_levels: [low, medium, high] + default_reasoning_effort: high + - name: google/gemini-3-pro-preview + max_input_tokens: 1048576 + supports_vision: true + supports_function_calling: true + reasoning_levels: [low, high] + default_reasoning_level: high + - name: google/gemini-3-flash-preview + max_input_tokens: 1048576 + supports_vision: true + supports_function_calling: true + reasoning_levels: [minimal, low, medium, high] + default_reasoning_level: high - name: google/gemini-2.5-flash max_input_tokens: 1048576 input_price: 0.3 output_price: 2.5 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high] + default_reasoning_effort: low - name: google/gemini-2.5-pro max_input_tokens: 1048576 input_price: 1.25 output_price: 10 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high] + default_reasoning_effort: high - name: google/gemini-2.5-flash-lite max_input_tokens: 1048576 input_price: 0.3 @@ -1827,6 +2016,8 @@ output_price: 50 supports_function_calling: true supports_vision: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: anthropic/claude-opus-4-8 max_input_tokens: 1000000 max_output_tokens: 128000 @@ -1835,6 +2026,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: anthropic/claude-opus-4-7 max_input_tokens: 1000000 max_output_tokens: 128000 @@ -1843,6 +2036,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: anthropic/claude-opus-4.6 max_input_tokens: 200000 max_output_tokens: 8192 @@ -1851,6 +2046,8 @@ output_price: 25 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, max] + default_reasoning_effort: high - name: anthropic/claude-sonnet-5 max_input_tokens: 1000000 max_output_tokens: 128000 @@ -1859,6 +2056,8 @@ output_price: 15 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, xhigh, max] + default_reasoning_effort: high - name: anthropic/claude-sonnet-4.6 max_input_tokens: 200000 max_output_tokens: 8192 @@ -1867,6 +2066,8 @@ output_price: 15 supports_vision: true supports_function_calling: true + reasoning_levels: [low, medium, high, max] + default_reasoning_effort: high - name: anthropic/claude-opus-4.5 max_input_tokens: 200000 max_output_tokens: 8192 diff --git a/src/client/bedrock.rs b/src/client/bedrock.rs index b521d25..2f15dcb 100644 --- a/src/client/bedrock.rs +++ b/src/client/bedrock.rs @@ -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() diff --git a/src/client/claude.rs b/src/client/claude.rs index cacf4d5..e20726e 100644 --- a/src/client/claude.rs +++ b/src/client/claude.rs @@ -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(); } diff --git a/src/client/common.rs b/src/client/common.rs index a3e55ba..1fb0435 100644 --- a/src/client/common.rs +++ b/src/client/common.rs @@ -286,6 +286,7 @@ pub struct ChatCompletionsData { pub messages: Vec, pub temperature: Option, pub top_p: Option, + pub reasoning_effort: Option, pub functions: Option>, pub stream: bool, } diff --git a/src/client/model.rs b/src/client/model.rs index 325ce47..0dcb150 100644 --- a/src/client/model.rs +++ b/src/client/model.rs @@ -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, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_reasoning_effort: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] no_stream: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] diff --git a/src/client/openai.rs b/src/client/openai.rs index 54ddae6..f49937f 100644 --- a/src/client/openai.rs +++ b/src/client/openai.rs @@ -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(); } diff --git a/src/client/vertexai.rs b/src/client/vertexai.rs index e7bf287..d779c1e 100644 --- a/src/client/vertexai.rs +++ b/src/client/vertexai.rs @@ -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. diff --git a/src/config/agent.rs b/src/config/agent.rs index a7c0b4b..86715c8 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -575,6 +575,10 @@ impl RoleLike for Agent { self.config.top_p } + fn reasoning_effort(&self) -> Option { + self.config.reasoning_effort.clone() + } + fn enabled_tools(&self) -> Option> { None } @@ -596,6 +600,10 @@ impl RoleLike for Agent { self.config.top_p = value; } + fn set_reasoning_effort(&mut self, value: Option) { + self.config.reasoning_effort = value; + } + fn set_enabled_tools(&mut self, value: Option>) { match value { Some(tools) => { @@ -637,6 +645,8 @@ pub struct AgentConfig { #[serde(skip_serializing_if = "Option::is_none")] pub top_p: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub agent_session: Option, #[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::(&with_prefix("top_p")) { self.top_p = v; } + if let Some(v) = read_env_value::(&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) { diff --git a/src/config/app_config.rs b/src/config/app_config.rs index 0c660cf..29b4efe 100644 --- a/src/config/app_config.rs +++ b/src/config/app_config.rs @@ -21,6 +21,7 @@ pub struct AppConfig { pub model_id: String, pub temperature: Option, pub top_p: Option, + pub reasoning_effort: Option, 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::(&get_env_name("top_p")) { self.top_p = v; } + if let Some(v) = super::read_env_value::(&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; diff --git a/src/config/input.rs b/src/config/input.rs index 89aa8ee..27ecbd5 100644 --- a/src/config/input.rs +++ b/src/config/input.rs @@ -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, }) diff --git a/src/config/request_context.rs b/src/config/request_context.rs index 0781c24..8ee2c74 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -969,6 +969,16 @@ impl RequestContext { } } + pub fn set_reasoning_effort_on_role_like(&mut self, value: Option) -> 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>) -> 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 = 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 = super::parse_value(value)?; let parsed: Option> = 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(); diff --git a/src/config/role.rs b/src/config/role.rs index 63272c4..d996ed8 100644 --- a/src/config/role.rs +++ b/src/config/role.rs @@ -32,7 +32,9 @@ pub trait RoleLike { fn enabled_mcp_servers(&self) -> Option>; fn set_model(&mut self, model: Model); fn set_temperature(&mut self, value: Option); + fn reasoning_effort(&self) -> Option; fn set_top_p(&mut self, value: Option); + fn set_reasoning_effort(&mut self, value: Option); fn set_enabled_tools(&mut self, value: Option>); fn set_enabled_mcp_servers(&mut self, value: Option>); } @@ -51,6 +53,8 @@ pub struct Role { temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] top_p: Option, + #[serde(skip_serializing_if = "Option::is_none")] + reasoning_effort: Option, #[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 { + self.reasoning_effort.clone() + } + fn enabled_tools(&self) -> Option> { self.enabled_tools.clone() } @@ -433,6 +450,10 @@ impl RoleLike for Role { self.top_p = value; } + fn set_reasoning_effort(&mut self, value: Option) { + self.reasoning_effort = value; + } + fn set_enabled_tools(&mut self, value: Option>) { self.enabled_tools = value; } diff --git a/src/config/session.rs b/src/config/session.rs index 37d6694..82baf5b 100644 --- a/src/config/session.rs +++ b/src/config/session.rs @@ -24,6 +24,8 @@ pub struct Session { temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] top_p: Option, + #[serde(skip_serializing_if = "Option::is_none")] + reasoning_effort: Option, #[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 { + self.reasoning_effort.clone() + } + fn enabled_tools(&self) -> Option> { self.enabled_tools.clone() } @@ -823,6 +830,13 @@ impl RoleLike for Session { } } + fn set_reasoning_effort(&mut self, value: Option) { + if self.reasoning_effort != value { + self.reasoning_effort = value; + self.dirty = true; + } + } + fn set_enabled_tools(&mut self, value: Option>) { if self.enabled_tools != value { self.enabled_tools = value; diff --git a/src/graph/llm.rs b/src/graph/llm.rs index 9008d55..b62f4a2 100644 --- a/src/graph/llm.rs +++ b/src/graph/llm.rs @@ -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, diff --git a/src/graph/types.rs b/src/graph/types.rs index 437ddd7..020badf 100644 --- a/src/graph/types.rs +++ b/src/graph/types.rs @@ -25,6 +25,9 @@ pub struct Graph { #[serde(default, skip_serializing_if = "Option::is_none")] pub top_p: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + #[serde(default)] pub global_tools: Vec, @@ -288,6 +291,9 @@ pub struct LlmNode { #[serde(default, skip_serializing_if = "Option::is_none")] pub top_p: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub fallback: Option, diff --git a/src/graph/validator.rs b/src/graph/validator.rs index 7d5c370..f655c8e 100644 --- a/src/graph/validator.rs +++ b/src/graph/validator.rs @@ -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, diff --git a/src/rag/graph.rs b/src/rag/graph.rs index a24455b..166a3e7 100644 --- a/src/rag/graph.rs +++ b/src/rag/graph.rs @@ -362,6 +362,7 @@ pub async fn extract_entities( messages, temperature: Some(0.0), top_p: None, + reasoning_effort: None, functions: None, stream: false, }; diff --git a/src/repl/mod.rs b/src/repl/mod.rs index a23cc88..071e5aa 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -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 ...") } }, + ".reasoning" => match args { + Some(level) => { + let set_args = format!("reasoning_effort {level}"); + ctx.update(&set_args, abort_signal).await?; + } + None => { + println!("Usage: .reasoning ") + } + }, ".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]