mirror of
https://github.com/openai/codex.git
synced 2026-10-21 05:22:21 +00:00
Record reasoning effort changes in conversation history behind a flag (#43110)
## What changed Add the disabled-by-default `reasoning_effort_override` feature for OpenAI models with `use_responses_lite` enabled. Append a trusted `configuration_update` after accepted input when no effort is established in surviving history or the resolved effort changes. Preserve the existing history prefix and continue sending the request-level reasoning effort. Share effort normalization through `ModelInfo::resolve_reasoning_effort` so requests and history updates resolve `ultra` consistently and translate `persistent` to `disabled`. Exclude other custom effort values from history updates, and compare only against harness-authored configuration items. ## Testing Add integration coverage for effort transitions, deduplication, history prefix and cache-key preservation, alias normalization, and feature/provider/model gating. Add unit coverage for model-specific `ultra` resolution, fallbacks, and `persistent` translation. GitOrigin-RevId: c3fe7050058076454d8ea20054a618bca9d48c7c
This commit is contained in:
@@ -879,6 +879,9 @@
|
||||
"realtime_conversation": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning_effort_override": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"recommended_plugins": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -6271,6 +6274,9 @@
|
||||
"realtime_conversation": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning_effort_override": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"recommended_plugins": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -183,42 +183,6 @@ pub(crate) struct CompactConversationRequestSettings {
|
||||
pub(crate) service_tier: Option<String>,
|
||||
}
|
||||
|
||||
fn reasoning_effort_for_request(
|
||||
model_info: &ModelInfo,
|
||||
effort: ReasoningEffortConfig,
|
||||
) -> ReasoningEffortConfig {
|
||||
match effort {
|
||||
ReasoningEffortConfig::Ultra => model_info
|
||||
.multi_agent_reasoning_effort
|
||||
.as_ref()
|
||||
.filter(|effort| {
|
||||
*effort != &ReasoningEffortConfig::Ultra
|
||||
&& model_info
|
||||
.supported_reasoning_levels
|
||||
.iter()
|
||||
.any(|preset| &preset.effort == *effort)
|
||||
})
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
let supported_reasoning_levels = &model_info.supported_reasoning_levels;
|
||||
supported_reasoning_levels
|
||||
.iter()
|
||||
.find(|preset| preset.effort == ReasoningEffortConfig::Max)
|
||||
.or_else(|| {
|
||||
supported_reasoning_levels
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|preset| preset.effort != ReasoningEffortConfig::Ultra)
|
||||
})
|
||||
.map(|preset| preset.effort.clone())
|
||||
})
|
||||
.unwrap_or(ReasoningEffortConfig::Medium),
|
||||
// Keep "persistent" in local settings; the Responses API calls it "disabled".
|
||||
ReasoningEffortConfig::Persistent => ReasoningEffortConfig::Custom("disabled".to_string()),
|
||||
effort => effort,
|
||||
}
|
||||
}
|
||||
|
||||
fn session_telemetry_for_request(
|
||||
session_telemetry: &SessionTelemetry,
|
||||
request: &ResponsesApiRequest,
|
||||
@@ -806,7 +770,7 @@ impl ModelClient {
|
||||
model: model_info.slug.clone(),
|
||||
raw_memories,
|
||||
reasoning: effort
|
||||
.map(|effort| reasoning_effort_for_request(model_info, effort))
|
||||
.map(|effort| model_info.resolve_reasoning_effort(effort))
|
||||
.map(|effort| Reasoning {
|
||||
effort: Some(effort),
|
||||
summary: None,
|
||||
@@ -912,7 +876,7 @@ impl ModelClient {
|
||||
Reasoning {
|
||||
effort: effort
|
||||
.or_else(|| model_info.default_reasoning_level.clone())
|
||||
.map(|effort| reasoning_effort_for_request(model_info, effort)),
|
||||
.map(|effort| model_info.resolve_reasoning_effort(effort)),
|
||||
summary: (model_info.supports_reasoning_summary_parameter
|
||||
&& summary != ReasoningSummaryConfig::None)
|
||||
.then_some(summary),
|
||||
|
||||
@@ -230,6 +230,7 @@ mod mcp_refresh;
|
||||
mod mcp_runtime;
|
||||
pub(crate) mod multi_agents;
|
||||
mod realtime_history;
|
||||
mod reasoning_effort;
|
||||
mod retained_context;
|
||||
mod review;
|
||||
mod rollout_budget;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Gated reasoning-effort updates appended after accepted input.
|
||||
//!
|
||||
//! Only trusted harness items establish an effort. Updates append to surviving
|
||||
//! history without replacing the request-level reasoning effort.
|
||||
|
||||
use super::session::Session;
|
||||
use super::step_context::StepContext;
|
||||
use super::step_settings::ResolvedStepSettings;
|
||||
use codex_features::Feature;
|
||||
use codex_history::CodexHarnessMetadata;
|
||||
use codex_history::ResponseItemEnvelope;
|
||||
use codex_protocol::models::ConfigurationReasoning;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
|
||||
impl Session {
|
||||
/// Establishes the selected effort in surviving history, independent of replayed settings.
|
||||
pub(crate) async fn record_reasoning_effort_override(&self, step_context: &StepContext) {
|
||||
let settings = &step_context.settings;
|
||||
let Some(effort) = self.effort_for_configuration_update(settings).await else {
|
||||
return;
|
||||
};
|
||||
let should_skip = {
|
||||
let state = self.state.lock().await;
|
||||
let established_effort =
|
||||
state
|
||||
.history
|
||||
.annotated_items()
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|envelope| {
|
||||
if !envelope
|
||||
.metadata
|
||||
.as_ref()
|
||||
.is_some_and(|metadata| metadata.harness_authored_configuration)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
match &envelope.item {
|
||||
ResponseItem::ConfigurationUpdate { reasoning } => {
|
||||
Some(&reasoning.effort)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
established_effort == Some(&effort)
|
||||
};
|
||||
if should_skip {
|
||||
return;
|
||||
}
|
||||
|
||||
self.record_annotated_conversation_items(
|
||||
step_context.turn.as_ref(),
|
||||
vec![ResponseItemEnvelope {
|
||||
item: ResponseItem::ConfigurationUpdate {
|
||||
reasoning: ConfigurationReasoning { effort },
|
||||
},
|
||||
metadata: Some(CodexHarnessMetadata {
|
||||
harness_authored_configuration: true,
|
||||
..Default::default()
|
||||
}),
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn effort_for_configuration_update(
|
||||
&self,
|
||||
settings: &ResolvedStepSettings,
|
||||
) -> Option<ReasoningEffort> {
|
||||
if !self.enabled(Feature::ReasoningEffortOverride)
|
||||
|| !settings.model_info.use_responses_lite
|
||||
|| !self.provider().await.is_openai()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let effort = settings
|
||||
.model_info
|
||||
.resolve_reasoning_effort(settings.effective_reasoning_effort()?);
|
||||
// Persistent normalizes to "disabled". Keep unknown custom values out of
|
||||
// durable updates so injected items stay bounded to known backend modes.
|
||||
if matches!(&effort, ReasoningEffort::Custom(value) if value != "disabled") {
|
||||
return None;
|
||||
}
|
||||
Some(effort)
|
||||
}
|
||||
}
|
||||
@@ -413,6 +413,10 @@ pub(crate) async fn run_turn(
|
||||
.record_step_world_state_if_changed(&world_state, step_context.as_ref())
|
||||
.await?;
|
||||
|
||||
// Keep the override after accepted input so ordinary turn rollback removes it too.
|
||||
sess.record_reasoning_effort_override(step_context.as_ref())
|
||||
.await;
|
||||
|
||||
// Construct the input that we will send to the model.
|
||||
let sampling_request_input: Vec<ResponseItem> = async {
|
||||
sess.clone_history()
|
||||
|
||||
@@ -128,6 +128,7 @@ mod quota_exceeded;
|
||||
mod realtime_conversation;
|
||||
mod realtime_initial_items;
|
||||
mod realtime_sideband_endpoint;
|
||||
mod reasoning_effort_override;
|
||||
mod remote_env;
|
||||
mod remote_models;
|
||||
mod request_compression;
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
//! Trusted reasoning-effort updates follow surviving history and the next turn's selected settings.
|
||||
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ResponsesRequest;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::submit_thread_settings;
|
||||
use core_test_support::test_codex::TestCodexBuilder;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use test_case::test_case;
|
||||
|
||||
fn override_builder() -> TestCodexBuilder {
|
||||
test_codex()
|
||||
.with_model_info_override("gpt-5.4", |model| {
|
||||
model.use_responses_lite = true;
|
||||
})
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ReasoningEffortOverride)
|
||||
.expect("enable reasoning effort overrides");
|
||||
config.model_reasoning_effort = Some(ReasoningEffort::Medium);
|
||||
})
|
||||
}
|
||||
|
||||
fn effort_updates(request: &ResponsesRequest) -> Vec<Value> {
|
||||
request
|
||||
.input()
|
||||
.into_iter()
|
||||
.filter(|item| item["type"] == "configuration_update")
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn effort_update(effort: ReasoningEffort) -> Value {
|
||||
serde_json::json!({
|
||||
"type": "configuration_update",
|
||||
"reasoning": {"effort": effort},
|
||||
})
|
||||
}
|
||||
|
||||
fn message(role: &str, text: &str) -> Value {
|
||||
serde_json::json!({
|
||||
"type": "message",
|
||||
"role": role,
|
||||
"content": [{"type": "input_text", "text": text}],
|
||||
})
|
||||
}
|
||||
|
||||
#[test_case(ReasoningEffort::High; "high to persistent and back")]
|
||||
#[test_case(ReasoningEffort::Persistent; "persistent to high and back")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn reasoning_effort_override_persistent_transitions(
|
||||
initial_effort: ReasoningEffort,
|
||||
) -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = responses::start_mock_server().await;
|
||||
let mut mocks = Vec::new();
|
||||
for id in ["first", "changed", "unchanged", "restored"] {
|
||||
mocks.push(
|
||||
responses::mount_sse_once(&server, responses::sse(vec![responses::ev_completed(id)]))
|
||||
.await,
|
||||
);
|
||||
}
|
||||
let high = effort_update(ReasoningEffort::High);
|
||||
let disabled = effort_update(ReasoningEffort::Custom("disabled".to_string()));
|
||||
let (changed_effort, initial_update, changed_update) =
|
||||
if initial_effort == ReasoningEffort::Persistent {
|
||||
(ReasoningEffort::High, disabled, high)
|
||||
} else {
|
||||
(ReasoningEffort::Persistent, high, disabled)
|
||||
};
|
||||
let test = override_builder().build_with_auto_env(&server).await?;
|
||||
for effort in [
|
||||
initial_effort.clone(),
|
||||
changed_effort.clone(),
|
||||
changed_effort,
|
||||
initial_effort,
|
||||
] {
|
||||
submit_thread_settings(
|
||||
&test.codex,
|
||||
ThreadSettingsOverrides {
|
||||
effort: Some(Some(effort)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
test.submit_text_turn("continue").await?;
|
||||
}
|
||||
assert_eq!(
|
||||
mocks
|
||||
.iter()
|
||||
.map(|mock| effort_updates(&mock.single_request()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
vec![initial_update.clone()],
|
||||
vec![initial_update.clone(), changed_update.clone()],
|
||||
vec![initial_update.clone(), changed_update.clone()],
|
||||
vec![initial_update.clone(), changed_update, initial_update],
|
||||
],
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn reasoning_effort_override_preserves_prefix_and_only_appends_on_change()
|
||||
-> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = responses::start_mock_server().await;
|
||||
let mut mocks = Vec::new();
|
||||
for id in ["first", "changed", "unchanged", "lowered"] {
|
||||
mocks.push(
|
||||
responses::mount_sse_once(&server, responses::sse(vec![responses::ev_completed(id)]))
|
||||
.await,
|
||||
);
|
||||
}
|
||||
let test = override_builder().build_with_auto_env(&server).await?;
|
||||
test.submit_text_turn("first message").await?;
|
||||
submit_thread_settings(
|
||||
&test.codex,
|
||||
ThreadSettingsOverrides {
|
||||
effort: Some(Some(ReasoningEffort::High)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
test.submit_text_turn("second message").await?;
|
||||
test.submit_text_turn("third message").await?;
|
||||
submit_thread_settings(
|
||||
&test.codex,
|
||||
ThreadSettingsOverrides {
|
||||
effort: Some(Some(ReasoningEffort::Low)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
test.submit_text_turn("fourth message").await?;
|
||||
|
||||
let requests = mocks
|
||||
.iter()
|
||||
.map(responses::ResponseMock::single_request)
|
||||
.collect::<Vec<_>>();
|
||||
let inputs = requests
|
||||
.iter()
|
||||
.map(|request| {
|
||||
responses::strip_response_item_ids_from_json(responses::strip_metadata_from_json(
|
||||
Value::Array(request.input()),
|
||||
))
|
||||
.as_array()
|
||||
.expect("input array")
|
||||
.clone()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
requests
|
||||
.iter()
|
||||
.map(|request| request.body_json()["reasoning"]["effort"].clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
Value::from("medium"),
|
||||
Value::from("high"),
|
||||
Value::from("high"),
|
||||
Value::from("low"),
|
||||
],
|
||||
);
|
||||
let cache_keys = requests
|
||||
.iter()
|
||||
.map(|request| request.body_json()["prompt_cache_key"].clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(cache_keys, vec![cache_keys[0].clone(); requests.len()]);
|
||||
assert_eq!(
|
||||
inputs[0][inputs[0].len() - 2..],
|
||||
[
|
||||
message("user", "first message"),
|
||||
effort_update(ReasoningEffort::Medium)
|
||||
],
|
||||
);
|
||||
let mut expected = inputs[0].clone();
|
||||
expected.extend([
|
||||
message("user", "second message"),
|
||||
effort_update(ReasoningEffort::High),
|
||||
]);
|
||||
assert_eq!(inputs[1], expected);
|
||||
expected.push(message("user", "third message"));
|
||||
assert_eq!(inputs[2], expected);
|
||||
expected.extend([
|
||||
message("user", "fourth message"),
|
||||
effort_update(ReasoningEffort::Low),
|
||||
]);
|
||||
assert_eq!(inputs[3], expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(ReasoningEffort::High; "model maps ultra to high")]
|
||||
#[test_case(ReasoningEffort::Max; "model maps ultra to max")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn reasoning_effort_override_normalizes_ultra_before_comparing_updates(
|
||||
resolved_effort: ReasoningEffort,
|
||||
) -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = responses::start_mock_server().await;
|
||||
let first = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![responses::ev_completed("first")]),
|
||||
)
|
||||
.await;
|
||||
let second = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![responses::ev_completed("second")]),
|
||||
)
|
||||
.await;
|
||||
let model_effort = resolved_effort.clone();
|
||||
let test = override_builder()
|
||||
.with_model_info_override("gpt-5.4", move |model| {
|
||||
model.multi_agent_reasoning_effort = Some(model_effort.clone());
|
||||
model.supported_reasoning_levels = vec![ReasoningEffortPreset {
|
||||
effort: model_effort,
|
||||
description: "Model effort".to_string(),
|
||||
}];
|
||||
})
|
||||
.with_config(|config| config.model_reasoning_effort = Some(ReasoningEffort::Ultra))
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
test.submit_text_turn("ultra selection").await?;
|
||||
submit_thread_settings(
|
||||
&test.codex,
|
||||
ThreadSettingsOverrides {
|
||||
effort: Some(Some(resolved_effort.clone())),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
test.submit_text_turn("equivalent explicit effort").await?;
|
||||
let requests = [first.single_request(), second.single_request()];
|
||||
assert_eq!(
|
||||
requests.each_ref().map(effort_updates),
|
||||
[
|
||||
vec![effort_update(resolved_effort.clone())],
|
||||
vec![effort_update(resolved_effort.clone())],
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
requests.map(|request| request.body_json()["reasoning"]["effort"].clone()),
|
||||
[
|
||||
serde_json::to_value(&resolved_effort)?,
|
||||
serde_json::to_value(&resolved_effort)?
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum OverrideUnavailable {
|
||||
FeatureDisabled,
|
||||
NonOpenAiProvider,
|
||||
ResponsesLiteDisabled,
|
||||
}
|
||||
|
||||
#[test_case(OverrideUnavailable::FeatureDisabled; "feature disabled")]
|
||||
#[test_case(OverrideUnavailable::NonOpenAiProvider; "unsupported provider")]
|
||||
#[test_case(OverrideUnavailable::ResponsesLiteDisabled; "responses lite disabled")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn reasoning_effort_override_unavailable_uses_request_effort(
|
||||
unavailable: OverrideUnavailable,
|
||||
) -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = responses::start_mock_server().await;
|
||||
let first = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![responses::ev_completed("first")]),
|
||||
)
|
||||
.await;
|
||||
let second = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![responses::ev_completed("second")]),
|
||||
)
|
||||
.await;
|
||||
let mut builder = match unavailable {
|
||||
OverrideUnavailable::FeatureDisabled => override_builder().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.disable(Feature::ReasoningEffortOverride)
|
||||
.expect("disable overrides");
|
||||
}),
|
||||
OverrideUnavailable::NonOpenAiProvider => override_builder().with_config(|config| {
|
||||
config.model_provider.name = "unsupported provider".into();
|
||||
}),
|
||||
OverrideUnavailable::ResponsesLiteDisabled => override_builder()
|
||||
.with_model_info_override("gpt-5.4", |model| model.use_responses_lite = false),
|
||||
};
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
test.submit_text_turn("first").await?;
|
||||
submit_thread_settings(
|
||||
&test.codex,
|
||||
ThreadSettingsOverrides {
|
||||
effort: Some(Some(ReasoningEffort::High)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
test.submit_text_turn("second").await?;
|
||||
let requests = [first.single_request(), second.single_request()];
|
||||
assert_eq!(
|
||||
requests.each_ref().map(effort_updates),
|
||||
[Vec::<Value>::new(), Vec::new()]
|
||||
);
|
||||
assert_eq!(
|
||||
requests.map(|request| request.body_json()["reasoning"]["effort"].clone()),
|
||||
[Value::from("medium"), Value::from("high")],
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -324,6 +324,8 @@ pub enum Feature {
|
||||
ContextManagement,
|
||||
/// Track and report a shared token budget across a session's agent threads.
|
||||
RolloutBudget,
|
||||
/// Append trusted response configuration items when the selected reasoning effort changes.
|
||||
ReasoningEffortOverride,
|
||||
/// Add current-time reminders to model-visible context.
|
||||
CurrentTimeReminder,
|
||||
/// Route MCP tool approval prompts through the MCP elicitation request path.
|
||||
@@ -1607,6 +1609,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ReasoningEffortOverride,
|
||||
key: "reasoning_effort_override",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::CurrentTimeReminder,
|
||||
key: "current_time_reminder",
|
||||
|
||||
@@ -41,6 +41,8 @@ pub use guardian::GuardianScope;
|
||||
|
||||
#[path = "openai_models/guardian_v2.rs"]
|
||||
mod guardian_v2;
|
||||
#[path = "openai_models/reasoning_effort.rs"]
|
||||
mod reasoning_effort;
|
||||
|
||||
pub use guardian_v2::GuardianV2ModelConfig;
|
||||
pub use guardian_v2::GuardianV2TranscriptModelConfig;
|
||||
@@ -1020,7 +1022,6 @@ mod tests {
|
||||
multi_agent_reasoning_effort: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn personality_variables() -> ModelInstructionsVariables {
|
||||
ModelInstructionsVariables {
|
||||
personality_default: Some("default".to_string()),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//! Model-owned effort normalization shared by native requests and configuration update items.
|
||||
//!
|
||||
//! UI aliases resolve through the same model defaults and fallbacks in both paths.
|
||||
|
||||
use super::ModelInfo;
|
||||
use super::ReasoningEffort;
|
||||
|
||||
impl ModelInfo {
|
||||
/// Resolves a selected effort to the value sent in an ordinary inference request.
|
||||
pub fn resolve_reasoning_effort(&self, effort: ReasoningEffort) -> ReasoningEffort {
|
||||
match effort {
|
||||
ReasoningEffort::Ultra => self
|
||||
.multi_agent_reasoning_effort
|
||||
.as_ref()
|
||||
.filter(|effort| {
|
||||
*effort != &ReasoningEffort::Ultra
|
||||
&& self
|
||||
.supported_reasoning_levels
|
||||
.iter()
|
||||
.any(|preset| &preset.effort == *effort)
|
||||
})
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
self.supported_reasoning_levels
|
||||
.iter()
|
||||
.find(|preset| preset.effort == ReasoningEffort::Max)
|
||||
.or_else(|| {
|
||||
self.supported_reasoning_levels
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|preset| preset.effort != ReasoningEffort::Ultra)
|
||||
})
|
||||
.map(|preset| preset.effort.clone())
|
||||
})
|
||||
.unwrap_or(ReasoningEffort::Medium),
|
||||
// Keep "persistent" in local settings; the Responses API calls it "disabled".
|
||||
ReasoningEffort::Persistent => ReasoningEffort::Custom("disabled".to_string()),
|
||||
effort => effort,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[path = "reasoning_effort_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,52 @@
|
||||
use super::super::ReasoningEffortPreset;
|
||||
use super::super::tests::test_model;
|
||||
use super::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn resolves_ultra_using_model_override_and_supported_fallbacks() {
|
||||
use ReasoningEffort::High;
|
||||
use ReasoningEffort::Low;
|
||||
use ReasoningEffort::Max;
|
||||
use ReasoningEffort::Medium;
|
||||
use ReasoningEffort::Ultra;
|
||||
use ReasoningEffort::XHigh;
|
||||
|
||||
for (supported, multi_agent_effort, expected) in [
|
||||
(vec![High, Max, Ultra], Some(High), High),
|
||||
(vec![High, Max, Ultra], None, Max),
|
||||
(vec![High, XHigh, Ultra], None, XHigh),
|
||||
(vec![High, XHigh, Ultra], Some(Ultra), XHigh),
|
||||
(vec![High, XHigh, Ultra], Some(Low), XHigh),
|
||||
(vec![], None, Medium),
|
||||
] {
|
||||
let mut model = test_model(/*spec*/ None);
|
||||
model.supported_reasoning_levels = supported
|
||||
.into_iter()
|
||||
.map(|effort| ReasoningEffortPreset {
|
||||
effort,
|
||||
description: String::new(),
|
||||
})
|
||||
.collect();
|
||||
model.multi_agent_reasoning_effort = multi_agent_effort;
|
||||
assert_eq!(model.resolve_reasoning_effort(Ultra), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_native_efforts_and_translates_persistent_mode() {
|
||||
let model = test_model(/*spec*/ None);
|
||||
for effort in [
|
||||
ReasoningEffort::Low,
|
||||
ReasoningEffort::Medium,
|
||||
ReasoningEffort::High,
|
||||
ReasoningEffort::Max,
|
||||
ReasoningEffort::Custom("future-effort".to_string()),
|
||||
] {
|
||||
assert_eq!(model.resolve_reasoning_effort(effort.clone()), effort);
|
||||
}
|
||||
assert_eq!(
|
||||
model.resolve_reasoning_effort(ReasoningEffort::Persistent),
|
||||
ReasoningEffort::Custom("disabled".to_string()),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user