Remove GPT-5.4 from bundled catalogs and preserve migration prompts (#47932)

## Why

Saved model selections can outlive their catalog entries. Removing GPT-5.4 needs to preserve a migration path without applying it to other providers that may still support the same model slug.

## What changed

- Remove `gpt-5.4` from the bundled catalog and `openai.gpt-5.4` from the Bedrock catalogs, including GovCloud.
- Add fallback migration prompts to GPT-6 Sol for saved GPT-5.4 selections on OpenAI and Amazon Bedrock. Scope fallback migrations, including the existing GPT-5.4 Mini migration, to their owning providers while honoring catalog metadata.
- Update tests that depend on bundled GPT-5.4 metadata to use GPT-5.5 or explicit model overrides.

## Testing

Extend migration tests to cover catalog metadata precedence, acknowledged targets, missing or hidden replacements, and preserving the provider while saving the replacement model and its default reasoning effort. Update Bedrock catalog assertions and picker snapshots.

GitOrigin-RevId: f739795da423b9622db2a416be6c52b01b8e7a71
This commit is contained in:
andrewgu-oai
2026-09-24 20:28:31 +00:00
committed by copyberry
parent e154f7b807
commit 694d8d45bd
37 changed files with 251 additions and 295 deletions
@@ -808,8 +808,8 @@ async fn assert_plugin_measurement_analytics(remote: bool, background: bool) ->
let model = bundled_models_response()?
.models
.into_iter()
.find(|model| model.slug == "gpt-5.4")
.expect("bundled gpt-5.4 model");
.find(|model| model.slug == "gpt-5.5")
.expect("bundled gpt-5.5 model");
let models = [
("initial-model", ReasoningEffort::Low),
("invoking-model", ReasoningEffort::High),
@@ -635,7 +635,7 @@ async fn cold_resume_preserves_effective_developer_instructions_for_worker(
));
}
MockResponsesConfig::new(&server.uri())
.with_model("gpt-5.4")
.with_model("gpt-5.5")
.with_root_config(&format!(
"developer_instructions = {PARENT_INSTRUCTIONS:?}\nmodel_reasoning_effort = \"high\""
))
@@ -650,7 +650,7 @@ async fn cold_resume_preserves_effective_developer_instructions_for_worker(
.await?;
let ThreadStartResponse { thread, .. } = app_server
.start_thread(ThreadStartParams {
model: Some("gpt-5.4".to_string()),
model: Some("gpt-5.5".to_string()),
history_mode: Some(history_mode),
..Default::default()
})
@@ -27,7 +27,7 @@ use tokio::time::timeout;
use wiremock::ResponseTemplate;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const REQUESTED_MODEL: &str = "gpt-5.4";
const REQUESTED_MODEL: &str = "gpt-5.5";
const SERVER_MODEL: &str = "gpt-5.3-codex";
const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION: &str = "trusted_access_for_cyber";
const CYBER_POLICY_MESSAGE: &str =
@@ -256,7 +256,7 @@ async fn thread_start_provider_model_fallback_uses_bedrock_static_catalog() -> R
}
let supported_with_fallback = start_thread_with_model(
&mut mcp,
"openai.gpt-5.4",
"openai.gpt-5.5",
/*allow_provider_model_fallback*/ true,
)
.await?;
@@ -273,7 +273,7 @@ async fn thread_start_provider_model_fallback_uses_bedrock_static_catalog() -> R
supported_with_fallback.model,
unsupported_without_fallback.model,
],
vec!["openai.gpt-6-sol", "openai.gpt-5.4", "gpt-5.4-mini"]
vec!["openai.gpt-6-sol", "openai.gpt-5.5", "gpt-5.4-mini"]
);
Ok(())
}
+4 -4
View File
@@ -31,7 +31,7 @@ use codex_guardian_context::ConversationTranscriptEntryKind;
use codex_guardian_reviewer::guardian_output_contract_prompt;
use codex_history::RolloutItem;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_4_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_5_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::OPENAI_PROVIDER_ID;
@@ -4087,7 +4087,7 @@ async fn guardian_review_session_config_allows_pinned_disabled_feature() {
}
#[tokio::test]
async fn guardian_review_session_config_keeps_bedrock_provider_for_bedrock_gpt_5_4() {
async fn guardian_review_session_config_keeps_bedrock_provider_for_bedrock_gpt_5_5() {
let mut parent_config = test_config().await;
parent_config.model_provider_id = AMAZON_BEDROCK_PROVIDER_ID.to_string();
parent_config.model_provider =
@@ -4096,7 +4096,7 @@ async fn guardian_review_session_config_keeps_bedrock_provider_for_bedrock_gpt_5
let guardian_config = build_guardian_review_session_config_for_test(
crate::guardian::test_host::build_reviewer_config(&parent_config).expect("reviewer config"),
/*live_network_config*/ None,
AMAZON_BEDROCK_GPT_5_4_MODEL_ID,
AMAZON_BEDROCK_GPT_5_5_MODEL_ID,
Some(ReasoningEffort::Low),
ReasoningSummary::default(),
/*personality*/ None,
@@ -4115,7 +4115,7 @@ async fn guardian_review_session_config_keeps_bedrock_provider_for_bedrock_gpt_5
guardian_config.model_provider,
),
(
Some(AMAZON_BEDROCK_GPT_5_4_MODEL_ID.to_string()),
Some(AMAZON_BEDROCK_GPT_5_5_MODEL_ID.to_string()),
AMAZON_BEDROCK_PROVIDER_ID.to_string(),
expected_model_provider,
)
+9 -9
View File
@@ -492,7 +492,7 @@ async fn apply_patch_cli_uses_codex_self_exe_with_linux_sandbox_helper_alias() -
async fn apply_patch_cli_multiple_operations_integration() -> Result<()> {
skip_if_no_network!(Ok(()));
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?;
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.5")).await?;
// Seed workspace state
harness.write_file("modify.txt", "line1\nline2\n").await?;
@@ -1014,7 +1014,7 @@ async fn intercepted_apply_patch_updates_absolute_target_after_turn_cwd_is_remov
"the default Windows restricted-token test backend cannot enforce denied reads"
);
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?;
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.5")).await?;
let test = harness.test();
let workspace = test.workspace_path_uri("")?;
let original_cwd = test.workspace_path_uri("policy-cwd")?;
@@ -1165,7 +1165,7 @@ async fn escalated_patch_rejects_symlink_swapped_after_approval_request() -> Res
fs::write(work.join("file.txt"), "original\n")?;
fs::write(&outside, "original\n")?;
let harness = apply_patch_harness_with(move |builder| {
builder.with_model("gpt-5.4").with_config(move |config| {
builder.with_model("gpt-5.5").with_config(move |config| {
config.cwd = work.try_into().expect("absolute workspace");
config.workspace_roots = vec![config.cwd.clone()];
config
@@ -1545,7 +1545,7 @@ async fn apply_patch_exec_command_heredoc_with_cd_updates_relative_workdir() ->
skip_if_wine_exec!(Ok(()), "uses a POSIX shell heredoc and cd command");
skip_if_no_network!(Ok(()));
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?;
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.5")).await?;
// Prepare a file inside a subdir; update it via cd && apply_patch heredoc form.
harness.write_file("sub/in_sub.txt", "before\n").await?;
@@ -1585,7 +1585,7 @@ async fn apply_patch_cli_can_use_exec_command_output_as_patch_input() -> Result<
);
let harness =
apply_patch_harness_with(|builder| builder.with_model("gpt-5.4").with_windows_cmd_shell())
apply_patch_harness_with(|builder| builder.with_model("gpt-5.5").with_windows_cmd_shell())
.await?;
let source_contents = "line1\nnaïve café\nline3\n";
@@ -1829,7 +1829,7 @@ async fn apply_patch_exec_command_heredoc_with_cd_emits_turn_diff() -> Result<()
skip_if_wine_exec!(Ok(()), "uses a POSIX shell heredoc and cd command");
skip_if_no_network!(Ok(()));
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?;
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.5")).await?;
let test = harness.test();
let codex = test.codex.clone();
@@ -1895,7 +1895,7 @@ async fn apply_patch_turn_diff_paths_stay_repo_relative_when_session_cwd_is_nest
let harness = apply_patch_harness_with(|builder| {
builder
.with_model("gpt-5.4")
.with_model("gpt-5.5")
.with_config(|config| {
config.cwd = config.cwd.join("subdir");
})
@@ -1987,7 +1987,7 @@ async fn apply_patch_turn_diff_skips_git_root_when_feature_is_enabled(
let harness = apply_patch_harness_with(|builder| {
builder
.with_model("gpt-5.4")
.with_model("gpt-5.5")
.with_config(move |config| {
config.cwd = config.cwd.join("subdir");
if cwd_relative_turn_diffs {
@@ -2081,7 +2081,7 @@ async fn apply_patch_exec_command_failure_propagates_error_and_skips_diff() -> R
skip_if_wine_exec!(Ok(()), "uses a POSIX shell heredoc");
skip_if_no_network!(Ok(()));
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?;
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.5")).await?;
let test = harness.test();
let codex = test.codex.clone();
+1 -1
View File
@@ -2153,7 +2153,7 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file()
let sandbox_policy_for_config = sandbox_policy.clone();
let mut builder = test_codex()
.with_model("gpt-5.4")
.with_model("gpt-5.5")
.with_config(move |config| {
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
config
+1 -1
View File
@@ -1372,7 +1372,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
.await;
let codex_home = Arc::new(TempDir::new().unwrap());
let mut builder = test_codex().with_model("gpt-5.4");
let mut builder = test_codex().with_model("gpt-5.5");
let test = builder
.resume(&server, codex_home, session_path.clone())
.await
+1 -1
View File
@@ -5183,7 +5183,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
let model_provider = non_openai_model_provider(&server);
let test = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_model(previous_model)
.with_model_info_override(previous_model, |_| {})
.with_config(move |config| {
config.update_plan_enabled = true;
config.model_provider = model_provider;
+1 -1
View File
@@ -1485,7 +1485,7 @@ async fn guardian_session_is_reused_for_consecutive_tool_reviews_without_prewarm
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
extensions.tool_lifecycle_contributor(lifecycle_recorder.clone());
let mut builder = test_codex()
.with_model("gpt-5.4")
.with_model_info_override("gpt-5.4", |_| {})
.with_extensions(Arc::new(extensions.build()))
.with_config(move |config| {
let secret_file = config.cwd.join("guardian-secret.txt");
+2 -2
View File
@@ -3651,8 +3651,8 @@ async fn pre_tool_use_hook_model_tracks_step_after_a_turn_update() -> Result<()>
.expect("bundled models should parse")
.models
.into_iter()
.find(|model| model.slug == "gpt-5.4")
.expect("bundled gpt-5.4 model");
.find(|model| model.slug == "gpt-5.5")
.expect("bundled gpt-5.5 model");
let models = [model_a, model_b]
.into_iter()
.map(|slug| {
@@ -475,8 +475,8 @@ fn attribution_models(model_slugs: [&str; 2]) -> Vec<codex_protocol::openai_mode
.expect("bundled models should parse")
.models
.into_iter()
.find(|model| model.slug == "gpt-5.4")
.expect("bundled gpt-5.4 model");
.find(|model| model.slug == "gpt-5.5")
.expect("bundled gpt-5.5 model");
model_slugs
.into_iter()
.map(|slug| {
@@ -286,7 +286,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
let mut builder = test_codex()
.with_extensions(skills_extensions())
.with_model("gpt-5.4")
.with_model("gpt-5.5")
.with_config(|config| {
config.update_plan_enabled = true;
});
@@ -399,7 +399,7 @@ async fn snapshot_model_visible_layout_cwd_change_refreshes_agents() -> Result<(
let mut builder = test_codex()
.with_config(|config| config.update_plan_enabled = true)
.with_extensions(skills_extensions())
.with_model("gpt-5.4");
.with_model("gpt-5.5");
let test = builder.build(&server).await?;
let cwd_one = test.cwd_path().join("agents_one");
let cwd_two = test.cwd_path().join("agents_two");
@@ -546,9 +546,9 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
let mut resume_builder = test_codex()
.with_extensions(skills_extensions())
.with_model_info_override("gpt-5.4", |_| {})
.with_config(|config| {
config.update_plan_enabled = true;
config.model = Some("gpt-5.4".to_string());
config.personality = Some(Personality::Pragmatic);
});
let resumed = resume_builder.restart(&server, &initial).await?;
@@ -654,9 +654,9 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
let mut resume_builder = test_codex()
.with_extensions(skills_extensions())
.with_model_info_override("gpt-5.4", |_| {})
.with_config(|config| {
config.update_plan_enabled = true;
config.model = Some("gpt-5.4".to_string());
});
let resumed = resume_builder.restart(&server, &initial).await?;
let resume_override_cwd = resumed.cwd_path().join(PRETURN_CONTEXT_DIFF_CWD);
@@ -65,7 +65,7 @@ async fn guardian_reused_reviewer_avoids_stale_catalog_lookup() -> Result<()> {
models: bundled
.models
.into_iter()
.filter(|model| ["gpt-5.4", "codex-auto-review"].contains(&model.slug.as_str()))
.filter(|model| ["gpt-5.5", "codex-auto-review"].contains(&model.slug.as_str()))
.collect(),
};
assert_eq!(catalog.models.len(), 2);
@@ -74,7 +74,7 @@ async fn guardian_reused_reviewer_avoids_stale_catalog_lookup() -> Result<()> {
let mut builder = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_model("gpt-5.4");
.with_model("gpt-5.5");
builder = builder.with_config(|config| {
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
@@ -3025,8 +3025,8 @@ pub(super) fn guardian_parent_catalog() -> ModelsResponse {
.expect("bundled model catalog")
.models
.into_iter()
.find(|model| model.slug == "gpt-5.4")
.expect("gpt-5.4 in bundled catalog");
.find(|model| model.slug == "gpt-5.5")
.expect("gpt-5.5 in bundled catalog");
// Keep safety settings compatible so active publication can switch A to B.
ModelsResponse {
models: ["guardian-parent-a", "guardian-parent-b"]
+1 -1
View File
@@ -122,7 +122,7 @@ async fn responses_api_emits_api_request_event() {
let response_mock = mount_sse_once(&server, sse(vec![ev_completed("done")])).await;
let TestCodex { codex, .. } = test_codex()
.with_model("gpt-5.4")
.with_model("gpt-5.5")
.with_config(|config| {
config.service_tier = Some(ServiceTier::Fast.request_value().to_string());
config.model_reasoning_effort = Some(ReasoningEffort::High);
+7 -10
View File
@@ -22,8 +22,6 @@ use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use test_case::test_case;
const LOCAL_FRIENDLY_TEMPLATE: &str =
"You optimize for team morale and being a supportive teammate as much as code quality.";
const BUNDLED_FRIENDLY_TEMPLATE: &str = "You have a vivid inner life as Codex:";
const CUSTOM_INSTRUCTIONS: &str = "Custom instructions\n# Personality\nThis must remain\n## Writing Style\nThis must also remain\n# General\nGeneral instructions";
@@ -225,13 +223,8 @@ async fn default_instructions_are_friendly_without_config_toml() -> anyhow::Resu
Ok(())
}
#[test_case("gpt-5.4", LOCAL_FRIENDLY_TEMPLATE; "gpt_5_4")]
#[test_case("gpt-5.5", BUNDLED_FRIENDLY_TEMPLATE; "gpt_5_5")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fixed_friendly_personality_ignores_pragmatic_update(
model: &str,
friendly_template: &str,
) -> anyhow::Result<()> {
async fn fixed_friendly_personality_ignores_pragmatic_update() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -240,7 +233,7 @@ async fn fixed_friendly_personality_ignores_pragmatic_update(
vec![sse_completed("resp-1"), sse_completed("resp-2")],
)
.await;
let mut builder = test_codex().with_model(model).with_config(|config| {
let mut builder = test_codex().with_model("gpt-5.5").with_config(|config| {
config.personality = Some(Personality::Friendly);
});
let test = builder.build_with_auto_env(&server).await?;
@@ -259,7 +252,11 @@ async fn fixed_friendly_personality_ignores_pragmatic_update(
let requests = responses.requests();
assert_eq!(requests.len(), 2);
assert!(requests[0].instructions_text().contains(friendly_template));
assert!(
requests[0]
.instructions_text()
.contains(BUNDLED_FRIENDLY_TEMPLATE)
);
assert_eq!(
requests[1].instructions_text(),
requests[0].instructions_text()
+1 -1
View File
@@ -3118,7 +3118,7 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re
let rmcp_test_server_bin = remote_aware_stdio_server_bin()?;
let fixture = test_codex()
.with_model("gpt-5.4")
.with_model("gpt-5.5")
.with_config(move |config| {
insert_mcp_server(
config,
+4 -4
View File
@@ -219,7 +219,7 @@ async fn tool_call_output_exceeds_limit_truncated_for_model() -> Result<()> {
let server = start_mock_server().await;
// Use a model that exposes the exec_command tool.
let mut builder = test_codex().with_model("gpt-5.4");
let mut builder = test_codex().with_model("gpt-5.5");
let fixture = builder.build(&server).await?;
let call_id = "shell-too-large";
@@ -302,7 +302,7 @@ async fn tool_call_output_truncated_only_once() -> Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.4");
let mut builder = test_codex().with_model("gpt-5.5");
let fixture = builder.build(&server).await?;
let call_id = "shell-single-truncation";
let command = if cfg!(windows) {
@@ -593,7 +593,7 @@ async fn token_policy_marker_reports_tokens() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| {
let mut builder = test_codex().with_model("gpt-5.5").with_config(|config| {
config.tool_output_token_limit = Some(50); // small budget to force truncation
});
let fixture = builder.build(&server).await?;
@@ -699,7 +699,7 @@ async fn exec_command_output_not_truncated_with_custom_limit() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| {
let mut builder = test_codex().with_model("gpt-5.5").with_config(|config| {
config.tool_output_token_limit = Some(50_000); // ample budget
});
let fixture = builder.build(&server).await?;
+2 -2
View File
@@ -1972,7 +1972,7 @@ async fn exec_command_clamps_model_requested_max_output_tokens_to_policy() -> Re
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| {
let mut builder = test_codex().with_model("gpt-5.5").with_config(|config| {
config.tool_output_token_limit = Some(50);
});
let test = builder.build_with_auto_env(&server).await?;
@@ -2031,7 +2031,7 @@ async fn write_stdin_clamps_model_requested_max_output_tokens_to_policy() -> Res
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| {
let mut builder = test_codex().with_model("gpt-5.5").with_config(|config| {
config.tool_output_token_limit = Some(50);
});
let test = builder.build_with_auto_env(&server).await?;
+6 -6
View File
@@ -839,12 +839,12 @@ async fn view_image_routes_to_selected_remote_environment() -> anyhow::Result<()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5_4()
async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5_5()
-> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.4");
let mut builder = test_codex().with_model("gpt-5.5");
let test = builder.build_with_auto_env(&server).await?;
let TestCodex {
codex,
@@ -935,7 +935,7 @@ async fn view_image_unified_budget_hides_detail_but_accepts_legacy_hints() -> an
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| {
let mut builder = test_codex().with_model("gpt-5.5").with_config(|config| {
let _ = config.features.enable(Feature::UnifiedImageBudget);
});
let test = builder.build_with_auto_env(&server).await?;
@@ -1011,7 +1011,7 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.4");
let mut builder = test_codex().with_model("gpt-5.5");
let test = builder.build_with_auto_env(&server).await?;
let TestCodex {
codex,
@@ -1089,7 +1089,7 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.4");
let mut builder = test_codex().with_model("gpt-5.5");
let test = builder.build_with_auto_env(&server).await?;
let TestCodex {
codex,
@@ -1292,7 +1292,7 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_only
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.4");
let mut builder = test_codex().with_model("gpt-5.5");
let test = builder.build_with_auto_env(&server).await?;
let TestCodex {
codex,
+4 -4
View File
@@ -4,7 +4,7 @@ use codex_config::test_support::CloudConfigBundleFixture;
use codex_features::Feature;
use codex_login::CodexAuth;
use codex_login::auth::BedrockApiKeyAuth;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_4_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_5_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;
use codex_model_provider_info::AMAZON_BEDROCK_RUNTIME_PROVIDER_ID;
use codex_model_provider_info::ModelProviderInfo;
@@ -106,8 +106,8 @@ async fn amazon_bedrock_web_search_uses_text_only_hosted_tools() {
});
let mut builder = test_codex().with_auth(auth);
builder = match model_catalog {
ModelCatalog::BuiltIn => builder.with_model(AMAZON_BEDROCK_GPT_5_4_MODEL_ID),
ModelCatalog::Configured => builder.with_model_info_override("gpt-5.4", |model_info| {
ModelCatalog::BuiltIn => builder.with_model(AMAZON_BEDROCK_GPT_5_5_MODEL_ID),
ModelCatalog::Configured => builder.with_model_info_override("gpt-5.5", |model_info| {
model_info.web_search_tool_type = WebSearchToolType::TextAndImage;
}),
};
@@ -232,7 +232,7 @@ async fn amazon_bedrock_web_search_is_disabled_when_managed_requirements_prohibi
});
let mut builder = test_codex()
.with_auth(auth)
.with_model(AMAZON_BEDROCK_GPT_5_4_MODEL_ID)
.with_model(AMAZON_BEDROCK_GPT_5_5_MODEL_ID)
.with_cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(format!(
r#"allowed_web_search_modes = ["{allowed_mode}"]"#
-1
View File
@@ -80,7 +80,6 @@ pub const AMAZON_BEDROCK_PROVIDER_ID: &str = "amazon-bedrock";
const AMAZON_BEDROCK_RUNTIME_PROVIDER_NAME: &str = "Amazon Bedrock Runtime";
pub const AMAZON_BEDROCK_RUNTIME_PROVIDER_ID: &str = "amazon-bedrock-runtime";
pub const AMAZON_BEDROCK_GPT_5_5_MODEL_ID: &str = "openai.gpt-5.5";
pub const AMAZON_BEDROCK_GPT_5_4_MODEL_ID: &str = "openai.gpt-5.4";
pub const AMAZON_BEDROCK_GPT_5_6_SOL_MODEL_ID: &str = "openai.gpt-5.6-sol";
pub const AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID: &str = "openai.gpt-6-sol";
pub const AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID: &str = "openai.gpt-6-luna";
@@ -1,4 +1,3 @@
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_4_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_5_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_6_LUNA_MODEL_ID;
use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_6_SOL_MODEL_ID;
@@ -22,7 +21,6 @@ const GPT_6_SOL_OPENAI_MODEL_ID: &str = "gpt-6-sol";
const GPT_6_LUNA_OPENAI_MODEL_ID: &str = "gpt-6-luna";
const GPT_6_ASTRA_OPENAI_MODEL_ID: &str = "gpt-6-astra";
const GPT_5_5_OPENAI_MODEL_ID: &str = "gpt-5.5";
const GPT_5_4_OPENAI_MODEL_ID: &str = "gpt-5.4";
pub(crate) fn static_model_catalog() -> ModelsResponse {
normalize_bedrock_catalog(ModelsResponse {
@@ -69,12 +67,6 @@ pub(crate) fn static_model_catalog() -> ModelsResponse {
"GPT-5.5",
/*priority*/ 6,
),
gpt_5_bedrock_model(
GPT_5_4_OPENAI_MODEL_ID,
AMAZON_BEDROCK_GPT_5_4_MODEL_ID,
"GPT-5.4",
/*priority*/ 7,
),
],
})
}
@@ -84,9 +76,7 @@ pub(super) fn static_gov_model_catalog() -> ModelsResponse {
catalog.models.retain(|model| {
matches!(
model.slug.as_str(),
AMAZON_BEDROCK_GPT_5_6_TERRA_MODEL_ID
| AMAZON_BEDROCK_GPT_5_6_LUNA_MODEL_ID
| AMAZON_BEDROCK_GPT_5_4_MODEL_ID
AMAZON_BEDROCK_GPT_5_6_TERRA_MODEL_ID | AMAZON_BEDROCK_GPT_5_6_LUNA_MODEL_ID
)
});
catalog
@@ -178,7 +168,6 @@ mod tests {
AMAZON_BEDROCK_GPT_5_6_TERRA_MODEL_ID,
AMAZON_BEDROCK_GPT_5_6_LUNA_MODEL_ID,
AMAZON_BEDROCK_GPT_5_5_MODEL_ID,
AMAZON_BEDROCK_GPT_5_4_MODEL_ID,
]
);
}
@@ -241,19 +230,13 @@ mod tests {
Some(GPT_5_BEDROCK_CONTEXT_WINDOW),
WebSearchToolType::Text,
),
(
AMAZON_BEDROCK_GPT_5_4_MODEL_ID,
Some(GPT_5_BEDROCK_CONTEXT_WINDOW),
Some(GPT_5_BEDROCK_CONTEXT_WINDOW),
WebSearchToolType::Text,
),
]
);
}
#[test]
fn configured_bedrock_catalogs_normalize_unsupported_model_capabilities() {
let model = bundled_openai_model(GPT_5_4_OPENAI_MODEL_ID);
let model = bundled_openai_model(GPT_5_5_OPENAI_MODEL_ID);
let mut expected = model.clone();
expected.additional_speed_tiers.clear();
expected.service_tiers.clear();
-2
View File
@@ -1302,7 +1302,6 @@ printf '%s\n' '{"AccessKeyId":"exported","SecretAccessKey":"secret"}'
("openai.gpt-5.6-terra", "GPT-5.6 Terra"),
("openai.gpt-5.6-luna", "GPT-5.6 Luna"),
("openai.gpt-5.5", "GPT-5.5"),
("openai.gpt-5.4", "GPT-5.4"),
]
);
@@ -1325,7 +1324,6 @@ printf '%s\n' '{"AccessKeyId":"exported","SecretAccessKey":"secret"}'
"openai.gpt-5.6-terra",
"openai.gpt-5.6-luna",
"openai.gpt-5.5",
"openai.gpt-5.4",
]
);
File diff suppressed because one or more lines are too long
+23 -6
View File
@@ -5,6 +5,8 @@
use super::*;
use codex_config::ConfigLayerSource;
use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;
use codex_model_provider_info::OPENAI_PROVIDER_ID;
use std::collections::HashSet;
use std::path::PathBuf;
@@ -111,6 +113,7 @@ pub(super) fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &
}
pub(super) fn model_upgrade_for_migration(
model_provider_id: &str,
model: &str,
available_models: &[ModelPreset],
) -> Option<ModelUpgrade> {
@@ -118,24 +121,37 @@ pub(super) fn model_upgrade_for_migration(
return preset.upgrade.clone();
}
// Saved selections can outlive their catalog entries. Keep only their migration metadata.
let (target_model, current_name, target_name) = match model {
"gpt-5.4-mini" => ("gpt-6-luna", "GPT-5.4 Mini", "GPT-6 Luna"),
// Saved selections can outlive their catalog entries. Scope fallback migration metadata
// to the owning provider, since other providers may still support the same model slug.
let (target_model, current_name, target_name) = match (model_provider_id, model) {
(OPENAI_PROVIDER_ID, "gpt-5.4") => ("gpt-6-sol", "GPT-5.4", "GPT-6 Sol"),
(AMAZON_BEDROCK_PROVIDER_ID, "openai.gpt-5.4") => (
"openai.gpt-6-sol",
"GPT-5.4 on Amazon Bedrock",
"GPT-6 Sol on Amazon Bedrock",
),
(OPENAI_PROVIDER_ID, "gpt-5.4-mini") => ("gpt-6-luna", "GPT-5.4 Mini", "GPT-6 Luna"),
_ => return None,
};
let availability = if model == "openai.gpt-5.4" {
"no longer offered in Codex"
} else {
"no longer available"
};
Some(ModelUpgrade {
id: target_model.to_string(),
migration_config_key: model.to_string(),
model_link: None,
upgrade_copy: None,
migration_markdown: Some(format!(
"{current_name} is no longer available\n\nCodex now uses {target_name} in place of {current_name}. Switch to {target_name} to continue.\n"
"{current_name} is {availability}\n\nCodex now uses {target_name} in place of {current_name}. Switch to {target_name} to continue.\n"
)),
retirement_at: None,
})
}
pub(super) fn should_show_model_migration_prompt(
model_provider_id: &str,
current_model: &str,
target_model: &str,
seen_migrations: &BTreeMap<String, String>,
@@ -158,7 +174,7 @@ pub(super) fn should_show_model_migration_prompt(
return false;
}
if model_upgrade_for_migration(current_model, available_models)
if model_upgrade_for_migration(model_provider_id, current_model, available_models)
.is_some_and(|upgrade| upgrade.id == target_model)
{
return true;
@@ -298,7 +314,7 @@ pub(super) async fn handle_model_migration_prompt_if_needed(
app_event_tx: &AppEventSender,
available_models: &[ModelPreset],
) -> std::io::Result<Option<AppExitInfo>> {
let upgrade = model_upgrade_for_migration(model, available_models);
let upgrade = model_upgrade_for_migration(&config.model_provider_id, model, available_models);
if let Some(ModelUpgrade {
id: target_model,
@@ -315,6 +331,7 @@ pub(super) async fn handle_model_migration_prompt_if_needed(
let target_model = target_model.to_string();
if !should_show_model_migration_prompt(
&config.model_provider_id,
model,
&target_model,
&local_settings.notices.model_migrations,
+155 -80
View File
@@ -1,6 +1,8 @@
use super::*;
use assert_matches::assert_matches;
use codex_config::types::ModelAvailabilityNuxConfig;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ModelAvailabilityNux;
use pretty_assertions::assert_eq;
@@ -10,6 +12,16 @@ fn all_model_presets() -> Vec<ModelPreset> {
crate::test_support::TEST_MODEL_PRESETS.clone()
}
fn bedrock_model_presets() -> Vec<ModelPreset> {
create_model_provider(
ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None),
/*auth_manager*/ None,
)
.models_manager_without_cache(/*config_model_catalog*/ None)
.try_list_models()
.expect("Bedrock model catalog")
}
#[tokio::test]
async fn model_picker_refresh_updates_app_catalog_from_app_server() -> Result<()> {
let (mut app, mut rx, _op_rx) = make_test_app_with_channels().await;
@@ -175,40 +187,43 @@ async fn model_migration_prompt_only_shows_for_deprecated_models() {
let seen = BTreeMap::new();
let presets = model_presets_with_test_upgrades();
assert!(should_show_model_migration_prompt(
"gpt-5.2", "gpt-5.5", &seen, &presets
"openai", "gpt-5.2", "gpt-5.5", &seen, &presets
));
assert!(should_show_model_migration_prompt(
"openai",
"gpt-5.4",
"gpt-6-sol",
&seen,
&presets
));
assert!(!should_show_model_migration_prompt(
"gpt-5.4", "gpt-5.4", &seen, &presets
"openai", "gpt-5.4", "gpt-5.4", &seen, &presets
));
}
#[test]
fn retired_model_migration_respects_catalog_metadata() {
let mut presets = model_presets_with_test_upgrades();
let current = presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.2")
.expect("test preset");
current.id = "gpt-5.4-mini".to_string();
current.model = "gpt-5.4-mini".to_string();
let expected = current.upgrade.clone();
assert_eq!(
model_upgrade_for_migration("gpt-5.4-mini", &presets),
expected
);
for model in ["gpt-5.4-mini", "gpt-5.4", "openai.gpt-5.4"] {
let mut presets = model_presets_with_test_upgrades();
let current = presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.2")
.expect("test preset");
current.id = model.to_string();
current.model = model.to_string();
let expected = current.upgrade.clone();
assert_eq!(
model_upgrade_for_migration("openai", model, &presets),
expected
);
presets
.iter_mut()
.find(|preset| preset.model == "gpt-5.4-mini")
.expect("catalog preset")
.upgrade = None;
assert_eq!(model_upgrade_for_migration("gpt-5.4-mini", &presets), None);
presets
.iter_mut()
.find(|preset| preset.model == model)
.expect("catalog preset")
.upgrade = None;
assert_eq!(model_upgrade_for_migration("openai", model, &presets), None);
}
}
#[test]
@@ -366,30 +381,39 @@ async fn prepare_startup_tooltip_override_persists_model_availability_nux_count(
#[tokio::test]
async fn accepted_model_migration_persists_target_default_reasoning_effort() -> Result<()> {
let presets = all_model_presets();
let mut migration_copies = Vec::new();
for (model, replacement) in [
("gpt-5.4", "gpt-6-sol"),
("gpt-5.4-mini", "gpt-6-luna"),
("gpt-5.5", "gpt-6-sol"),
("gpt-5.6-luna", "gpt-6-luna"),
("gpt-5.6-terra", "gpt-6-sol"),
("gpt-5.6-sol", "gpt-6-sol"),
for (model, replacement, provider_id, presets) in [
("gpt-5.4", "gpt-6-sol", "openai", all_model_presets()),
("gpt-5.4-mini", "gpt-6-luna", "openai", all_model_presets()),
("gpt-5.5", "gpt-6-sol", "openai", all_model_presets()),
("gpt-5.6-luna", "gpt-6-luna", "openai", all_model_presets()),
("gpt-5.6-terra", "gpt-6-sol", "openai", all_model_presets()),
("gpt-5.6-sol", "gpt-6-sol", "openai", all_model_presets()),
(
"openai.gpt-5.4",
"openai.gpt-6-sol",
"amazon-bedrock",
bedrock_model_presets(),
),
] {
let codex_home = tempdir()?;
std::fs::write(
codex_home.path().join("config.toml"),
format!("model = \"{model}\"\nmodel_reasoning_effort = \"xhigh\"\n"),
format!(
"model = \"{model}\"\nmodel_reasoning_effort = \"xhigh\"\nmodel_provider = \"{provider_id}\"\n"
),
)?;
let mut config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await?;
let current_model = config.model.clone().expect("saved model");
let upgrade = model_upgrade_for_migration(&current_model, &presets)
let provider = config.model_provider.clone();
let upgrade = model_upgrade_for_migration(provider_id, &current_model, &presets)
.expect("catalog selection should have an upgrade");
assert_eq!(upgrade.id, replacement);
assert!(should_show_model_migration_prompt(
provider_id,
&current_model,
&upgrade.id,
&BTreeMap::new(),
@@ -421,8 +445,20 @@ async fn accepted_model_migration_persists_target_default_reasoning_effort() ->
target_effort.clone(),
);
assert_eq!(config.model.as_deref(), Some(replacement));
assert_eq!(config.model_reasoning_effort, Some(target_effort.clone()));
assert_eq!(
(
config.model.as_deref(),
config.model_reasoning_effort.as_ref(),
config.model_provider_id.as_str(),
&config.model_provider,
),
(
Some(replacement),
Some(&target_effort),
provider_id,
&provider
)
);
let acknowledged = rx.try_recv().expect("acknowledged event");
assert_matches!(
@@ -474,65 +510,103 @@ async fn accepted_model_migration_persists_target_default_reasoning_effort() ->
Meet GPT-6 Sol
Our latest Sol is more intelligent and more efficient so your usage limits go further. This model is a great daily driver for complex tasks, especially coding.
GPT-5.4 on Amazon Bedrock is no longer offered in Codex
Codex now uses GPT-6 Sol on Amazon Bedrock in place of GPT-5.4 on Amazon Bedrock. Switch to GPT-6 Sol on Amazon Bedrock to continue.
");
Ok(())
}
#[tokio::test]
async fn model_migration_prompt_respects_hide_flag_and_self_target() {
let presets = all_model_presets();
let mut seen = BTreeMap::new();
// A previously acknowledged intermediate upgrade must not hide the new target.
seen.insert("gpt-5.4-mini".to_string(), "gpt-5.6-luna".to_string());
assert!(should_show_model_migration_prompt(
"gpt-5.4-mini",
"gpt-6-luna",
&seen,
&presets
));
seen.insert("gpt-5.4-mini".to_string(), "gpt-6-luna".to_string());
assert!(!should_show_model_migration_prompt(
"gpt-5.4-mini",
"gpt-6-luna",
&seen,
&presets
));
assert!(!should_show_model_migration_prompt(
"gpt-6-luna",
"gpt-6-luna",
&seen,
&presets
));
for (model, previous_target, target, presets) in [
(
"gpt-5.4-mini",
"gpt-5.6-luna",
"gpt-6-luna",
all_model_presets(),
),
("gpt-5.4", "gpt-5.6-terra", "gpt-6-sol", all_model_presets()),
(
"openai.gpt-5.4",
"openai.gpt-5.6-terra",
"openai.gpt-6-sol",
bedrock_model_presets(),
),
] {
let provider_id = if model == "openai.gpt-5.4" {
"amazon-bedrock"
} else {
"openai"
};
// A previously acknowledged intermediate upgrade must not hide the new target.
let mut seen = BTreeMap::from([(model.to_string(), previous_target.to_string())]);
assert!(should_show_model_migration_prompt(
provider_id,
model,
target,
&seen,
&presets
));
seen.insert(model.to_string(), target.to_string());
assert!(!should_show_model_migration_prompt(
provider_id,
model,
target,
&seen,
&presets
));
assert!(!should_show_model_migration_prompt(
provider_id,
target,
target,
&seen,
&presets
));
}
}
#[tokio::test]
async fn model_migration_prompt_skips_when_target_missing_or_hidden() {
let mut available = all_model_presets();
available.retain(|preset| preset.model != "gpt-6-luna");
for (model, target_model, mut available) in [
("gpt-5.4-mini", "gpt-6-luna", all_model_presets()),
("gpt-5.4", "gpt-6-sol", all_model_presets()),
(
"openai.gpt-5.4",
"openai.gpt-6-sol",
bedrock_model_presets(),
),
] {
let provider_id = if model == "openai.gpt-5.4" {
"amazon-bedrock"
} else {
"openai"
};
let target = available
.iter_mut()
.find(|preset| preset.model == target_model)
.expect("target preset present");
target.show_in_picker = false;
assert!(!should_show_model_migration_prompt(
provider_id,
model,
target_model,
&BTreeMap::new(),
&available,
));
assert!(target_preset_for_upgrade(&available, target_model).is_none());
assert!(!should_show_model_migration_prompt(
"gpt-5.4-mini",
"gpt-6-luna",
&BTreeMap::new(),
&available,
));
assert!(target_preset_for_upgrade(&available, "gpt-6-luna").is_none());
let mut with_hidden_target = all_model_presets();
let target = with_hidden_target
.iter_mut()
.find(|preset| preset.model == "gpt-6-luna")
.expect("target preset present");
target.show_in_picker = false;
assert!(!should_show_model_migration_prompt(
"gpt-5.4-mini",
"gpt-6-luna",
&BTreeMap::new(),
&with_hidden_target,
));
assert!(target_preset_for_upgrade(&with_hidden_target, "gpt-6-luna").is_none());
available.retain(|preset| preset.model != target_model);
assert!(!should_show_model_migration_prompt(
provider_id,
model,
target_model,
&BTreeMap::new(),
&available,
));
assert!(target_preset_for_upgrade(&available, target_model).is_none());
}
}
#[tokio::test]
@@ -564,6 +638,7 @@ async fn model_migration_prompt_shows_for_hidden_model() {
.show_in_picker = true;
assert!(
should_show_model_migration_prompt(
"openai",
&current.model,
&upgrade.id,
&config.notices.model_migrations,
@@ -90,6 +90,7 @@ async fn cli_fork_omits_implicit_model_and_effort() -> Result<()> {
});
assert!(
crate::app::startup_prompts::should_show_model_migration_prompt(
"openai",
"gpt-5.5",
"server-model",
&Default::default(),
+2 -2
View File
@@ -3989,7 +3989,7 @@ mod tests {
duration_ms: None,
}],
},
model: "gpt-5.4".to_string(),
model: "gpt-5.5".to_string(),
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp/project").abs(),
@@ -4011,7 +4011,7 @@ mod tests {
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Plan,
settings: codex_protocol::config_types::Settings {
model: "gpt-5.4".to_string(),
model: "gpt-5.5".to_string(),
reasoning_effort: None,
developer_instructions: Some("Keep planning".to_string()),
},
@@ -7,6 +7,5 @@ expression: "render_bottom_popup(&chat, 100)"
› 1. GPT-5.6 Terra (current) Older balanced model for straightforward work.
2. GPT-5.6 Luna Older fast and efficient model.
3. GPT-5.4 Strong model for everyday coding.
enter select · esc back
@@ -12,6 +12,5 @@ expression: "render_bottom_popup(&chat, 100)"
5. GPT-5.6 Terra Older balanced model for straightforward work.
6. GPT-5.6 Luna Older fast and efficient model.
7. GPT-5.5 Legacy coding model.
8. GPT-5.4 Strong model for everyday coding.
enter select · esc back
@@ -6,4 +6,4 @@ expression: normalized_backend_snapshot(terminal.backend())
" "
"› Ask Codex to do anything "
" "
" GPT-5.4 Pursuing goal (40K / 50K) "
" GPT-5.5 Pursuing goal (40K / 50K) "
@@ -6,4 +6,4 @@ expression: normalized_backend_snapshot(terminal.backend())
" "
"› Ask Codex to do anything "
" "
" GPT-5.4 Goal achieved (2d 23h 42m) "
" GPT-5.5 Goal achieved (2d 23h 42m) "
@@ -2,4 +2,4 @@
source: tui/src/chatwidget/tests/status_and_layout.rs
expression: "chat.last_terminal_title.as_deref().expect(\"terminal title\")"
---
GPT-5.4
GPT-5.5
@@ -4023,7 +4023,7 @@ async fn status_line_model_with_reasoning_includes_fast_for_fast_capable_models(
#[tokio::test]
async fn terminal_title_model_updates_on_model_change_without_manual_refresh() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.local_settings.tui.terminal_title = Some(vec!["model".to_string()]);
chat.refresh_terminal_title();
@@ -4228,7 +4228,7 @@ async fn status_line_goal_active_token_budget_footer_snapshot() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.set_feature_enabled(Feature::Goals, /*enabled*/ true);
chat.show_welcome_banner = false;
chat.local_settings.tui.status_line = Some(vec!["model-name".to_string()]);
@@ -4265,7 +4265,7 @@ async fn status_line_goal_complete_elapsed_footer_snapshot() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.5")).await;
chat.set_feature_enabled(Feature::Goals, /*enabled*/ true);
chat.show_welcome_banner = false;
chat.local_settings.tui.status_line = Some(vec!["model-name".to_string()]);
@@ -4,4 +4,4 @@ expression: "crate::chatwidget::tests::helpers::render_bottom_popup(&chat, 40)"
---
› Ask Codex to do anything
GPT-5.4 default · /tmp/pr… Plan mode
GPT-5.5 default · /tmp/pr… Plan mode