diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index bf2aeb4161..7edc90337a 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -539,10 +539,7 @@ 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 - }) + effective_model_context_window(&self.model_info) } pub(crate) fn resolve_path(&self, path: Option) -> PathBuf { @@ -684,6 +681,13 @@ impl SessionConfiguration { } } +fn effective_model_context_window(model_info: &ModelInfo) -> Option { + let effective_context_window_percent = model_info.effective_context_window_percent; + model_info + .context_window + .map(|context_window| context_window.saturating_mul(effective_context_window_percent) / 100) +} + #[derive(Default, Clone)] pub(crate) struct SessionSettingsUpdate { pub(crate) cwd: Option, @@ -1044,10 +1048,14 @@ impl Session { } }; session_configuration.thread_name = thread_name.clone(); + let model_info = models_manager + .get_model_info(session_configuration.collaboration_mode.model(), &config) + .await; let state = SessionState::new( session_configuration.clone(), conversation_id.to_string(), config.codex_home.clone(), + effective_model_context_window(&model_info), ); let services = SessionServices { @@ -2036,6 +2044,7 @@ impl Session { let mut history = ContextManager::new( Arc::new(self.conversation_id.to_string()), Arc::new(turn_context.config.codex_home.clone()), + turn_context.model_context_window(), ); for item in rollout_items { match item { @@ -5529,6 +5538,7 @@ mod tests { session_configuration, conversation_id.to_string(), config.codex_home.clone(), + effective_model_context_window(&model_info), ); let initial = RateLimitSnapshot { primary: Some(RateLimitWindow { @@ -5617,6 +5627,7 @@ mod tests { session_configuration, conversation_id.to_string(), config.codex_home.clone(), + effective_model_context_window(&model_info), ); let initial = RateLimitSnapshot { primary: Some(RateLimitWindow { @@ -5906,6 +5917,7 @@ mod tests { session_configuration.clone(), conversation_id.to_string(), config.codex_home.clone(), + effective_model_context_window(&model_info), ); mark_state_initial_context_seeded(&mut state); let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone())); @@ -6042,6 +6054,7 @@ mod tests { session_configuration.clone(), conversation_id.to_string(), config.codex_home.clone(), + effective_model_context_window(&model_info), ); mark_state_initial_context_seeded(&mut state); let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone())); @@ -6544,6 +6557,7 @@ mod tests { let mut live_history = ContextManager::new( Arc::new(session.conversation_id.to_string()), Arc::new(turn_context.config.codex_home.clone()), + turn_context.model_context_window(), ); let initial_context = session.build_initial_context(turn_context).await; diff --git a/codex-rs/core/src/context_manager/context_discoverable.rs b/codex-rs/core/src/context_manager/context_discoverable.rs index 0a5f490cf1..8d3499f1f8 100644 --- a/codex-rs/core/src/context_manager/context_discoverable.rs +++ b/codex-rs/core/src/context_manager/context_discoverable.rs @@ -92,8 +92,7 @@ impl ContextDiscoverable for Message { }; let estimated_tokens = i64::try_from(approx_token_count(&user_text)).unwrap_or(i64::MAX); - let offload_threshold = context_window_tokens.saturating_mul(95) / 100; - if current_usage_tokens.saturating_add(estimated_tokens) <= offload_threshold { + if current_usage_tokens.saturating_add(estimated_tokens) <= context_window_tokens { return ResponseItem::Message(self.clone()); } diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 93ad67e87a..bfb19c5d6d 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -29,10 +29,14 @@ pub(crate) struct ContextManager { } impl ContextManager { - pub(crate) fn new(thread_id: Arc, codex_home: Arc) -> Self { + pub(crate) fn new( + thread_id: Arc, + codex_home: Arc, + model_context_window: Option, + ) -> Self { Self { items: Vec::new(), - token_info: TokenUsageInfo::new_or_append(&None, &None, None), + token_info: TokenUsageInfo::new_or_append(&None, &None, model_context_window), thread_id, codex_home, } diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 64174cd56c..5925f68175 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -39,6 +39,7 @@ fn create_history_with_items(items: Vec) -> ContextManager { let mut h = ContextManager::new( Arc::new("test-thread".to_string()), Arc::new(PathBuf::from("/tmp/test-codex-home")), + None, ); // Use a generous but fixed token budget; tests only rely on truncation // behavior, not on a specific model's token limit. @@ -139,6 +140,7 @@ fn filters_non_api_messages() { let mut h = ContextManager::new( Arc::new("test-thread".to_string()), Arc::new(PathBuf::from("/tmp/test-codex-home")), + None, ); let policy = TruncationPolicy::Tokens(10_000); // System message is not API messages; Other is ignored. @@ -622,6 +624,7 @@ fn record_items_truncates_function_call_output_content() { let mut history = ContextManager::new( Arc::new("test-thread".to_string()), Arc::new(PathBuf::from("/tmp/test-codex-home")), + None, ); // Any reasonably small token budget works; the test only cares that // truncation happens and the marker is present. @@ -664,6 +667,7 @@ fn record_items_truncates_custom_tool_call_output_content() { let mut history = ContextManager::new( Arc::new("test-thread".to_string()), Arc::new(PathBuf::from("/tmp/test-codex-home")), + None, ); let policy = TruncationPolicy::Tokens(1_000); let line = "custom output that is very long\n"; @@ -700,6 +704,7 @@ fn record_items_respects_custom_token_limit() { let mut history = ContextManager::new( Arc::new("test-thread".to_string()), Arc::new(PathBuf::from("/tmp/test-codex-home")), + None, ); let policy = TruncationPolicy::Tokens(10); let long_output = "tokenized content repeated many times ".repeat(200); @@ -733,6 +738,7 @@ fn record_items_with_discoverability_offloads_large_user_message() { let mut history = ContextManager::new( Arc::new("thread-1".to_string()), Arc::new(codex_home.path().to_path_buf()), + None, ); let policy = TruncationPolicy::Tokens(10_000); let large_text = "large user prompt ".repeat(200); @@ -766,6 +772,7 @@ fn record_items_with_discoverability_keeps_small_user_message_in_history() { let mut history = ContextManager::new( Arc::new("thread-2".to_string()), Arc::new(codex_home.path().to_path_buf()), + None, ); let policy = TruncationPolicy::Tokens(10_000); let item = user_input_text_msg("small prompt"); @@ -782,6 +789,7 @@ fn record_items_with_discoverability_does_not_change_non_user_messages() { let mut history = ContextManager::new( Arc::new("thread-3".to_string()), Arc::new(codex_home.path().to_path_buf()), + None, ); let policy = TruncationPolicy::Tokens(10_000); let item = assistant_msg(&"assistant reply ".repeat(300)); @@ -798,6 +806,7 @@ fn record_items_with_discoverability_ignores_image_only_user_messages() { let mut history = ContextManager::new( Arc::new("thread-4".to_string()), Arc::new(codex_home.path().to_path_buf()), + None, ); let policy = TruncationPolicy::Tokens(10_000); let item = ResponseItem::Message(codex_protocol::models::Message { @@ -822,6 +831,7 @@ fn record_items_with_discoverability_offloads_once_above_ninety_five_percent_win let mut history = ContextManager::new( Arc::new("thread-95".to_string()), Arc::new(codex_home.path().to_path_buf()), + None, ); let policy = TruncationPolicy::Tokens(10_000); let text = "near context threshold ".repeat(100); diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index d6c3ee4eaf..7d87312f4f 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -36,8 +36,13 @@ impl SessionState { session_configuration: SessionConfiguration, thread_id: String, codex_home: PathBuf, + model_context_window: Option, ) -> Self { - let history = ContextManager::new(Arc::new(thread_id), Arc::new(codex_home)); + let history = ContextManager::new( + Arc::new(thread_id), + Arc::new(codex_home), + model_context_window, + ); Self { session_configuration, history,