Remove offline fallback for models (#11238)

# External (non-OpenAI) Pull Request Requirements

Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md

If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.

Include a link to a bug report or enhancement request.
This commit is contained in:
Ahmed Ibrahim
2026-02-09 16:58:54 -08:00
committed by GitHub
parent a3e4bd3bc0
commit a1abd53b6a
8 changed files with 122 additions and 499 deletions

View File

@@ -5061,6 +5061,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;
@@ -5094,6 +5095,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;
@@ -5166,33 +5168,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 {
@@ -5209,7 +5202,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(),

View File

@@ -142,7 +142,7 @@ impl ModelsManager {
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)
}
@@ -366,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)
}
}

View File

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

View File

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

View File

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

View File

@@ -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",
);
}

View File

@@ -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(())

View File

@@ -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)]