Add max context window model metadata (#18382)

Adds max_context_window to model metadata and routes core context-window
reads through resolved model info. Config model_context_window overrides
are clamped to max_context_window when present; without an override, the
model context_window is used.
This commit is contained in:
Ahmed Ibrahim
2026-04-17 21:48:14 -07:00
committed by GitHub
Unverified
parent e9c70fff3f
commit 5bb193aa88
17 changed files with 330 additions and 6 deletions
@@ -43,6 +43,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
@@ -89,6 +89,7 @@ async fn models_client_hits_models_endpoint() {
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
+1 -1
View File
@@ -239,7 +239,7 @@ pub(super) fn build_stage_one_input_message(
rollout_contents: &str,
) -> anyhow::Result<String> {
let rollout_token_limit = model_info
.context_window
.resolved_context_window()
.and_then(|limit| (limit > 0).then_some(limit))
.map(|limit| limit.saturating_mul(model_info.effective_context_window_percent) / 100)
.map(|limit| (limit.saturating_mul(phase_one::CONTEXT_WINDOW_PERCENT) / 100).max(1))
@@ -41,6 +41,7 @@ fn build_stage_one_input_message_uses_default_limit_when_model_context_window_mi
let input = format!("{}{}{}", "a".repeat(700_000), "middle", "z".repeat(700_000));
let mut model_info = model_info_from_slug("gpt-5.2-codex");
model_info.context_window = None;
model_info.max_context_window = None;
let expected_truncated = truncate_text(
&input,
TruncationPolicy::Tokens(phase_one::DEFAULT_STAGE_ONE_ROLLOUT_TOKEN_LIMIT),
+5 -3
View File
@@ -75,9 +75,11 @@ pub(crate) struct TurnContext {
impl TurnContext {
pub(crate) fn model_context_window(&self) -> Option<i64> {
let effective_context_window_percent = self.model_info.effective_context_window_percent;
self.model_info.context_window.map(|context_window| {
context_window.saturating_mul(effective_context_window_percent) / 100
})
self.model_info
.resolved_context_window()
.map(|context_window| {
context_window.saturating_mul(effective_context_window_percent) / 100
})
}
pub(crate) fn apps_enabled(&self) -> bool {
@@ -97,6 +97,7 @@ fn test_model_info(
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
@@ -914,6 +915,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(large_context_window),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent,
experimental_supported_tools: Vec::new(),
@@ -350,6 +350,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
+2
View File
@@ -666,6 +666,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(128_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
@@ -783,6 +784,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(128_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
+230
View File
@@ -116,6 +116,233 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
Ok(())
}
/// Scenario: the model advertises a default 273k context window and a 400k max
/// context window, and the user explicitly configures 1M. This verifies the
/// runtime turn clamps the override to the advertised max window.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_models_config_context_window_override_clamps_to_max_context_window() -> Result<()> {
skip_if_no_network!(Ok(()));
skip_if_sandbox!(Ok(()));
let server = MockServer::start().await;
let requested_model = "gpt-5.4-test";
let mut remote_model =
test_remote_model("gpt-5.4", ModelVisibility::List, /*priority*/ 1_000);
remote_model.context_window = Some(273_000);
remote_model.max_context_window = Some(400_000);
remote_model.effective_context_window_percent = 100;
mount_models_once(
&server,
ModelsResponse {
models: vec![remote_model],
},
)
.await;
mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let TestCodex {
codex, cwd, config, ..
} = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
config.model = Some(requested_model.to_string());
config.model_context_window = Some(1_000_000);
})
.build(&server)
.await?;
codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "check context window".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: requested_model.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
let turn_started_event = wait_for_event(&codex, |event| {
matches!(
event,
EventMsg::TurnStarted(started)
if started.model_context_window == Some(400_000)
)
})
.await;
let EventMsg::TurnStarted(turn_started) = turn_started_event else {
unreachable!("wait_for_event returned unexpected event");
};
assert_eq!(turn_started.model_context_window, Some(400_000));
Ok(())
}
/// Scenario: the user explicitly configures a context window above the model's
/// max_context_window. This verifies the runtime window is clamped to the max
/// instead of using the oversized config value.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_models_config_override_above_max_uses_max_context_window() -> Result<()> {
skip_if_no_network!(Ok(()));
skip_if_sandbox!(Ok(()));
let server = MockServer::start().await;
let requested_model = "gpt-5.4-test";
let mut remote_model =
test_remote_model("gpt-5.4", ModelVisibility::List, /*priority*/ 1_000);
remote_model.context_window = Some(273_000);
remote_model.max_context_window = Some(400_000);
remote_model.effective_context_window_percent = 100;
mount_models_once(
&server,
ModelsResponse {
models: vec![remote_model],
},
)
.await;
mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let TestCodex {
codex, cwd, config, ..
} = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
config.model = Some(requested_model.to_string());
config.model_context_window = Some(500_000);
})
.build(&server)
.await?;
codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "check context window".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: requested_model.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
let turn_started_event = wait_for_event(&codex, |event| {
matches!(
event,
EventMsg::TurnStarted(started)
if started.model_context_window == Some(400_000)
)
})
.await;
let EventMsg::TurnStarted(turn_started) = turn_started_event else {
unreachable!("wait_for_event returned unexpected event");
};
assert_eq!(turn_started.model_context_window, Some(400_000));
Ok(())
}
/// Scenario: model metadata includes both context_window and max_context_window,
/// but the user did not configure an override. This verifies the runtime keeps
/// using the model's default context_window in the no-override path.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_models_use_context_window_when_config_override_is_absent() -> Result<()> {
skip_if_no_network!(Ok(()));
skip_if_sandbox!(Ok(()));
let server = MockServer::start().await;
let requested_model = "gpt-5.4-test";
let mut remote_model =
test_remote_model("gpt-5.4", ModelVisibility::List, /*priority*/ 1_000);
remote_model.context_window = Some(273_000);
remote_model.max_context_window = Some(400_000);
remote_model.effective_context_window_percent = 100;
mount_models_once(
&server,
ModelsResponse {
models: vec![remote_model],
},
)
.await;
mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let TestCodex {
codex, cwd, config, ..
} = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
config.model = Some(requested_model.to_string());
})
.build(&server)
.await?;
codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "check context window".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: requested_model.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
let turn_started_event = wait_for_event(&codex, |event| {
matches!(
event,
EventMsg::TurnStarted(started)
if started.model_context_window == Some(273_000)
)
})
.await;
let EventMsg::TurnStarted(turn_started) = turn_started_event else {
unreachable!("wait_for_event returned unexpected event");
};
assert_eq!(turn_started.model_context_window, Some(273_000));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -312,6 +539,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
@@ -561,6 +789,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
@@ -1044,6 +1273,7 @@ fn test_remote_model_with_policy(
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
+1
View File
@@ -1020,6 +1020,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
@@ -82,6 +82,7 @@ fn test_model_info(
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
+1
View File
@@ -1365,6 +1365,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
+13
View File
@@ -15,6 +15,7 @@
},
"supports_parallel_tool_calls": true,
"context_window": 272000,
"max_context_window": 272000,
"reasoning_summary_format": "experimental",
"default_reasoning_summary": "none",
"slug": "gpt-5.3-codex",
@@ -88,6 +89,7 @@
},
"supports_parallel_tool_calls": true,
"context_window": 272000,
"max_context_window": 1000000,
"reasoning_summary_format": "experimental",
"default_reasoning_summary": "none",
"slug": "gpt-5.4",
@@ -161,6 +163,7 @@
},
"supports_parallel_tool_calls": true,
"context_window": 272000,
"max_context_window": 272000,
"reasoning_summary_format": "experimental",
"default_reasoning_summary": "auto",
"slug": "gpt-5.2-codex",
@@ -235,6 +238,7 @@
},
"supports_parallel_tool_calls": false,
"context_window": 272000,
"max_context_window": 272000,
"reasoning_summary_format": "experimental",
"default_reasoning_summary": "auto",
"slug": "gpt-5.1-codex-max",
@@ -302,6 +306,7 @@
},
"supports_parallel_tool_calls": false,
"context_window": 272000,
"max_context_window": 272000,
"reasoning_summary_format": "experimental",
"default_reasoning_summary": "auto",
"slug": "gpt-5.1-codex",
@@ -365,6 +370,7 @@
},
"supports_parallel_tool_calls": true,
"context_window": 272000,
"max_context_window": 272000,
"reasoning_summary_format": "none",
"default_reasoning_summary": "auto",
"slug": "gpt-5.2",
@@ -432,6 +438,7 @@
},
"supports_parallel_tool_calls": true,
"context_window": 272000,
"max_context_window": 272000,
"reasoning_summary_format": "none",
"default_reasoning_summary": "auto",
"slug": "gpt-5.1",
@@ -495,6 +502,7 @@
},
"supports_parallel_tool_calls": false,
"context_window": 272000,
"max_context_window": 272000,
"reasoning_summary_format": "experimental",
"default_reasoning_summary": "auto",
"slug": "gpt-5-codex",
@@ -558,6 +566,7 @@
},
"supports_parallel_tool_calls": false,
"context_window": 272000,
"max_context_window": 272000,
"reasoning_summary_format": "none",
"default_reasoning_summary": "auto",
"slug": "gpt-5",
@@ -624,6 +633,7 @@
},
"supports_parallel_tool_calls": false,
"context_window": 128000,
"max_context_window": 128000,
"reasoning_summary_format": "none",
"default_reasoning_summary": "auto",
"slug": "gpt-oss-120b",
@@ -683,6 +693,7 @@
},
"supports_parallel_tool_calls": false,
"context_window": 128000,
"max_context_window": 128000,
"reasoning_summary_format": "none",
"default_reasoning_summary": "auto",
"slug": "gpt-oss-20b",
@@ -743,6 +754,7 @@
},
"supports_parallel_tool_calls": false,
"context_window": 272000,
"max_context_window": 272000,
"reasoning_summary_format": "experimental",
"default_reasoning_summary": "auto",
"slug": "gpt-5.1-codex-mini",
@@ -802,6 +814,7 @@
},
"supports_parallel_tool_calls": false,
"context_window": 272000,
"max_context_window": 272000,
"reasoning_summary_format": "experimental",
"default_reasoning_summary": "auto",
"slug": "gpt-5-codex-mini",
@@ -70,6 +70,7 @@ fn remote_model_with_visibility(
"supports_parallel_tool_calls": false,
"supports_image_detail_original": false,
"context_window": 272_000,
"max_context_window": 272_000,
"experimental_supported_tools": [],
}))
.expect("valid model")
+8 -1
View File
@@ -27,7 +27,13 @@ pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig)
model.supports_reasoning_summaries = true;
}
if let Some(context_window) = config.model_context_window {
model.context_window = Some(context_window);
model.context_window = Some(
model
.max_context_window
.map_or(context_window, |max_context_window| {
context_window.min(max_context_window)
}),
);
}
if let Some(auto_compact_token_limit) = config.model_auto_compact_token_limit {
model.auto_compact_token_limit = Some(auto_compact_token_limit);
@@ -84,6 +90,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
max_context_window: Some(272_000),
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
@@ -43,3 +43,32 @@ fn reasoning_summaries_override_false_is_noop_when_model_is_false() {
assert_eq!(updated, model);
}
#[test]
fn model_context_window_override_clamps_to_max_context_window() {
let mut model = model_info_from_slug("unknown-model");
model.context_window = Some(273_000);
model.max_context_window = Some(400_000);
let config = ModelsManagerConfig {
model_context_window: Some(500_000),
..Default::default()
};
let updated = with_config_overrides(model.clone(), &config);
let mut expected = model;
expected.context_window = Some(400_000);
assert_eq!(updated, expected);
}
#[test]
fn model_context_window_uses_model_value_without_override() {
let mut model = model_info_from_slug("unknown-model");
model.context_window = Some(273_000);
model.max_context_window = Some(400_000);
let config = ModelsManagerConfig::default();
let updated = with_config_overrides(model.clone(), &config);
assert_eq!(updated, model);
}
+32 -1
View File
@@ -277,6 +277,9 @@ pub struct ModelInfo {
pub supports_image_detail_original: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_window: Option<i64>,
/// Maximum context window allowed for config overrides.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_context_window: Option<i64>,
/// Token threshold for automatic compaction. When omitted, core derives it
/// from `context_window` (90%). When provided, core clamps it to 90% of the
/// context window when available.
@@ -300,9 +303,13 @@ pub struct ModelInfo {
}
impl ModelInfo {
pub fn resolved_context_window(&self) -> Option<i64> {
self.context_window.or(self.max_context_window)
}
pub fn auto_compact_token_limit(&self) -> Option<i64> {
let context_limit = self
.context_window
.resolved_context_window()
.map(|context_window| (context_window * 9) / 10);
let config_limit = self.auto_compact_token_limit;
if let Some(context_limit) = context_limit {
@@ -555,6 +562,7 @@ mod tests {
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: None,
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: vec![],
@@ -780,6 +788,29 @@ mod tests {
assert!(!model.supports_search_tool);
}
#[test]
fn resolved_context_window_prefers_context_window() {
let model = ModelInfo {
context_window: Some(273_000),
max_context_window: Some(400_000),
..test_model(/*spec*/ None)
};
assert_eq!(model.resolved_context_window(), Some(273_000));
}
#[test]
fn resolved_context_window_falls_back_to_max_context_window() {
let model = ModelInfo {
context_window: None,
max_context_window: Some(400_000),
..test_model(/*spec*/ None)
};
assert_eq!(model.resolved_context_window(), Some(400_000));
assert_eq!(model.auto_compact_token_limit(), Some(360_000));
}
#[test]
fn model_preset_preserves_availability_nux() {
let preset = ModelPreset::from(ModelInfo {