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)
This commit is contained in:
Bennet Bo Fenner 2026-05-29 18:28:27 +02:00 committed by GitHub
parent 2ea99a81f1
commit 122619624d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 141 additions and 5 deletions

View file

@ -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<open_ai::ReasoningEffort> {
if model.supports_reasoning_effort() {
Some(open_ai::ReasoningEffort::Low)
} else {
None
}
}
fn reasoning_effort_for_request(
request: &LanguageModelRequest,
model: &x_ai::Model,
) -> Option<open_ai::ReasoningEffort> {
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::<open_ai::ReasoningEffort>().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<LanguageModelEffortLevel> {
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<LanguageModelEffortLevel> {
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::<Vec<_>>();
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<Self>) -> impl IntoElement {
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();

View file

@ -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,
}
}
}