From 5bb193aa88fef0f5ef3fbbd2c6253ba93d3f6521 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Fri, 17 Apr 2026 21:48:14 -0700 Subject: [PATCH] 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. --- .../app-server/tests/common/models_cache.rs | 1 + .../codex-api/tests/models_integration.rs | 1 + codex-rs/core/src/memories/prompts.rs | 2 +- codex-rs/core/src/memories/prompts_tests.rs | 1 + codex-rs/core/src/session/turn_context.rs | 8 +- codex-rs/core/tests/suite/model_switching.rs | 2 + codex-rs/core/tests/suite/models_cache_ttl.rs | 1 + codex-rs/core/tests/suite/personality.rs | 2 + codex-rs/core/tests/suite/remote_models.rs | 230 ++++++++++++++++++ codex-rs/core/tests/suite/rmcp_client.rs | 1 + .../tests/suite/spawn_agent_description.rs | 1 + codex-rs/core/tests/suite/view_image.rs | 1 + codex-rs/models-manager/models.json | 13 + codex-rs/models-manager/src/manager_tests.rs | 1 + codex-rs/models-manager/src/model_info.rs | 9 +- .../models-manager/src/model_info_tests.rs | 29 +++ codex-rs/protocol/src/openai_models.rs | 33 ++- 17 files changed, 330 insertions(+), 6 deletions(-) diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index f38b6fd99..557fa5620 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -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(), diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index 96de0b2ce..9f95c9441 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -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(), diff --git a/codex-rs/core/src/memories/prompts.rs b/codex-rs/core/src/memories/prompts.rs index fc85917c9..9425a5380 100644 --- a/codex-rs/core/src/memories/prompts.rs +++ b/codex-rs/core/src/memories/prompts.rs @@ -239,7 +239,7 @@ pub(super) fn build_stage_one_input_message( rollout_contents: &str, ) -> anyhow::Result { 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)) diff --git a/codex-rs/core/src/memories/prompts_tests.rs b/codex-rs/core/src/memories/prompts_tests.rs index 959bb6911..d0ce447c4 100644 --- a/codex-rs/core/src/memories/prompts_tests.rs +++ b/codex-rs/core/src/memories/prompts_tests.rs @@ -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), diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index f647c6495..dd86804ee 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -75,9 +75,11 @@ pub(crate) struct TurnContext { impl TurnContext { pub(crate) fn model_context_window(&self) -> Option { 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 { diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 2b50a755e..10b97721a 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -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(), diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 9dfc32fc7..bb0ecc85f 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -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(), diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 2bcd07418..53cb8f9bf 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -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(), diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 80e725ab8..6feb18a2e 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -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(), diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 8c7e7133a..733edf063 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -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(), diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index 8c131b16b..1d2bd1ed0 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -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(), diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 47254ab17..891c4d238 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -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(), diff --git a/codex-rs/models-manager/models.json b/codex-rs/models-manager/models.json index 90b53216a..fd089d899 100644 --- a/codex-rs/models-manager/models.json +++ b/codex-rs/models-manager/models.json @@ -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", diff --git a/codex-rs/models-manager/src/manager_tests.rs b/codex-rs/models-manager/src/manager_tests.rs index 23de81781..a70f46567 100644 --- a/codex-rs/models-manager/src/manager_tests.rs +++ b/codex-rs/models-manager/src/manager_tests.rs @@ -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") diff --git a/codex-rs/models-manager/src/model_info.rs b/codex-rs/models-manager/src/model_info.rs index fefec50ce..8e8abae54 100644 --- a/codex-rs/models-manager/src/model_info.rs +++ b/codex-rs/models-manager/src/model_info.rs @@ -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(), diff --git a/codex-rs/models-manager/src/model_info_tests.rs b/codex-rs/models-manager/src/model_info_tests.rs index ec5b6ed85..70ad3da8d 100644 --- a/codex-rs/models-manager/src/model_info_tests.rs +++ b/codex-rs/models-manager/src/model_info_tests.rs @@ -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); +} diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 3339e674b..41275e6a6 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -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, + /// Maximum context window allowed for config overrides. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_context_window: Option, /// 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 { + self.context_window.or(self.max_context_window) + } + pub fn auto_compact_token_limit(&self) -> Option { 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 {