mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Merge ea6ac292ea into sapling-pr-archive-bolinfest
This commit is contained in:
+273
-105
File diff suppressed because one or more lines are too long
+89
-43
@@ -5079,6 +5079,7 @@ mod tests {
|
||||
use crate::exec::ExecToolCallOutput;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::mcp_connection_manager::ToolInfo;
|
||||
use crate::models_manager::model_info;
|
||||
use crate::shell::default_user_shell;
|
||||
use crate::tools::format_exec_output_str;
|
||||
|
||||
@@ -5112,6 +5113,7 @@ mod tests {
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
@@ -5184,33 +5186,24 @@ mod tests {
|
||||
async fn get_base_instructions_no_user_content() {
|
||||
let prompt_with_apply_patch_instructions =
|
||||
include_str!("../prompt_with_apply_patch_instructions.md");
|
||||
let models_response: ModelsResponse =
|
||||
serde_json::from_str(include_str!("../models.json")).expect("valid models.json");
|
||||
let model_info_for_slug = |slug: &str, config: &Config| {
|
||||
let model = models_response
|
||||
.models
|
||||
.iter()
|
||||
.find(|candidate| candidate.slug == slug)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| panic!("model slug {slug} is missing from models.json"));
|
||||
model_info::with_config_overrides(model, config)
|
||||
};
|
||||
let test_cases = vec![
|
||||
InstructionsTestCase {
|
||||
slug: "gpt-3.5",
|
||||
expects_apply_patch_instructions: true,
|
||||
},
|
||||
InstructionsTestCase {
|
||||
slug: "gpt-4.1",
|
||||
expects_apply_patch_instructions: true,
|
||||
},
|
||||
InstructionsTestCase {
|
||||
slug: "gpt-4o",
|
||||
expects_apply_patch_instructions: true,
|
||||
},
|
||||
InstructionsTestCase {
|
||||
slug: "gpt-5",
|
||||
expects_apply_patch_instructions: true,
|
||||
},
|
||||
InstructionsTestCase {
|
||||
slug: "gpt-5.1",
|
||||
expects_apply_patch_instructions: false,
|
||||
},
|
||||
InstructionsTestCase {
|
||||
slug: "codex-mini-latest",
|
||||
expects_apply_patch_instructions: true,
|
||||
},
|
||||
InstructionsTestCase {
|
||||
slug: "gpt-oss:120b",
|
||||
slug: "gpt-5.1",
|
||||
expects_apply_patch_instructions: false,
|
||||
},
|
||||
InstructionsTestCase {
|
||||
@@ -5227,7 +5220,7 @@ mod tests {
|
||||
|
||||
for test_case in test_cases {
|
||||
let config = test_config();
|
||||
let model_info = ModelsManager::construct_model_info_offline(test_case.slug, &config);
|
||||
let model_info = model_info_for_slug(test_case.slug, &config);
|
||||
if test_case.expects_apply_patch_instructions {
|
||||
assert_eq!(
|
||||
model_info.base_instructions.as_str(),
|
||||
@@ -5375,8 +5368,9 @@ mod tests {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let (rollout_items, expected) = sample_rollout(&session, &turn_context).await;
|
||||
|
||||
let reconstruction_turn = session.new_default_turn().await;
|
||||
let reconstructed = session
|
||||
.reconstruct_history_from_rollout(&turn_context, &rollout_items)
|
||||
.reconstruct_history_from_rollout(reconstruction_turn.as_ref(), &rollout_items)
|
||||
.await;
|
||||
|
||||
assert_eq!(expected, reconstructed);
|
||||
@@ -5587,7 +5581,12 @@ mod tests {
|
||||
.record_initial_history(InitialHistory::Forked(rollout_items))
|
||||
.await;
|
||||
|
||||
expected.extend(session.build_initial_context(&turn_context).await);
|
||||
let reconstruction_turn = session.new_default_turn().await;
|
||||
expected.extend(
|
||||
session
|
||||
.build_initial_context(reconstruction_turn.as_ref())
|
||||
.await,
|
||||
);
|
||||
let history = session.state.lock().await.clone_history();
|
||||
assert_eq!(expected, history.raw_items());
|
||||
}
|
||||
@@ -6114,6 +6113,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// todo: use online model info
|
||||
pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let (tx_event, _rx_event) = async_channel::unbounded();
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
@@ -6805,16 +6805,50 @@ mod tests {
|
||||
|
||||
async fn sample_rollout(
|
||||
session: &Session,
|
||||
turn_context: &TurnContext,
|
||||
_turn_context: &TurnContext,
|
||||
) -> (Vec<RolloutItem>, Vec<ResponseItem>) {
|
||||
let mut rollout_items = Vec::new();
|
||||
let mut live_history = ContextManager::new();
|
||||
|
||||
let initial_context = session.build_initial_context(turn_context).await;
|
||||
// Use the same turn_context source as record_initial_history so model_info (and thus
|
||||
// personality_spec) matches reconstruction.
|
||||
let reconstruction_turn = session.new_default_turn().await;
|
||||
let mut initial_context = session
|
||||
.build_initial_context(reconstruction_turn.as_ref())
|
||||
.await;
|
||||
// Ensure personality_spec is present when Personality is enabled, so expected matches
|
||||
// what reconstruction produces (build_initial_context may omit it when baked into model).
|
||||
if !initial_context.iter().any(|m| {
|
||||
matches!(m, ResponseItem::Message { role, content, .. }
|
||||
if role == "developer"
|
||||
&& content.iter().any(|c| {
|
||||
matches!(c, ContentItem::InputText { text } if text.contains("<personality_spec>"))
|
||||
}))
|
||||
})
|
||||
&& let Some(p) = reconstruction_turn.personality
|
||||
&& session.features.enabled(Feature::Personality)
|
||||
&& let Some(personality_message) = reconstruction_turn
|
||||
.model_info
|
||||
.model_messages
|
||||
.as_ref()
|
||||
.and_then(|m| m.get_personality_message(Some(p)).filter(|s| !s.is_empty()))
|
||||
{
|
||||
let msg =
|
||||
DeveloperInstructions::personality_spec_message(personality_message).into();
|
||||
let insert_at = initial_context
|
||||
.iter()
|
||||
.position(|m| matches!(m, ResponseItem::Message { role, .. } if role == "developer"))
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0);
|
||||
initial_context.insert(insert_at, msg);
|
||||
}
|
||||
for item in &initial_context {
|
||||
rollout_items.push(RolloutItem::ResponseItem(item.clone()));
|
||||
}
|
||||
live_history.record_items(initial_context.iter(), turn_context.truncation_policy);
|
||||
live_history.record_items(
|
||||
initial_context.iter(),
|
||||
reconstruction_turn.truncation_policy,
|
||||
);
|
||||
|
||||
let user1 = ResponseItem::Message {
|
||||
id: None,
|
||||
@@ -6825,7 +6859,10 @@ mod tests {
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
live_history.record_items(std::iter::once(&user1), turn_context.truncation_policy);
|
||||
live_history.record_items(
|
||||
std::iter::once(&user1),
|
||||
reconstruction_turn.truncation_policy,
|
||||
);
|
||||
rollout_items.push(RolloutItem::ResponseItem(user1.clone()));
|
||||
|
||||
let assistant1 = ResponseItem::Message {
|
||||
@@ -6837,17 +6874,17 @@ mod tests {
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
live_history.record_items(std::iter::once(&assistant1), turn_context.truncation_policy);
|
||||
live_history.record_items(
|
||||
std::iter::once(&assistant1),
|
||||
reconstruction_turn.truncation_policy,
|
||||
);
|
||||
rollout_items.push(RolloutItem::ResponseItem(assistant1.clone()));
|
||||
|
||||
let summary1 = "summary one";
|
||||
let snapshot1 = live_history.clone().for_prompt();
|
||||
let user_messages1 = collect_user_messages(&snapshot1);
|
||||
let rebuilt1 = compact::build_compacted_history(
|
||||
session.build_initial_context(turn_context).await,
|
||||
&user_messages1,
|
||||
summary1,
|
||||
);
|
||||
let rebuilt1 =
|
||||
compact::build_compacted_history(initial_context.clone(), &user_messages1, summary1);
|
||||
live_history.replace(rebuilt1);
|
||||
rollout_items.push(RolloutItem::Compacted(CompactedItem {
|
||||
message: summary1.to_string(),
|
||||
@@ -6863,7 +6900,10 @@ mod tests {
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
live_history.record_items(std::iter::once(&user2), turn_context.truncation_policy);
|
||||
live_history.record_items(
|
||||
std::iter::once(&user2),
|
||||
reconstruction_turn.truncation_policy,
|
||||
);
|
||||
rollout_items.push(RolloutItem::ResponseItem(user2.clone()));
|
||||
|
||||
let assistant2 = ResponseItem::Message {
|
||||
@@ -6875,17 +6915,17 @@ mod tests {
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
live_history.record_items(std::iter::once(&assistant2), turn_context.truncation_policy);
|
||||
live_history.record_items(
|
||||
std::iter::once(&assistant2),
|
||||
reconstruction_turn.truncation_policy,
|
||||
);
|
||||
rollout_items.push(RolloutItem::ResponseItem(assistant2.clone()));
|
||||
|
||||
let summary2 = "summary two";
|
||||
let snapshot2 = live_history.clone().for_prompt();
|
||||
let user_messages2 = collect_user_messages(&snapshot2);
|
||||
let rebuilt2 = compact::build_compacted_history(
|
||||
session.build_initial_context(turn_context).await,
|
||||
&user_messages2,
|
||||
summary2,
|
||||
);
|
||||
let rebuilt2 =
|
||||
compact::build_compacted_history(initial_context.clone(), &user_messages2, summary2);
|
||||
live_history.replace(rebuilt2);
|
||||
rollout_items.push(RolloutItem::Compacted(CompactedItem {
|
||||
message: summary2.to_string(),
|
||||
@@ -6901,7 +6941,10 @@ mod tests {
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
live_history.record_items(std::iter::once(&user3), turn_context.truncation_policy);
|
||||
live_history.record_items(
|
||||
std::iter::once(&user3),
|
||||
reconstruction_turn.truncation_policy,
|
||||
);
|
||||
rollout_items.push(RolloutItem::ResponseItem(user3));
|
||||
|
||||
let assistant3 = ResponseItem::Message {
|
||||
@@ -6913,7 +6956,10 @@ mod tests {
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
live_history.record_items(std::iter::once(&assistant3), turn_context.truncation_policy);
|
||||
live_history.record_items(
|
||||
std::iter::once(&assistant3),
|
||||
reconstruction_turn.truncation_policy,
|
||||
);
|
||||
rollout_items.push(RolloutItem::ResponseItem(assistant3));
|
||||
|
||||
(rollout_items, live_history.for_prompt())
|
||||
|
||||
@@ -498,7 +498,7 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
FeatureSpec {
|
||||
id: Feature::RemoteModels,
|
||||
key: "remote_models",
|
||||
stage: Stage::UnderDevelopment,
|
||||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
@@ -633,3 +633,23 @@ pub fn maybe_push_unstable_features_warning(
|
||||
msg: EventMsg::Warning(WarningEvent { message }),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn under_development_features_are_disabled_by_default() {
|
||||
for spec in FEATURES {
|
||||
if matches!(spec.stage, Stage::UnderDevelopment) {
|
||||
assert_eq!(
|
||||
spec.default_enabled, false,
|
||||
"feature `{}` is under development and must be disabled by default",
|
||||
spec.key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,18 +137,38 @@ impl ModelsManager {
|
||||
/// Look up model metadata, applying remote overrides and config adjustments.
|
||||
pub async fn get_model_info(&self, model: &str, config: &Config) -> ModelInfo {
|
||||
let remote = self
|
||||
.get_remote_models(config)
|
||||
.await
|
||||
.into_iter()
|
||||
.find(|m| m.slug == model);
|
||||
.find_remote_model_by_longest_prefix(model, config)
|
||||
.await;
|
||||
let model = if let Some(remote) = remote {
|
||||
remote
|
||||
} else {
|
||||
model_info::find_model_info_for_slug(model)
|
||||
model_info::model_info_from_slug(model)
|
||||
};
|
||||
model_info::with_config_overrides(model, config)
|
||||
}
|
||||
|
||||
async fn find_remote_model_by_longest_prefix(
|
||||
&self,
|
||||
model: &str,
|
||||
config: &Config,
|
||||
) -> Option<ModelInfo> {
|
||||
let mut best: Option<ModelInfo> = None;
|
||||
for candidate in self.get_remote_models(config).await {
|
||||
if !model.starts_with(&candidate.slug) {
|
||||
continue;
|
||||
}
|
||||
let is_better_match = if let Some(current) = best.as_ref() {
|
||||
candidate.slug.len() > current.slug.len()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if is_better_match {
|
||||
best = Some(candidate);
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// Refresh models if the provided ETag differs from the cached ETag.
|
||||
///
|
||||
/// Uses `Online` strategy to fetch latest models when ETags differ.
|
||||
@@ -346,7 +366,7 @@ impl ModelsManager {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
/// Build `ModelInfo` without consulting remote state or cache.
|
||||
pub fn construct_model_info_offline(model: &str, config: &Config) -> ModelInfo {
|
||||
model_info::with_config_overrides(model_info::find_model_info_for_slug(model), config)
|
||||
model_info::with_config_overrides(model_info::model_info_from_slug(model), config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::openai_models::ApplyPatchToolType;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelInstructionsVariables;
|
||||
use codex_protocol::openai_models::ModelMessages;
|
||||
use codex_protocol::openai_models::ModelVisibility;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_protocol::openai_models::TruncationMode;
|
||||
use codex_protocol::openai_models::TruncationPolicyConfig;
|
||||
use codex_protocol::openai_models::default_input_modalities;
|
||||
@@ -17,65 +13,11 @@ use crate::truncate::approx_bytes_for_tokens;
|
||||
use tracing::warn;
|
||||
|
||||
pub const BASE_INSTRUCTIONS: &str = include_str!("../../prompt.md");
|
||||
const BASE_INSTRUCTIONS_WITH_APPLY_PATCH: &str =
|
||||
include_str!("../../prompt_with_apply_patch_instructions.md");
|
||||
|
||||
const GPT_5_CODEX_INSTRUCTIONS: &str = include_str!("../../gpt_5_codex_prompt.md");
|
||||
const GPT_5_1_INSTRUCTIONS: &str = include_str!("../../gpt_5_1_prompt.md");
|
||||
const GPT_5_2_INSTRUCTIONS: &str = include_str!("../../gpt_5_2_prompt.md");
|
||||
const GPT_5_1_CODEX_MAX_INSTRUCTIONS: &str = include_str!("../../gpt-5.1-codex-max_prompt.md");
|
||||
|
||||
const GPT_5_2_CODEX_INSTRUCTIONS: &str = include_str!("../../gpt-5.2-codex_prompt.md");
|
||||
const GPT_5_2_CODEX_INSTRUCTIONS_TEMPLATE: &str =
|
||||
include_str!("../../templates/model_instructions/gpt-5.2-codex_instructions_template.md");
|
||||
|
||||
const GPT_5_2_CODEX_PERSONALITY_FRIENDLY: &str =
|
||||
include_str!("../../templates/personalities/gpt-5.2-codex_friendly.md");
|
||||
const GPT_5_2_CODEX_PERSONALITY_PRAGMATIC: &str =
|
||||
include_str!("../../templates/personalities/gpt-5.2-codex_pragmatic.md");
|
||||
|
||||
pub(crate) const CONTEXT_WINDOW_272K: i64 = 272_000;
|
||||
|
||||
macro_rules! model_info {
|
||||
(
|
||||
$slug:expr $(, $key:ident : $value:expr )* $(,)?
|
||||
) => {{
|
||||
#[allow(unused_mut)]
|
||||
let mut model = ModelInfo {
|
||||
slug: $slug.to_string(),
|
||||
display_name: $slug.to_string(),
|
||||
description: None,
|
||||
// This is primarily used when remote metadata is available. When running
|
||||
// offline, core generally omits the effort field unless explicitly
|
||||
// configured by the user.
|
||||
default_reasoning_level: None,
|
||||
supported_reasoning_levels: supported_reasoning_level_low_medium_high(),
|
||||
shell_type: ConfigShellToolType::Default,
|
||||
visibility: ModelVisibility::None,
|
||||
supported_in_api: true,
|
||||
priority: 99,
|
||||
upgrade: None,
|
||||
base_instructions: BASE_INSTRUCTIONS.to_string(),
|
||||
model_messages: None,
|
||||
supports_reasoning_summaries: false,
|
||||
support_verbosity: false,
|
||||
default_verbosity: None,
|
||||
apply_patch_tool_type: None,
|
||||
truncation_policy: TruncationPolicyConfig::bytes(10_000),
|
||||
supports_parallel_tool_calls: false,
|
||||
context_window: Some(CONTEXT_WINDOW_272K),
|
||||
auto_compact_token_limit: None,
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
input_modalities: default_input_modalities(),
|
||||
};
|
||||
|
||||
$(
|
||||
model.$key = $value;
|
||||
)*
|
||||
model
|
||||
}};
|
||||
}
|
||||
const DEFAULT_PERSONALITY_HEADER: &str = "You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals.";
|
||||
const LOCAL_FRIENDLY_TEMPLATE: &str =
|
||||
"You optimize for team morale and being a supportive teammate as much as code quality.";
|
||||
const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer.";
|
||||
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
|
||||
|
||||
pub(crate) fn with_config_overrides(mut model: ModelInfo, config: &Config) -> ModelInfo {
|
||||
if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries {
|
||||
@@ -111,290 +53,48 @@ pub(crate) fn with_config_overrides(mut model: ModelInfo, config: &Config) -> Mo
|
||||
model
|
||||
}
|
||||
|
||||
// todo(aibrahim): remove most of the entries here when enabling models.json
|
||||
pub(crate) fn find_model_info_for_slug(slug: &str) -> ModelInfo {
|
||||
if slug.starts_with("o3") || slug.starts_with("o4-mini") {
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(),
|
||||
supports_reasoning_summaries: true,
|
||||
context_window: Some(200_000),
|
||||
)
|
||||
} else if slug.starts_with("codex-mini-latest") {
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(),
|
||||
shell_type: ConfigShellToolType::Local,
|
||||
supports_reasoning_summaries: true,
|
||||
context_window: Some(200_000),
|
||||
)
|
||||
} else if slug.starts_with("gpt-4.1") {
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(),
|
||||
supports_reasoning_summaries: false,
|
||||
context_window: Some(1_047_576),
|
||||
)
|
||||
} else if slug.starts_with("gpt-oss") || slug.starts_with("openai/gpt-oss") {
|
||||
model_info!(
|
||||
slug,
|
||||
apply_patch_tool_type: Some(ApplyPatchToolType::Function),
|
||||
context_window: Some(96_000),
|
||||
)
|
||||
} else if slug.starts_with("gpt-4o") {
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(),
|
||||
supports_reasoning_summaries: false,
|
||||
context_window: Some(128_000),
|
||||
)
|
||||
} else if slug.starts_with("gpt-3.5") {
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(),
|
||||
supports_reasoning_summaries: false,
|
||||
context_window: Some(16_385),
|
||||
)
|
||||
} else if slug.starts_with("test-gpt-5") {
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(),
|
||||
experimental_supported_tools: vec![
|
||||
"grep_files".to_string(),
|
||||
"list_dir".to_string(),
|
||||
"read_file".to_string(),
|
||||
"test_sync_tool".to_string(),
|
||||
],
|
||||
supports_parallel_tool_calls: true,
|
||||
supports_reasoning_summaries: true,
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
support_verbosity: true,
|
||||
truncation_policy: TruncationPolicyConfig::tokens(10_000),
|
||||
)
|
||||
} else if slug.starts_with("exp-codex") || slug.starts_with("codex-1p") {
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(),
|
||||
model_messages: Some(ModelMessages {
|
||||
instructions_template: Some(GPT_5_2_CODEX_INSTRUCTIONS_TEMPLATE.to_string()),
|
||||
instructions_variables: Some(ModelInstructionsVariables {
|
||||
personality_default: Some("".to_string()),
|
||||
personality_friendly: Some(GPT_5_2_CODEX_PERSONALITY_FRIENDLY.to_string()),
|
||||
personality_pragmatic: Some(GPT_5_2_CODEX_PERSONALITY_PRAGMATIC.to_string()),
|
||||
}),
|
||||
}),
|
||||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
supports_parallel_tool_calls: true,
|
||||
supports_reasoning_summaries: true,
|
||||
support_verbosity: false,
|
||||
truncation_policy: TruncationPolicyConfig::tokens(10_000),
|
||||
context_window: Some(CONTEXT_WINDOW_272K),
|
||||
)
|
||||
} else if slug.starts_with("exp-") {
|
||||
model_info!(
|
||||
slug,
|
||||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||||
supports_reasoning_summaries: true,
|
||||
support_verbosity: true,
|
||||
default_verbosity: Some(Verbosity::Low),
|
||||
base_instructions: BASE_INSTRUCTIONS.to_string(),
|
||||
default_reasoning_level: Some(ReasoningEffort::Medium),
|
||||
truncation_policy: TruncationPolicyConfig::bytes(10_000),
|
||||
shell_type: ConfigShellToolType::UnifiedExec,
|
||||
supports_parallel_tool_calls: true,
|
||||
context_window: Some(CONTEXT_WINDOW_272K),
|
||||
)
|
||||
} else if slug.starts_with("gpt-5.2-codex") || slug.starts_with("bengalfox") {
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(),
|
||||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
supports_parallel_tool_calls: true,
|
||||
supports_reasoning_summaries: true,
|
||||
support_verbosity: false,
|
||||
truncation_policy: TruncationPolicyConfig::tokens(10_000),
|
||||
context_window: Some(CONTEXT_WINDOW_272K),
|
||||
supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh(),
|
||||
base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(),
|
||||
model_messages: Some(ModelMessages {
|
||||
instructions_template: Some(GPT_5_2_CODEX_INSTRUCTIONS_TEMPLATE.to_string()),
|
||||
instructions_variables: Some(ModelInstructionsVariables {
|
||||
personality_default: Some("".to_string()),
|
||||
personality_friendly: Some(GPT_5_2_CODEX_PERSONALITY_FRIENDLY.to_string()),
|
||||
personality_pragmatic: Some(GPT_5_2_CODEX_PERSONALITY_PRAGMATIC.to_string()),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
} else if slug.starts_with("gpt-5.1-codex-max") {
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: GPT_5_1_CODEX_MAX_INSTRUCTIONS.to_string(),
|
||||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
supports_parallel_tool_calls: false,
|
||||
supports_reasoning_summaries: true,
|
||||
support_verbosity: false,
|
||||
truncation_policy: TruncationPolicyConfig::tokens(10_000),
|
||||
context_window: Some(CONTEXT_WINDOW_272K),
|
||||
supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh(),
|
||||
)
|
||||
} else if (slug.starts_with("gpt-5-codex")
|
||||
|| slug.starts_with("gpt-5.1-codex")
|
||||
|| slug.starts_with("codex-"))
|
||||
&& !slug.contains("-mini")
|
||||
{
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(),
|
||||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
supports_parallel_tool_calls: false,
|
||||
supports_reasoning_summaries: true,
|
||||
support_verbosity: false,
|
||||
truncation_policy: TruncationPolicyConfig::tokens(10_000),
|
||||
context_window: Some(CONTEXT_WINDOW_272K),
|
||||
supported_reasoning_levels: supported_reasoning_level_low_medium_high(),
|
||||
)
|
||||
} else if slug.starts_with("gpt-5-codex")
|
||||
|| slug.starts_with("gpt-5.1-codex")
|
||||
|| slug.starts_with("codex-")
|
||||
{
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(),
|
||||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
supports_parallel_tool_calls: false,
|
||||
supports_reasoning_summaries: true,
|
||||
support_verbosity: false,
|
||||
truncation_policy: TruncationPolicyConfig::tokens(10_000),
|
||||
context_window: Some(CONTEXT_WINDOW_272K),
|
||||
)
|
||||
} else if slug.starts_with("gpt-5.2") || slug.starts_with("boomslang") {
|
||||
model_info!(
|
||||
slug,
|
||||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||||
supports_reasoning_summaries: true,
|
||||
support_verbosity: true,
|
||||
default_verbosity: Some(Verbosity::Low),
|
||||
base_instructions: GPT_5_2_INSTRUCTIONS.to_string(),
|
||||
default_reasoning_level: Some(ReasoningEffort::Medium),
|
||||
truncation_policy: TruncationPolicyConfig::bytes(10_000),
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
supports_parallel_tool_calls: true,
|
||||
context_window: Some(CONTEXT_WINDOW_272K),
|
||||
supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh_non_codex(),
|
||||
)
|
||||
} else if slug.starts_with("gpt-5.1") {
|
||||
model_info!(
|
||||
slug,
|
||||
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
|
||||
supports_reasoning_summaries: true,
|
||||
support_verbosity: true,
|
||||
default_verbosity: Some(Verbosity::Low),
|
||||
base_instructions: GPT_5_1_INSTRUCTIONS.to_string(),
|
||||
default_reasoning_level: Some(ReasoningEffort::Medium),
|
||||
truncation_policy: TruncationPolicyConfig::bytes(10_000),
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
supports_parallel_tool_calls: true,
|
||||
context_window: Some(CONTEXT_WINDOW_272K),
|
||||
supported_reasoning_levels: supported_reasoning_level_low_medium_high_non_codex(),
|
||||
)
|
||||
} else if slug.starts_with("gpt-5") {
|
||||
model_info!(
|
||||
slug,
|
||||
base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(),
|
||||
shell_type: ConfigShellToolType::Default,
|
||||
supports_reasoning_summaries: true,
|
||||
support_verbosity: true,
|
||||
truncation_policy: TruncationPolicyConfig::bytes(10_000),
|
||||
context_window: Some(CONTEXT_WINDOW_272K),
|
||||
)
|
||||
} else {
|
||||
warn!("Unknown model {slug} is used. This will degrade the performance of Codex.");
|
||||
model_info!(
|
||||
slug,
|
||||
context_window: None,
|
||||
supported_reasoning_levels: Vec::new(),
|
||||
default_reasoning_level: None
|
||||
)
|
||||
/// Build a minimal fallback model descriptor for missing/unknown slugs.
|
||||
pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo {
|
||||
warn!("Unknown model {slug} is used. This will use fallback model metadata.");
|
||||
ModelInfo {
|
||||
slug: slug.to_string(),
|
||||
display_name: slug.to_string(),
|
||||
description: None,
|
||||
default_reasoning_level: None,
|
||||
supported_reasoning_levels: Vec::new(),
|
||||
shell_type: ConfigShellToolType::Default,
|
||||
visibility: ModelVisibility::None,
|
||||
supported_in_api: true,
|
||||
priority: 99,
|
||||
upgrade: None,
|
||||
base_instructions: BASE_INSTRUCTIONS.to_string(),
|
||||
model_messages: local_personality_messages_for_slug(slug),
|
||||
supports_reasoning_summaries: false,
|
||||
support_verbosity: false,
|
||||
default_verbosity: None,
|
||||
apply_patch_tool_type: None,
|
||||
truncation_policy: TruncationPolicyConfig::bytes(10_000),
|
||||
supports_parallel_tool_calls: false,
|
||||
context_window: Some(272_000),
|
||||
auto_compact_token_limit: None,
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_reasoning_level_low_medium_high() -> Vec<ReasoningEffortPreset> {
|
||||
vec![
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Low,
|
||||
description: "Fast responses with lighter reasoning".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Medium,
|
||||
description: "Balances speed and reasoning depth for everyday tasks".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::High,
|
||||
description: "Greater reasoning depth for complex problems".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn supported_reasoning_level_low_medium_high_non_codex() -> Vec<ReasoningEffortPreset> {
|
||||
vec![
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Low,
|
||||
description: "Balances speed with some reasoning; useful for straightforward queries and short explanations".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Medium,
|
||||
description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::High,
|
||||
description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn supported_reasoning_level_low_medium_high_xhigh() -> Vec<ReasoningEffortPreset> {
|
||||
vec![
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Low,
|
||||
description: "Fast responses with lighter reasoning".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Medium,
|
||||
description: "Balances speed and reasoning depth for everyday tasks".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::High,
|
||||
description: "Greater reasoning depth for complex problems".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::XHigh,
|
||||
description: "Extra high reasoning depth for complex problems".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn supported_reasoning_level_low_medium_high_xhigh_non_codex() -> Vec<ReasoningEffortPreset> {
|
||||
vec![
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Low,
|
||||
description: "Balances speed with some reasoning; useful for straightforward queries and short explanations".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Medium,
|
||||
description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::High,
|
||||
description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::XHigh,
|
||||
description: "Extra high reasoning for complex problems".to_string(),
|
||||
},
|
||||
]
|
||||
fn local_personality_messages_for_slug(slug: &str) -> Option<ModelMessages> {
|
||||
match slug {
|
||||
"gpt-5.2-codex" | "exp-codex-personality" => Some(ModelMessages {
|
||||
instructions_template: Some(format!(
|
||||
"{DEFAULT_PERSONALITY_HEADER}\n\n{PERSONALITY_PLACEHOLDER}\n\n{BASE_INSTRUCTIONS}"
|
||||
)),
|
||||
instructions_variables: Some(ModelInstructionsVariables {
|
||||
personality_default: Some(String::new()),
|
||||
personality_friendly: Some(LOCAL_FRIENDLY_TEMPLATE.to_string()),
|
||||
personality_pragmatic: Some(LOCAL_PRAGMATIC_TEMPLATE.to_string()),
|
||||
}),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1499,7 +1499,10 @@ mod tests {
|
||||
use crate::client_common::tools::FreeformTool;
|
||||
use crate::config::test_config;
|
||||
use crate::models_manager::manager::ModelsManager;
|
||||
use crate::models_manager::model_info::with_config_overrides;
|
||||
use crate::tools::registry::ConfiguredToolSpec;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
@@ -1630,10 +1633,21 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn model_info_from_models_json(slug: &str) -> ModelInfo {
|
||||
let config = test_config();
|
||||
let response: ModelsResponse =
|
||||
serde_json::from_str(include_str!("../../models.json")).expect("valid models.json");
|
||||
let model = response
|
||||
.models
|
||||
.into_iter()
|
||||
.find(|candidate| candidate.slug == slug)
|
||||
.unwrap_or_else(|| panic!("model slug {slug} is missing from models.json"));
|
||||
with_config_overrides(model, &config)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
||||
let config = test_config();
|
||||
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
|
||||
let model_info = model_info_from_models_json("gpt-5-codex");
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
features.enable(Feature::CollaborationModes);
|
||||
@@ -1752,8 +1766,7 @@ mod tests {
|
||||
web_search_mode: Option<WebSearchMode>,
|
||||
expected_tools: &[&str],
|
||||
) {
|
||||
let config = test_config();
|
||||
let model_info = ModelsManager::construct_model_info_offline(model_slug, &config);
|
||||
let model_info = model_info_from_models_json(model_slug);
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
features,
|
||||
@@ -1917,20 +1930,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codex_mini_defaults() {
|
||||
fn test_gpt_5_1_codex_max_defaults() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_default_model_tools(
|
||||
"codex-mini-latest",
|
||||
"gpt-5.1-codex-max",
|
||||
&features,
|
||||
Some(WebSearchMode::Cached),
|
||||
"local_shell",
|
||||
"shell_command",
|
||||
&[
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
],
|
||||
@@ -2026,35 +2040,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exp_5_1_defaults() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"exp-5.1",
|
||||
&features,
|
||||
Some(WebSearchMode::Cached),
|
||||
&[
|
||||
"exec_command",
|
||||
"write_stdin",
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codex_mini_unified_exec_web_search() {
|
||||
fn test_gpt_5_1_codex_max_unified_exec_web_search() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"codex-mini-latest",
|
||||
"gpt-5.1-codex-max",
|
||||
&features,
|
||||
Some(WebSearchMode::Live),
|
||||
&[
|
||||
@@ -2065,6 +2056,7 @@ mod tests {
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
],
|
||||
@@ -2115,8 +2107,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_test_model_info_includes_sync_tool() {
|
||||
let config = test_config();
|
||||
let model_info = ModelsManager::construct_model_info_offline("test-gpt-5-codex", &config);
|
||||
let mut model_info = model_info_from_models_json("gpt-5-codex");
|
||||
model_info.experimental_supported_tools = vec![
|
||||
"test_sync_tool".to_string(),
|
||||
"read_file".to_string(),
|
||||
"grep_files".to_string(),
|
||||
"list_dir".to_string(),
|
||||
];
|
||||
let features = Features::with_defaults();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
|
||||
@@ -3,15 +3,8 @@ use codex_core::CodexAuth;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::built_in_model_providers;
|
||||
use codex_core::models_manager::manager::RefreshStrategy;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_protocol::openai_models::default_input_modalities;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -26,8 +19,8 @@ async fn list_models_returns_api_key_models() -> Result<()> {
|
||||
.list_models(&config, RefreshStrategy::OnlineIfUncached)
|
||||
.await;
|
||||
|
||||
let expected_models = expected_models_for_api_key();
|
||||
assert_eq!(expected_models, models);
|
||||
let slugs: Vec<String> = models.into_iter().map(|m| m.id).collect();
|
||||
assert_eq!(expected_slugs(), slugs);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -44,486 +37,24 @@ async fn list_models_returns_chatgpt_models() -> Result<()> {
|
||||
.list_models(&config, RefreshStrategy::OnlineIfUncached)
|
||||
.await;
|
||||
|
||||
let expected_models = expected_models_for_chatgpt();
|
||||
assert_eq!(expected_models, models);
|
||||
let slugs: Vec<String> = models.into_iter().map(|m| m.id).collect();
|
||||
assert_eq!(expected_slugs(), slugs);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expected_models_for_api_key() -> Vec<ModelPreset> {
|
||||
fn expected_slugs() -> Vec<String> {
|
||||
vec![
|
||||
gpt_52_codex(),
|
||||
gpt_5_2(),
|
||||
gpt_5_1_codex_max(),
|
||||
gpt_5_1_codex(),
|
||||
gpt_5_1_codex_mini(),
|
||||
gpt_5_1(),
|
||||
gpt_5_codex(),
|
||||
gpt_5(),
|
||||
gpt_5_codex_mini(),
|
||||
bengalfox(),
|
||||
boomslang(),
|
||||
"gpt-5.2-codex".into(),
|
||||
"gpt-5.1-codex-max".into(),
|
||||
"gpt-5.1-codex".into(),
|
||||
"gpt-5.2".into(),
|
||||
"gpt-5.1".into(),
|
||||
"gpt-5-codex".into(),
|
||||
"gpt-5".into(),
|
||||
"gpt-5.1-codex-mini".into(),
|
||||
"gpt-5-codex-mini".into(),
|
||||
"bengalfox".into(),
|
||||
"boomslang".into(),
|
||||
]
|
||||
}
|
||||
|
||||
fn expected_models_for_chatgpt() -> Vec<ModelPreset> {
|
||||
expected_models_for_api_key()
|
||||
}
|
||||
|
||||
fn gpt_52_codex() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "gpt-5.2-codex".to_string(),
|
||||
model: "gpt-5.2-codex".to_string(),
|
||||
display_name: "gpt-5.2-codex".to_string(),
|
||||
description: "Latest frontier agentic coding model.".to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Low,
|
||||
"Fast responses with lighter reasoning",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Balances speed and reasoning depth for everyday tasks",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Greater reasoning depth for complex problems",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::XHigh,
|
||||
"Extra high reasoning depth for complex problems",
|
||||
),
|
||||
],
|
||||
supports_personality: false,
|
||||
is_default: true,
|
||||
upgrade: None,
|
||||
show_in_picker: true,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn gpt_5_1_codex_max() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "gpt-5.1-codex-max".to_string(),
|
||||
model: "gpt-5.1-codex-max".to_string(),
|
||||
display_name: "gpt-5.1-codex-max".to_string(),
|
||||
description: "Codex-optimized flagship for deep and fast reasoning.".to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Low,
|
||||
"Fast responses with lighter reasoning",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Balances speed and reasoning depth for everyday tasks",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Greater reasoning depth for complex problems",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::XHigh,
|
||||
"Extra high reasoning depth for complex problems",
|
||||
),
|
||||
],
|
||||
supports_personality: false,
|
||||
is_default: false,
|
||||
upgrade: Some(gpt52_codex_upgrade(
|
||||
"gpt-5.1-codex-max",
|
||||
HashMap::from([
|
||||
(ReasoningEffort::Low, ReasoningEffort::Low),
|
||||
(ReasoningEffort::None, ReasoningEffort::Low),
|
||||
(ReasoningEffort::Medium, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::High, ReasoningEffort::High),
|
||||
(ReasoningEffort::Minimal, ReasoningEffort::Low),
|
||||
(ReasoningEffort::XHigh, ReasoningEffort::XHigh),
|
||||
]),
|
||||
)),
|
||||
show_in_picker: true,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn gpt_5_1_codex_mini() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "gpt-5.1-codex-mini".to_string(),
|
||||
model: "gpt-5.1-codex-mini".to_string(),
|
||||
display_name: "gpt-5.1-codex-mini".to_string(),
|
||||
description: "Optimized for codex. Cheaper, faster, but less capable.".to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Dynamically adjusts reasoning based on the task",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Maximizes reasoning depth for complex or ambiguous problems",
|
||||
),
|
||||
],
|
||||
supports_personality: false,
|
||||
is_default: false,
|
||||
upgrade: Some(gpt52_codex_upgrade(
|
||||
"gpt-5.1-codex-mini",
|
||||
HashMap::from([
|
||||
(ReasoningEffort::High, ReasoningEffort::High),
|
||||
(ReasoningEffort::XHigh, ReasoningEffort::High),
|
||||
(ReasoningEffort::Minimal, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::None, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::Low, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::Medium, ReasoningEffort::Medium),
|
||||
]),
|
||||
)),
|
||||
show_in_picker: true,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn gpt_5_2() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "gpt-5.2".to_string(),
|
||||
model: "gpt-5.2".to_string(),
|
||||
display_name: "gpt-5.2".to_string(),
|
||||
description:
|
||||
"Latest frontier model with improvements across knowledge, reasoning and coding"
|
||||
.to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Low,
|
||||
"Balances speed with some reasoning; useful for straightforward queries and short explanations",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Provides a solid balance of reasoning depth and latency for general-purpose tasks",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Maximizes reasoning depth for complex or ambiguous problems",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::XHigh,
|
||||
"Extra high reasoning for complex problems",
|
||||
),
|
||||
],
|
||||
supports_personality: false,
|
||||
is_default: false,
|
||||
upgrade: Some(gpt52_codex_upgrade(
|
||||
"gpt-5.2",
|
||||
HashMap::from([
|
||||
(ReasoningEffort::High, ReasoningEffort::High),
|
||||
(ReasoningEffort::None, ReasoningEffort::Low),
|
||||
(ReasoningEffort::Minimal, ReasoningEffort::Low),
|
||||
(ReasoningEffort::Low, ReasoningEffort::Low),
|
||||
(ReasoningEffort::Medium, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::XHigh, ReasoningEffort::XHigh),
|
||||
]),
|
||||
)),
|
||||
show_in_picker: true,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bengalfox() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "bengalfox".to_string(),
|
||||
model: "bengalfox".to_string(),
|
||||
display_name: "bengalfox".to_string(),
|
||||
description: "bengalfox".to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Low,
|
||||
"Fast responses with lighter reasoning",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Balances speed and reasoning depth for everyday tasks",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Greater reasoning depth for complex problems",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::XHigh,
|
||||
"Extra high reasoning depth for complex problems",
|
||||
),
|
||||
],
|
||||
supports_personality: true,
|
||||
is_default: false,
|
||||
upgrade: None,
|
||||
show_in_picker: false,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn boomslang() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "boomslang".to_string(),
|
||||
model: "boomslang".to_string(),
|
||||
display_name: "boomslang".to_string(),
|
||||
description: "boomslang".to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Low,
|
||||
"Balances speed with some reasoning; useful for straightforward queries and short explanations",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Provides a solid balance of reasoning depth and latency for general-purpose tasks",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Maximizes reasoning depth for complex or ambiguous problems",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::XHigh,
|
||||
"Extra high reasoning depth for complex problems",
|
||||
),
|
||||
],
|
||||
supports_personality: false,
|
||||
is_default: false,
|
||||
upgrade: None,
|
||||
show_in_picker: false,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn gpt_5_codex() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "gpt-5-codex".to_string(),
|
||||
model: "gpt-5-codex".to_string(),
|
||||
display_name: "gpt-5-codex".to_string(),
|
||||
description: "Optimized for codex.".to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Low,
|
||||
"Fastest responses with limited reasoning",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Dynamically adjusts reasoning based on the task",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Maximizes reasoning depth for complex or ambiguous problems",
|
||||
),
|
||||
],
|
||||
supports_personality: false,
|
||||
is_default: false,
|
||||
upgrade: Some(gpt52_codex_upgrade(
|
||||
"gpt-5-codex",
|
||||
HashMap::from([
|
||||
(ReasoningEffort::Minimal, ReasoningEffort::Low),
|
||||
(ReasoningEffort::High, ReasoningEffort::High),
|
||||
(ReasoningEffort::Medium, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::XHigh, ReasoningEffort::High),
|
||||
(ReasoningEffort::None, ReasoningEffort::Low),
|
||||
(ReasoningEffort::Low, ReasoningEffort::Low),
|
||||
]),
|
||||
)),
|
||||
show_in_picker: false,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn gpt_5_codex_mini() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "gpt-5-codex-mini".to_string(),
|
||||
model: "gpt-5-codex-mini".to_string(),
|
||||
display_name: "gpt-5-codex-mini".to_string(),
|
||||
description: "Optimized for codex. Cheaper, faster, but less capable.".to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Dynamically adjusts reasoning based on the task",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Maximizes reasoning depth for complex or ambiguous problems",
|
||||
),
|
||||
],
|
||||
supports_personality: false,
|
||||
is_default: false,
|
||||
upgrade: Some(gpt52_codex_upgrade(
|
||||
"gpt-5-codex-mini",
|
||||
HashMap::from([
|
||||
(ReasoningEffort::None, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::XHigh, ReasoningEffort::High),
|
||||
(ReasoningEffort::High, ReasoningEffort::High),
|
||||
(ReasoningEffort::Low, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::Medium, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::Minimal, ReasoningEffort::Medium),
|
||||
]),
|
||||
)),
|
||||
show_in_picker: false,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn gpt_5_1_codex() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "gpt-5.1-codex".to_string(),
|
||||
model: "gpt-5.1-codex".to_string(),
|
||||
display_name: "gpt-5.1-codex".to_string(),
|
||||
description: "Optimized for codex.".to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Low,
|
||||
"Fastest responses with limited reasoning",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Dynamically adjusts reasoning based on the task",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Maximizes reasoning depth for complex or ambiguous problems",
|
||||
),
|
||||
],
|
||||
supports_personality: false,
|
||||
is_default: false,
|
||||
upgrade: Some(gpt52_codex_upgrade(
|
||||
"gpt-5.1-codex",
|
||||
HashMap::from([
|
||||
(ReasoningEffort::Minimal, ReasoningEffort::Low),
|
||||
(ReasoningEffort::Low, ReasoningEffort::Low),
|
||||
(ReasoningEffort::Medium, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::None, ReasoningEffort::Low),
|
||||
(ReasoningEffort::High, ReasoningEffort::High),
|
||||
(ReasoningEffort::XHigh, ReasoningEffort::High),
|
||||
]),
|
||||
)),
|
||||
show_in_picker: false,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn gpt_5() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "gpt-5".to_string(),
|
||||
model: "gpt-5".to_string(),
|
||||
display_name: "gpt-5".to_string(),
|
||||
description: "Broad world knowledge with strong general reasoning.".to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Minimal,
|
||||
"Fastest responses with little reasoning",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::Low,
|
||||
"Balances speed with some reasoning; useful for straightforward queries and short explanations",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Provides a solid balance of reasoning depth and latency for general-purpose tasks",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Maximizes reasoning depth for complex or ambiguous problems",
|
||||
),
|
||||
],
|
||||
supports_personality: false,
|
||||
is_default: false,
|
||||
upgrade: Some(gpt52_codex_upgrade(
|
||||
"gpt-5",
|
||||
HashMap::from([
|
||||
(ReasoningEffort::XHigh, ReasoningEffort::High),
|
||||
(ReasoningEffort::Minimal, ReasoningEffort::Minimal),
|
||||
(ReasoningEffort::Low, ReasoningEffort::Low),
|
||||
(ReasoningEffort::None, ReasoningEffort::Minimal),
|
||||
(ReasoningEffort::High, ReasoningEffort::High),
|
||||
(ReasoningEffort::Medium, ReasoningEffort::Medium),
|
||||
]),
|
||||
)),
|
||||
show_in_picker: false,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn gpt_5_1() -> ModelPreset {
|
||||
ModelPreset {
|
||||
id: "gpt-5.1".to_string(),
|
||||
model: "gpt-5.1".to_string(),
|
||||
display_name: "gpt-5.1".to_string(),
|
||||
description: "Broad world knowledge with strong general reasoning.".to_string(),
|
||||
default_reasoning_effort: ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: vec![
|
||||
effort(
|
||||
ReasoningEffort::Low,
|
||||
"Balances speed with some reasoning; useful for straightforward queries and short explanations",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::Medium,
|
||||
"Provides a solid balance of reasoning depth and latency for general-purpose tasks",
|
||||
),
|
||||
effort(
|
||||
ReasoningEffort::High,
|
||||
"Maximizes reasoning depth for complex or ambiguous problems",
|
||||
),
|
||||
],
|
||||
supports_personality: false,
|
||||
is_default: false,
|
||||
upgrade: Some(gpt52_codex_upgrade(
|
||||
"gpt-5.1",
|
||||
HashMap::from([
|
||||
(ReasoningEffort::None, ReasoningEffort::Low),
|
||||
(ReasoningEffort::Medium, ReasoningEffort::Medium),
|
||||
(ReasoningEffort::High, ReasoningEffort::High),
|
||||
(ReasoningEffort::XHigh, ReasoningEffort::High),
|
||||
(ReasoningEffort::Low, ReasoningEffort::Low),
|
||||
(ReasoningEffort::Minimal, ReasoningEffort::Low),
|
||||
]),
|
||||
)),
|
||||
show_in_picker: false,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
}
|
||||
}
|
||||
|
||||
fn gpt52_codex_upgrade(
|
||||
migration_config_key: &str,
|
||||
reasoning_effort_mapping: HashMap<ReasoningEffort, ReasoningEffort>,
|
||||
) -> ModelUpgrade {
|
||||
ModelUpgrade {
|
||||
id: "gpt-5.2-codex".to_string(),
|
||||
reasoning_effort_mapping: Some(reasoning_effort_mapping),
|
||||
migration_config_key: migration_config_key.to_string(),
|
||||
model_link: None,
|
||||
upgrade_copy: None,
|
||||
migration_markdown: Some(
|
||||
indoc! {r#"
|
||||
**Codex just got an upgrade. Introducing {model_to}.**
|
||||
|
||||
Codex is now powered by {model_to}, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work. Learn more about {model_to} at https://openai.com/index/introducing-gpt-5-2-codex
|
||||
|
||||
You can continue using {model_from} if you prefer.
|
||||
"#}
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn effort(reasoning_effort: ReasoningEffort, description: &str) -> ReasoningEffortPreset {
|
||||
ReasoningEffortPreset {
|
||||
effort: reasoning_effort,
|
||||
description: description.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::models_manager::manager::ModelsManager;
|
||||
use codex_protocol::openai_models::TruncationPolicyConfig;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
@@ -7,9 +9,14 @@ use tempfile::TempDir;
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn offline_model_info_without_tool_output_override() {
|
||||
let codex_home = TempDir::new().expect("create temp dir");
|
||||
let config = load_default_config_for_test(&codex_home).await;
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
let auth_manager = codex_core::AuthManager::from_auth_for_testing(
|
||||
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
|
||||
);
|
||||
let manager = ModelsManager::new(config.codex_home.clone(), auth_manager);
|
||||
|
||||
let model_info = ModelsManager::construct_model_info_offline("gpt-5.1", &config);
|
||||
let model_info = manager.get_model_info("gpt-5.1", &config).await;
|
||||
|
||||
assert_eq!(
|
||||
model_info.truncation_policy,
|
||||
@@ -21,9 +28,14 @@ async fn offline_model_info_without_tool_output_override() {
|
||||
async fn offline_model_info_with_tool_output_override() {
|
||||
let codex_home = TempDir::new().expect("create temp dir");
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
config.tool_output_token_limit = Some(123);
|
||||
let auth_manager = codex_core::AuthManager::from_auth_for_testing(
|
||||
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
|
||||
);
|
||||
let manager = ModelsManager::new(config.codex_home.clone(), auth_manager);
|
||||
|
||||
let model_info = ModelsManager::construct_model_info_offline("gpt-5.1-codex", &config);
|
||||
let model_info = manager.get_model_info("gpt-5.1-codex", &config).await;
|
||||
|
||||
assert_eq!(
|
||||
model_info.truncation_policy,
|
||||
|
||||
@@ -36,6 +36,7 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec<String> {
|
||||
.with_model(model)
|
||||
// Keep tool expectations stable when the default web_search mode changes.
|
||||
.with_config(|config| {
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
config
|
||||
.web_search_mode
|
||||
.set(WebSearchMode::Cached)
|
||||
@@ -68,22 +69,23 @@ async fn model_selects_expected_tools() {
|
||||
skip_if_no_network!();
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
let codex_tools = collect_tool_identifiers_for_model("codex-mini-latest").await;
|
||||
let gpt51_codex_max_tools = collect_tool_identifiers_for_model("gpt-5.1-codex-max").await;
|
||||
assert_eq!(
|
||||
codex_tools,
|
||||
gpt51_codex_max_tools,
|
||||
expected_default_tools(
|
||||
"local_shell",
|
||||
"shell_command",
|
||||
&[
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
],
|
||||
),
|
||||
"codex-mini-latest should expose the local shell tool",
|
||||
"gpt-5.1-codex-max should expose the apply_patch tool",
|
||||
);
|
||||
|
||||
let gpt5_codex_tools = collect_tool_identifiers_for_model("gpt-5-codex").await;
|
||||
@@ -160,21 +162,4 @@ async fn model_selects_expected_tools() {
|
||||
),
|
||||
"gpt-5.1 should expose the apply_patch tool",
|
||||
);
|
||||
let exp_tools = collect_tool_identifiers_for_model("exp-5.1").await;
|
||||
assert_eq!(
|
||||
exp_tools,
|
||||
vec![
|
||||
"exec_command".to_string(),
|
||||
"write_stdin".to_string(),
|
||||
"list_mcp_resources".to_string(),
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"apply_patch".to_string(),
|
||||
"web_search".to_string(),
|
||||
"view_image".to_string()
|
||||
],
|
||||
"exp-5.1 should expose the apply_patch tool",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
use codex_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::models_manager::model_info::BASE_INSTRUCTIONS;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
|
||||
use codex_core::protocol::EventMsg;
|
||||
@@ -179,7 +178,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn codex_mini_latest_tools() -> anyhow::Result<()> {
|
||||
async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -198,9 +197,10 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> {
|
||||
let TestCodex { codex, .. } = test_codex()
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some("be consistent and helpful".to_string());
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
config.features.disable(Feature::ApplyPatchFreeform);
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
config.model = Some("codex-mini-latest".to_string());
|
||||
config.model = Some("gpt-5".to_string());
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
@@ -228,15 +228,13 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> {
|
||||
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let expected_instructions = [BASE_INSTRUCTIONS, APPLY_PATCH_TOOL_INSTRUCTIONS].join("\n");
|
||||
|
||||
let body0 = req1.single_request().body_json();
|
||||
let instructions0 = body0["instructions"]
|
||||
.as_str()
|
||||
.expect("instructions should be a string");
|
||||
assert_eq!(
|
||||
normalize_newlines(instructions0),
|
||||
normalize_newlines(&expected_instructions)
|
||||
assert!(
|
||||
instructions0.contains("You are"),
|
||||
"expected non-empty instructions"
|
||||
);
|
||||
|
||||
let body1 = req2.single_request().body_json();
|
||||
@@ -245,7 +243,7 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> {
|
||||
.expect("instructions should be a string");
|
||||
assert_eq!(
|
||||
normalize_newlines(instructions1),
|
||||
normalize_newlines(&expected_instructions)
|
||||
normalize_newlines(instructions0)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -55,6 +55,69 @@ use wiremock::MockServer;
|
||||
|
||||
const REMOTE_MODEL_SLUG: &str = "codex-test";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let server = MockServer::start().await;
|
||||
let generic = test_remote_model_with_policy(
|
||||
"gpt-5.3",
|
||||
ModelVisibility::List,
|
||||
1_000,
|
||||
TruncationPolicyConfig::bytes(10_000),
|
||||
);
|
||||
let specific = test_remote_model_with_policy(
|
||||
"gpt-5.3-codex",
|
||||
ModelVisibility::List,
|
||||
1_000,
|
||||
TruncationPolicyConfig::bytes(10_000),
|
||||
);
|
||||
let specific = ModelInfo {
|
||||
display_name: "GPT 5.3 Codex".to_string(),
|
||||
base_instructions: "use specific prefix".to_string(),
|
||||
..specific
|
||||
};
|
||||
let generic = ModelInfo {
|
||||
display_name: "GPT 5.3".to_string(),
|
||||
base_instructions: "use generic prefix".to_string(),
|
||||
..generic
|
||||
};
|
||||
mount_models_once(
|
||||
&server,
|
||||
ModelsResponse {
|
||||
models: vec![generic.clone(), specific.clone()],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers()["openai"].clone()
|
||||
};
|
||||
let manager = ModelsManager::with_provider(
|
||||
codex_home.path().to_path_buf(),
|
||||
codex_core::auth::AuthManager::from_auth_for_testing(auth),
|
||||
provider,
|
||||
);
|
||||
|
||||
manager
|
||||
.list_models(&config, RefreshStrategy::OnlineIfUncached)
|
||||
.await;
|
||||
|
||||
let model_info = manager.get_model_info("gpt-5.3-codex-test", &config).await;
|
||||
|
||||
assert_eq!(model_info.slug, specific.slug);
|
||||
assert_eq!(model_info.base_instructions, specific.base_instructions);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -18,7 +18,6 @@ use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
@@ -30,67 +29,6 @@ use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
// Verifies byte-truncation formatting for function error output (RespondToModel errors)
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn truncate_function_error_trims_respond_to_model() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let mut builder = test_codex().with_model("test-gpt-5.1-codex");
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
// Construct a very long, non-existent path to force a RespondToModel error with a large message
|
||||
let long_path = "long path text should trigger truncation".repeat(8_000);
|
||||
let call_id = "grep-huge-error";
|
||||
let args = json!({
|
||||
"pattern": "alpha",
|
||||
"path": long_path,
|
||||
"limit": 10
|
||||
});
|
||||
let responses = vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(call_id, "grep_files", &serde_json::to_string(&args)?),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
];
|
||||
let mock = mount_sse_sequence(&server, responses).await;
|
||||
|
||||
test.submit_turn_with_policy(
|
||||
"trigger grep_files with long path to test truncation",
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let output = mock
|
||||
.function_call_output_text(call_id)
|
||||
.context("function error output present")?;
|
||||
|
||||
tracing::debug!(output = %output, "truncated function error output");
|
||||
|
||||
// Expect plaintext with token-based truncation marker and no omitted-lines marker
|
||||
assert!(
|
||||
serde_json::from_str::<serde_json::Value>(&output).is_err(),
|
||||
"expected error output to be plain text",
|
||||
);
|
||||
assert!(
|
||||
!output.contains("Total output lines:"),
|
||||
"error output should not include line-based truncation header: {output}",
|
||||
);
|
||||
let truncated_pattern = r"(?s)^unable to access `.*tokens truncated.*$";
|
||||
assert_regex_match(truncated_pattern, &output);
|
||||
assert!(
|
||||
!output.contains("omitted"),
|
||||
"line omission marker should not appear when no lines were dropped: {output}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Verifies that a standard tool call (shell_command) exceeding the model formatting
|
||||
// limits is truncated before being sent back to the model.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
cmake,
|
||||
llvmPackages,
|
||||
openssl,
|
||||
rustPlatform,
|
||||
pkg-config,
|
||||
@@ -15,8 +17,11 @@ rustPlatform.buildRustPackage (_: {
|
||||
doCheck = false;
|
||||
src = ./.;
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
cmake
|
||||
llvmPackages.clang
|
||||
llvmPackages.libclang.lib
|
||||
openssl
|
||||
pkg-config
|
||||
];
|
||||
|
||||
cargoLock.outputHashes = {
|
||||
@@ -25,8 +30,8 @@ rustPlatform.buildRustPackage (_: {
|
||||
"nucleo-0.5.0" = "sha256-Hm4SxtTSBrcWpXrtSqeO0TACbUxq3gizg1zD/6Yw/sI=";
|
||||
"nucleo-matcher-0.3.1" = "sha256-Hm4SxtTSBrcWpXrtSqeO0TACbUxq3gizg1zD/6Yw/sI=";
|
||||
"runfiles-0.1.0" = "sha256-uJpVLcQh8wWZA3GPv9D8Nt43EOirajfDJ7eq/FB+tek=";
|
||||
"tokio-tungstenite-0.28.0" = "sha256-vJZ3S41gHtRt4UAODsjAoSCaTksgzCALiBmbWgyDCi8=";
|
||||
"tungstenite-0.28.0" = "sha256-CyXZp58zGlUhEor7WItjQoS499IoSP55uWqr++ia+0A=";
|
||||
"tokio-tungstenite-0.28.0" = "sha256-hJAkvWxDjB9A9GqansahWhTmj/ekcelslLUTtwqI7lw=";
|
||||
"tungstenite-0.27.0" = "sha256-AN5wql2X2yJnQ7lnDxpljNw0Jua40GtmT+w3wjER010=";
|
||||
};
|
||||
|
||||
meta = with lib; {
|
||||
|
||||
@@ -199,7 +199,9 @@ async fn test_no_new_privs_is_enabled() {
|
||||
let output = run_cmd_output(
|
||||
&["bash", "-lc", "grep '^NoNewPrivs:' /proc/self/status"],
|
||||
&[],
|
||||
SHORT_TIMEOUT_MS,
|
||||
// We have seen timeouts when running this test in CI on GitHub,
|
||||
// so we are using a generous timeout until we can diagnose further.
|
||||
LONG_TIMEOUT_MS,
|
||||
)
|
||||
.await;
|
||||
let line = output
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
//!
|
||||
//! # Submission and Prompt Expansion
|
||||
//!
|
||||
//! When steer is enabled, `Enter` submits immediately. `Tab` requests queuing while a task is
|
||||
//! running; if no task is running, `Tab` submits just like Enter so input is never dropped.
|
||||
//! `Tab` does not submit when entering a `!` shell command.
|
||||
//!
|
||||
//! On submit/queue paths, the composer:
|
||||
//!
|
||||
//! - Expands pending paste placeholders so element ranges align with the final text.
|
||||
@@ -444,7 +448,8 @@ impl ChatComposer {
|
||||
/// Enables or disables "Steer" behavior for submission keys.
|
||||
///
|
||||
/// When steer is enabled, `Enter` produces [`InputResult::Submitted`] (send immediately) and
|
||||
/// `Tab` produces [`InputResult::Queued`] (eligible to queue if a task is running).
|
||||
/// `Tab` produces [`InputResult::Queued`] when a task is running; otherwise it submits
|
||||
/// immediately. `Tab` does not submit when the input is a `!` shell command.
|
||||
/// When steer is disabled, `Enter` produces [`InputResult::Queued`], preserving the default
|
||||
/// "queue while a task is running" behavior.
|
||||
pub fn set_steer_enabled(&mut self, enabled: bool) {
|
||||
@@ -2383,7 +2388,17 @@ impl ChatComposer {
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
} if self.is_task_running => self.handle_submission(true),
|
||||
} if self.steer_enabled && !self.is_bang_shell_command() => {
|
||||
self.handle_submission(self.is_task_running)
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Tab,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
} if self.is_task_running && !self.is_bang_shell_command() => {
|
||||
self.handle_submission(true)
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
@@ -2396,6 +2411,10 @@ impl ChatComposer {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_bang_shell_command(&self) -> bool {
|
||||
self.textarea.text().trim_start().starts_with('!')
|
||||
}
|
||||
|
||||
/// Applies any due `PasteBurst` flush at time `now`.
|
||||
///
|
||||
/// Converts [`PasteBurst::flush_if_due`] results into concrete textarea mutations.
|
||||
@@ -5427,6 +5446,65 @@ mod tests {
|
||||
assert!(elements.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_submits_when_no_task_running_in_steer_mode() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
composer.set_steer_enabled(true);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['h', 'i']);
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
InputResult::Submitted { ref text, .. } if text == "hi"
|
||||
));
|
||||
assert!(composer.textarea.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_does_not_submit_for_bang_shell_command_in_steer_mode() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
composer.set_steer_enabled(true);
|
||||
composer.set_task_running(false);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['!', 'l', 's']);
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||
|
||||
assert!(matches!(result, InputResult::None));
|
||||
assert!(
|
||||
composer.textarea.text().starts_with("!ls"),
|
||||
"expected Tab not to submit or clear a `!` command"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_mention_dispatches_command_and_inserts_at() {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
+4
-4
@@ -5,11 +5,11 @@ expression: popup
|
||||
Select Model and Effort
|
||||
Access legacy models by running codex -m <model_name> or in your config.toml
|
||||
|
||||
› 1. gpt-5.2-codex (default) Latest frontier agentic coding model.
|
||||
2. gpt-5.2 Latest frontier model with improvements across
|
||||
knowledge, reasoning and coding
|
||||
3. gpt-5.1-codex-max Codex-optimized flagship for deep and fast
|
||||
› 1. gpt-5.2-codex (default) Frontier agentic coding model.
|
||||
2. gpt-5.1-codex-max Codex-optimized flagship for deep and fast
|
||||
reasoning.
|
||||
3. gpt-5.2 Latest frontier model with improvements across
|
||||
knowledge, reasoning and coding
|
||||
4. gpt-5.1-codex-mini Optimized for codex. Cheaper, faster, but less
|
||||
capable.
|
||||
|
||||
|
||||
@@ -2361,7 +2361,8 @@ async fn steer_enter_submits_when_plan_stream_is_not_active() {
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn {
|
||||
personality: None, ..
|
||||
personality: Some(Personality::Pragmatic),
|
||||
..
|
||||
} => {}
|
||||
other => panic!("expected Op::UserTurn, got {other:?}"),
|
||||
}
|
||||
@@ -2912,7 +2913,7 @@ async fn collab_slash_command_opens_picker_and_updates_mode() {
|
||||
mode: ModeKind::Default,
|
||||
..
|
||||
}),
|
||||
personality: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
..
|
||||
} => {}
|
||||
other => {
|
||||
@@ -2930,7 +2931,7 @@ async fn collab_slash_command_opens_picker_and_updates_mode() {
|
||||
mode: ModeKind::Default,
|
||||
..
|
||||
}),
|
||||
personality: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
..
|
||||
} => {}
|
||||
other => {
|
||||
@@ -3128,7 +3129,7 @@ async fn collab_mode_is_sent_after_enabling() {
|
||||
mode: ModeKind::Default,
|
||||
..
|
||||
}),
|
||||
personality: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
..
|
||||
} => {}
|
||||
other => {
|
||||
@@ -3148,7 +3149,7 @@ async fn collab_mode_toggle_on_applies_default_preset() {
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn {
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
..
|
||||
} => {}
|
||||
other => panic!("expected Op::UserTurn without collaboration_mode, got {other:?}"),
|
||||
@@ -3166,7 +3167,7 @@ async fn collab_mode_toggle_on_applies_default_preset() {
|
||||
mode: ModeKind::Default,
|
||||
..
|
||||
}),
|
||||
personality: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
..
|
||||
} => {}
|
||||
other => {
|
||||
|
||||
@@ -18,6 +18,6 @@ Visit the Codex community forum: https://community.openai.com/c/codex/37
|
||||
You can run any shell command from Codex using `!` (e.g. `!ls`)
|
||||
Type / to open the command popup; Tab autocompletes slash commands.
|
||||
When the composer is empty, press Esc to step back and edit your last message; Enter confirms.
|
||||
Press Tab to queue a message instead of sending it immediately; Enter always sends immediately.
|
||||
Press Tab to queue a message when a task is running; otherwise it sends immediately (except `!`).
|
||||
Paste an image with Ctrl+V to attach it to your next message.
|
||||
You can resume a previous conversation by running `codex resume`
|
||||
|
||||
@@ -96,6 +96,10 @@ popup so gating stays in sync.
|
||||
|
||||
There are multiple submission paths, but they share the same core rules:
|
||||
|
||||
When steer mode is enabled, `Tab` requests queuing if a task is already running; otherwise it
|
||||
submits immediately. `Enter` always submits immediately in this mode. `Tab` does not submit when
|
||||
the input starts with `!` (shell command).
|
||||
|
||||
### Normal submit/queue path
|
||||
|
||||
`handle_submission` calls `prepare_submission_text` for both submit and queue. That method:
|
||||
|
||||
Reference in New Issue
Block a user