open_ai: Support specifying reasoning effort (#56411) (cherry-pick to stable) (#56808)
Some checks are pending
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions

Cherry-pick of #56411 to stable

----
Closes #54875

Release Notes:

- Added support for specifying effort level when using OpenAI models

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
This commit is contained in:
zed-zippy[bot] 2026-05-14 22:25:36 +00:00 committed by GitHub
parent a510bb493f
commit 8524c06024
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 143 additions and 18 deletions

View file

@ -6,10 +6,10 @@ use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt,
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, OPEN_AI_PROVIDER_ID, OPEN_AI_PROVIDER_NAME,
RateLimiter, env_var,
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, OPEN_AI_PROVIDER_ID,
OPEN_AI_PROVIDER_NAME, RateLimiter, env_var,
};
use menu;
use open_ai::{
@ -351,7 +351,34 @@ impl LanguageModel for OpenAiLanguageModel {
}
fn supports_thinking(&self) -> bool {
self.model.reasoning_effort().is_some()
self.model.uses_responses_api() && self.model.reasoning_effort().is_some()
}
fn supported_effort_levels(&self) -> Vec<LanguageModelEffortLevel> {
if !self.supports_thinking() {
return Vec::new();
}
let default_effort = self.model.reasoning_effort();
self.model
.supported_reasoning_efforts()
.iter()
.map(|effort| {
let (name, value) = match effort {
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"),
};
LanguageModelEffortLevel {
name: name.into(),
value: value.into(),
is_default: Some(*effort) == default_effort,
}
})
.collect()
}
fn supports_split_token_display(&self) -> bool {
@ -406,7 +433,7 @@ impl LanguageModel for OpenAiLanguageModel {
self.model.supports_parallel_tool_calls(),
self.model.supports_prompt_cache_key(),
self.max_output_tokens(),
self.model.reasoning_effort(),
None,
false,
);
let completions = self.stream_completion(request, cx);

View file

@ -190,8 +190,8 @@ pub fn into_open_ai_response(
tool_choice,
stop: _,
temperature,
thinking_allowed: _,
thinking_effort: _,
thinking_allowed,
thinking_effort,
speed: _,
} = request;
@ -233,10 +233,18 @@ pub fn into_open_ai_response(
} else {
None
},
reasoning: reasoning_effort.map(|effort| crate::responses::ReasoningConfig {
effort,
summary: Some(crate::responses::ReasoningSummaryMode::Auto),
}),
reasoning: if thinking_allowed {
thinking_effort
.as_deref()
.and_then(|effort| effort.parse::<ReasoningEffort>().ok())
.or(reasoning_effort)
.map(|effort| crate::responses::ReasoningConfig {
effort,
summary: Some(crate::responses::ReasoningSummaryMode::Auto),
})
} else {
None
},
}
}
@ -1000,8 +1008,8 @@ mod tests {
tool_choice: Some(LanguageModelToolChoice::Any),
stop: vec!["<STOP>".into()],
temperature: None,
thinking_allowed: false,
thinking_effort: None,
thinking_allowed: true,
thinking_effort: Some("high".into()),
speed: None,
};
@ -1065,12 +1073,46 @@ mod tests {
}
],
"prompt_cache_key": "thread-123",
"reasoning": { "effort": "low", "summary": "auto" }
"reasoning": { "effort": "high", "summary": "auto" }
});
assert_eq!(serialized, expected);
}
#[test]
fn into_open_ai_response_omits_reasoning_when_thinking_is_disabled() {
let request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: None,
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text("Hello".into())],
cache: false,
reasoning_details: None,
}],
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature: None,
thinking_allowed: false,
thinking_effort: Some("high".into()),
speed: None,
};
let response = into_open_ai_response(
request,
"gpt-5",
true,
true,
None,
Some(ReasoningEffort::Medium),
);
let serialized = serde_json::to_value(&response).unwrap();
assert_eq!(serialized.get("reasoning"), None);
}
#[test]
fn responses_stream_maps_tool_calls() {
let events = vec![

View file

@ -265,13 +265,69 @@ impl Model {
Self::Custom {
reasoning_effort, ..
} => reasoning_effort.to_owned(),
Self::FivePointThreeCodex | Self::FivePointFourPro | Self::FivePointFivePro => {
Some(ReasoningEffort::Medium)
}
Self::O1
| Self::O3
| Self::O3Mini
| Self::Five
| Self::FiveCodex
| Self::FiveMini
| Self::FiveNano
| Self::FivePointOne
| Self::FivePointTwo
| Self::FivePointTwoCodex
| Self::FivePointThreeCodex
| Self::FivePointFour
| Self::FivePointFourPro
| Self::FivePointFive
| Self::FivePointFivePro => Some(ReasoningEffort::Medium),
_ => None,
}
}
pub fn supported_reasoning_efforts(&self) -> &'static [ReasoningEffort] {
match self {
Self::Custom {
reasoning_effort: Some(effort),
..
} => match effort {
ReasoningEffort::Minimal => &[ReasoningEffort::Minimal],
ReasoningEffort::Low => &[ReasoningEffort::Low],
ReasoningEffort::Medium => &[ReasoningEffort::Medium],
ReasoningEffort::High => &[ReasoningEffort::High],
ReasoningEffort::XHigh => &[ReasoningEffort::XHigh],
},
Self::O1 | Self::O3 | Self::O3Mini | Self::FivePointOne => &[
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
],
Self::Five | Self::FiveMini | Self::FiveNano => &[
ReasoningEffort::Minimal,
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
],
Self::FiveCodex
| Self::FivePointTwoCodex
| Self::FivePointThreeCodex
| Self::FivePointFourPro => &[
ReasoningEffort::Medium,
ReasoningEffort::High,
ReasoningEffort::XHigh,
],
Self::FivePointTwo
| Self::FivePointFour
| Self::FivePointFive
| Self::FivePointFivePro => &[
ReasoningEffort::Low,
ReasoningEffort::Medium,
ReasoningEffort::High,
ReasoningEffort::XHigh,
],
_ => &[],
}
}
pub fn uses_responses_api(&self) -> bool {
match self {
Self::Custom {