From 1ebbf5035fd060711cc7c85bd83bdac3658d3a9d Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 29 May 2026 18:28:27 +0200 Subject: [PATCH] x_ai: Add support for specifying reasoning effort (#58078) See https://docs.x.ai/developers/model-capabilities/text/reasoning#the-reasoning_effort-parameter Closes #58056 Release Notes: - agent: Added support for specifying reasoning effort for Grok 4.3 (xAI) --- crates/language_models/src/provider/x_ai.rs | 139 +++++++++++++++++++- crates/x_ai/src/x_ai.rs | 7 + 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 623853b521..51eb2e3b81 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -6,10 +6,10 @@ use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, TaskExt, Window}; use http_client::HttpClient; use language_model::{ ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError, - LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, - LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, - LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, - env_var, + LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName, + LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, + LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, + LanguageModelToolSchemaFormat, RateLimiter, env_var, }; use open_ai::ResponseStreamEvent; pub use settings::XaiAvailableModel as AvailableModel; @@ -255,6 +255,75 @@ impl XAiLanguageModel { } } +fn x_ai_reasoning_efforts(model: &x_ai::Model) -> &'static [open_ai::ReasoningEffort] { + if model.supports_reasoning_effort() { + &[ + open_ai::ReasoningEffort::None, + open_ai::ReasoningEffort::Low, + open_ai::ReasoningEffort::Medium, + open_ai::ReasoningEffort::High, + ] + } else { + &[] + } +} + +fn default_thinking_reasoning_effort(model: &x_ai::Model) -> Option { + if model.supports_reasoning_effort() { + Some(open_ai::ReasoningEffort::Low) + } else { + None + } +} + +fn reasoning_effort_for_request( + request: &LanguageModelRequest, + model: &x_ai::Model, +) -> Option { + let supported_efforts = x_ai_reasoning_efforts(model); + if supported_efforts.is_empty() { + return None; + } + + if request.thinking_allowed { + request + .thinking_effort + .as_deref() + .and_then(|effort| effort.parse::().ok()) + .filter(|effort| supported_efforts.contains(effort)) + .filter(|effort| *effort != open_ai::ReasoningEffort::None) + .or_else(|| default_thinking_reasoning_effort(model)) + } else if supported_efforts.contains(&open_ai::ReasoningEffort::None) { + Some(open_ai::ReasoningEffort::None) + } else { + None + } +} + +fn supported_thinking_effort_levels(model: &x_ai::Model) -> Vec { + let default_effort = default_thinking_reasoning_effort(model); + x_ai_reasoning_efforts(model) + .iter() + .copied() + .filter_map(|effort| { + let (name, value) = match effort { + open_ai::ReasoningEffort::None => return None, + open_ai::ReasoningEffort::Minimal => ("Minimal", "minimal"), + open_ai::ReasoningEffort::Low => ("Low", "low"), + open_ai::ReasoningEffort::Medium => ("Medium", "medium"), + open_ai::ReasoningEffort::High => ("High", "high"), + open_ai::ReasoningEffort::XHigh => ("Extra High", "xhigh"), + }; + + Some(LanguageModelEffortLevel { + name: name.into(), + value: value.into(), + is_default: Some(effort) == default_effort, + }) + }) + .collect() +} + impl LanguageModel for XAiLanguageModel { fn id(&self) -> LanguageModelId { self.id.clone() @@ -291,6 +360,15 @@ impl LanguageModel for XAiLanguageModel { | LanguageModelToolChoice::None => true, } } + + fn supports_thinking(&self) -> bool { + self.model.supports_reasoning_effort() + } + + fn supported_effort_levels(&self) -> Vec { + supported_thinking_effort_levels(&self.model) + } + fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { if self.model.requires_json_schema_subset() { LanguageModelToolSchemaFormat::JsonSchemaSubset @@ -329,13 +407,14 @@ impl LanguageModel for XAiLanguageModel { LanguageModelCompletionError, >, > { + let reasoning_effort = reasoning_effort_for_request(&request, &self.model); let request = crate::provider::open_ai::into_open_ai( request, self.model.id(), self.model.supports_parallel_tool_calls(), self.model.supports_prompt_cache_key(), self.max_output_tokens(), - None, + reasoning_effort, false, ); let completions = self.stream_completion(request, cx); @@ -428,6 +507,56 @@ impl ConfigurationView { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn grok_43_supports_selectable_thinking_effort_levels() { + let effort_levels = supported_thinking_effort_levels(&x_ai::Model::Grok43); + let values = effort_levels + .iter() + .map(|level| level.value.as_ref()) + .collect::>(); + + assert_eq!(values, ["low", "medium", "high"]); + assert_eq!( + effort_levels + .iter() + .find(|level| level.is_default) + .map(|level| level.value.as_ref()), + Some("low") + ); + } + + #[test] + fn grok_43_request_uses_selected_reasoning_effort() { + let request = LanguageModelRequest { + thinking_allowed: true, + thinking_effort: Some("high".to_string()), + ..Default::default() + }; + + assert_eq!( + reasoning_effort_for_request(&request, &x_ai::Model::Grok43), + Some(open_ai::ReasoningEffort::High) + ); + } + + #[test] + fn grok_43_request_uses_none_when_thinking_is_disabled() { + let request = LanguageModelRequest { + thinking_allowed: false, + ..Default::default() + }; + + assert_eq!( + reasoning_effort_for_request(&request, &x_ai::Model::Grok43), + Some(open_ai::ReasoningEffort::None) + ); + } +} + impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let env_var_set = self.state.read(cx).api_key_state.is_from_env_var(); diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs index 7ba13d8352..6d9c558de8 100644 --- a/crates/x_ai/src/x_ai.rs +++ b/crates/x_ai/src/x_ai.rs @@ -122,4 +122,11 @@ impl Model { Self::Custom { .. } => false, } } + + pub fn supports_reasoning_effort(&self) -> bool { + match self { + Self::Grok43 => true, + Self::Grok420Reasoning | Self::Grok420NonReasoning | Self::Custom { .. } => false, + } + } }