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
This commit is contained in:
jif
2026-09-11 17:46:55 +00:00
committed by copyberry
parent 33bdf976cc
commit 4dcce4f0c4
5 changed files with 102 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -64,6 +64,9 @@ async fn history_images_reach_the_next_model_request() -> Result<(), Box<dyn std
);
let test = test_codex()
.with_auth(auth)
.with_model_info_override("gpt-5.5", |model_info| {
model_info.supports_experimental_context = true;
})
.with_extensions(Arc::new(extensions.build()))
.with_config(|config| {
config.model_provider.name = "OpenAI".to_string();