From 4dcce4f0c47e0183e4b05ffcbb38b4fffb8b8042 Mon Sep 17 00:00:00 2001 From: jif Date: Fri, 11 Sep 2026 17:46:55 +0000 Subject: [PATCH] Reject token-budget history notes for unsupported starting models (#44883) ## What changed After resolving startup configuration and model defaults, reject `features.token_budget.use_history_notes_extension` when the starting model lacks `supports_experimental_context`. Return an error directing users to disable the option or select a compatible model. ## Testing Add startup coverage for explicit configuration and model defaults, verifying rejection for unsupported models and successful activation for supported models and standalone token budgets. Update history-notes test fixtures to declare experimental context support. GitOrigin-RevId: abf1a024efc5acf97cfc858fbb93821363769dc0 --- .../tests/suite/v2/history_notes_extension.rs | 13 ++++ codex-rs/core/src/session/mod.rs | 10 +++ codex-rs/core/tests/suite/guardian_review.rs | 1 + codex-rs/core/tests/suite/token_budget.rs | 75 +++++++++++++++++++ .../history-notes/tests/image_model_input.rs | 3 + 5 files changed, 102 insertions(+) diff --git a/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs b/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs index 8d851ddeef..d4b0edcd47 100644 --- a/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs +++ b/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs @@ -112,8 +112,20 @@ async fn app_server_uses_configured_notes_backend_for_context_window_hints( .await; let codex_home = TempDir::new()?; + let config = load_default_config_for_test(&codex_home).await; + let mut model = codex_core::test_support::construct_model_info_offline("mock-model", &config); + model.supports_experimental_context = true; + let catalog_path = codex_home.path().join("models.json"); + std::fs::write( + &catalog_path, + serde_json::to_vec(&json!({"models": [model]}))?, + )?; MockResponsesConfig::new(&server.uri()) .with_root_config(&format!("chatgpt_base_url = \"{}\"", server.uri())) + .with_root_config(&format!( + "model_catalog_json = {}", + serde_json::to_string(&catalog_path)? + )) .with_model_provider("openai-custom") .with_provider_name("OpenAI") .with_provider_base_url(&format!("{}/backend-api/codex", server.uri())) @@ -321,6 +333,7 @@ async fn history_notes_and_async_message_emit_control_tool_analytics() -> Result let codex_home = TempDir::new()?; let config = load_default_config_for_test(&codex_home).await; let mut model = codex_core::test_support::construct_model_info_offline("mock-model", &config); + model.supports_experimental_context = true; model .experimental_supported_tools .push("send_user_message_async".to_string()); diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index d78802086b..3bb68a8839 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -707,6 +707,16 @@ impl Session { &model_info, )?; token_budget::apply_model_defaults(Arc::make_mut(&mut config), &model_info); + if config + .token_budget + .as_ref() + .is_some_and(|token_budget| token_budget.use_history_notes_extension) + && !model_info.supports_experimental_context + { + return Err(CodexErr::InvalidRequest(format!( + "features.token_budget.use_history_notes_extension is not supported by model `{model}`; disable it or select a model that supports experimental context" + ))); + } } let configured_config = Arc::clone(&config); let multi_agent_version = config.multi_agent_version_override().or_else(|| { diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index 466bb77b7e..39325ce757 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -308,6 +308,7 @@ async fn guardian_review_compacts_with_summary_despite_parent_token_budget( let mut builder = test_codex() .with_model_info_override("gpt-5.5", |model| { model.auto_review_model_override = Some(model.slug.clone()); + model.supports_experimental_context = true; model .model_messages .as_mut() diff --git a/codex-rs/core/tests/suite/token_budget.rs b/codex-rs/core/tests/suite/token_budget.rs index e8add8d490..b0f3dad55a 100644 --- a/codex-rs/core/tests/suite/token_budget.rs +++ b/codex-rs/core/tests/suite/token_budget.rs @@ -364,6 +364,80 @@ async fn experimental_context_requires_capable_model_and_codex_backend( Ok(()) } +#[test_case(false, true, false; "explicit_history_notes_unsupported")] +#[test_case(false, true, true; "explicit_history_notes_supported")] +#[test_case(false, false, false; "explicit_standalone_token_budget")] +#[test_case(true, true, false; "model_default_history_notes_unsupported")] +#[test_case(true, true, true; "model_default_history_notes_supported")] +#[test_case(true, false, false; "model_default_standalone_token_budget")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn token_budget_history_notes_requires_capable_starting_model( + use_model_defaults: bool, + use_history_notes: bool, + supports_context: bool, +) -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let backend_url = format!("{}/backend-api/codex", server.uri()); + let result = test_codex() + .with_auth(CodexAuth::from_external_chatgpt_tokens( + "header.e30.signature", + "account-123", + Some("plus"), + )?) + .with_pre_build_hook(move |home| { + let config = if use_model_defaults { + "[features.context_management]\nexperimental_mode = false\n".to_string() + } else { + format!( + "[features.context_management]\nexperimental_mode = false\n[features.token_budget]\nenabled = true\nuse_history_notes_extension = {use_history_notes}\n" + ) + }; + std::fs::write(home.join("config.toml"), config) + .expect("write token-budget configuration"); + }) + .with_model_info_override("gpt-5.2", move |model_info| { + model_info.supports_experimental_context = supports_context; + let mut defaults = model_token_budget_config(); + defaults.enabled = use_model_defaults; + defaults.use_history_notes_extension = use_history_notes; + model_info + .model_messages + .as_mut() + .expect("bundled model should have model messages") + .token_budget = Some(defaults); + }) + .with_config(move |config| { + config.model_provider.base_url = Some(backend_url); + config.model_context_window = Some(CONFIGURED_CONTEXT_WINDOW); + }) + .build_with_auto_env(&server) + .await; + + if use_history_notes && !supports_context { + let error = result + .err() + .expect("unsupported history/notes must fail at startup"); + assert!(error.to_string().contains( + "features.token_budget.use_history_notes_extension is not supported by model `gpt-5.2`" + )); + return Ok(()); + } + + let test = result?; + let response = mount_sse_once(&server, sse_completed("resp-1")).await; + test.submit_turn("inspect token-budget activation").await?; + let request = response.single_request(); + assert!( + tool_names(&request) + .iter() + .any(|name| name == "new_context") + ); + assert_eq!(token_budget_contexts(&request).len(), 1); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn token_budget_uses_model_message_defaults() -> Result<()> { skip_if_no_network!(Ok(())); @@ -376,6 +450,7 @@ async fn token_budget_uses_model_message_defaults() -> Result<()> { let expected_guidance = model_defaults.guidance_message.clone(); let test = test_codex() .with_model_info_override("gpt-5.2", move |model_info| { + model_info.supports_experimental_context = true; model_info .model_messages .as_mut() diff --git a/codex-rs/ext/history-notes/tests/image_model_input.rs b/codex-rs/ext/history-notes/tests/image_model_input.rs index 2a6a202546..be78b52a47 100644 --- a/codex-rs/ext/history-notes/tests/image_model_input.rs +++ b/codex-rs/ext/history-notes/tests/image_model_input.rs @@ -64,6 +64,9 @@ async fn history_images_reach_the_next_model_request() -> Result<(), Box