mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
Fallback pre-turn compaction on context overflow
This commit is contained in:
@@ -55,6 +55,17 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
|
||||
}
|
||||
|
||||
if status == http::StatusCode::BAD_REQUEST {
|
||||
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&body_text)
|
||||
&& matches!(
|
||||
value
|
||||
.get("error")
|
||||
.and_then(|error| error.get("code"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("context_length_exceeded")
|
||||
)
|
||||
{
|
||||
return CodexErr::ContextWindowExceeded;
|
||||
}
|
||||
if body_text
|
||||
.contains("The image data you provided does not represent a valid image")
|
||||
{
|
||||
@@ -130,6 +141,24 @@ mod tests {
|
||||
assert!(matches!(err, CodexErr::ServerOverloaded));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_api_error_maps_context_length_exceeded_from_400_body() {
|
||||
let body = serde_json::json!({
|
||||
"error": {
|
||||
"code": "context_length_exceeded",
|
||||
"message": "Your input exceeds the context window of this model."
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
let err = map_api_error(ApiError::Transport(TransportError::Http {
|
||||
status: http::StatusCode::BAD_REQUEST,
|
||||
url: Some("http://localhost/v1/responses/compact".to_string()),
|
||||
headers: Some(HeaderMap::new()),
|
||||
body: Some(body),
|
||||
}));
|
||||
assert!(matches!(err, CodexErr::ContextWindowExceeded));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_api_error_maps_server_overloaded_from_503_body() {
|
||||
let body = serde_json::json!({
|
||||
|
||||
+121
-42
@@ -4718,9 +4718,7 @@ enum PreTurnCompactionOutcome {
|
||||
CompactedWithIncomingItems,
|
||||
/// Pre-turn compaction succeeded without incoming turn items
|
||||
/// (incoming user message should be appended after the compaction summary).
|
||||
/// This compaction strategy is currently out of distribution for our compaction model,
|
||||
/// but is planned to be trained on in the future.
|
||||
#[cfg(test)]
|
||||
/// This compaction strategy is currently out of distribution for our compaction model.
|
||||
CompactedWithoutIncomingItems,
|
||||
}
|
||||
|
||||
@@ -4752,10 +4750,6 @@ async fn persist_pre_turn_items_for_compaction_outcome(
|
||||
sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), input, response_item)
|
||||
.await;
|
||||
}
|
||||
// TODO(ccunningham): Followup PR will use compacting excluding incoming items as a fallback
|
||||
// (even though it is out of distribution for current models).
|
||||
// Also future models may prefer compacting pre-turn history without incoming turn items.
|
||||
#[cfg(test)]
|
||||
PreTurnCompactionOutcome::CompactedWithoutIncomingItems => {
|
||||
// Reseed canonical initial context above the incoming user message.
|
||||
let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
|
||||
@@ -4769,7 +4763,10 @@ async fn persist_pre_turn_items_for_compaction_outcome(
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs pre-turn auto-compaction with incoming turn context + user message included.
|
||||
/// Runs pre-turn auto-compaction with incoming turn context + user message included first.
|
||||
///
|
||||
/// If the include-incoming compaction attempt fails with `ContextWindowExceeded`, this function
|
||||
/// retries once with a history-only compaction strategy that excludes incoming turn items.
|
||||
///
|
||||
/// On failure this function emits any user-visible error event itself and returns `Err(())` as a
|
||||
/// sentinel so callers can stop the turn without duplicating error messaging logic.
|
||||
@@ -4802,43 +4799,125 @@ async fn run_pre_turn_auto_compaction_if_needed(
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = compact_result {
|
||||
if !pre_turn_context_items.is_empty() {
|
||||
// Preserve model-visible settings updates even when pre-turn compaction fails
|
||||
// before we can persist turn input.
|
||||
sess.record_conversation_items(turn_context, pre_turn_context_items)
|
||||
.await;
|
||||
match compact_result {
|
||||
Ok(()) => {}
|
||||
Err(CodexErr::ContextWindowExceeded) => {
|
||||
let fallback_result = run_auto_compact(
|
||||
sess,
|
||||
turn_context,
|
||||
AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
|
||||
TurnContextReinjection::Skip,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
match fallback_result {
|
||||
Ok(()) => {
|
||||
let has_user_turn_boundary_after_compaction = sess
|
||||
.clone_history()
|
||||
.await
|
||||
.raw_items()
|
||||
.iter()
|
||||
.any(crate::context_manager::is_user_turn_boundary);
|
||||
if !has_user_turn_boundary_after_compaction {
|
||||
if !pre_turn_context_items.is_empty() {
|
||||
// Preserve model-visible settings updates even when pre-turn
|
||||
// compaction cannot proceed before we can persist turn input.
|
||||
sess.record_conversation_items(turn_context, pre_turn_context_items)
|
||||
.await;
|
||||
}
|
||||
let message = format!(
|
||||
"Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
|
||||
);
|
||||
let event = EventMsg::Error(
|
||||
CodexErr::ContextWindowExceeded.to_error_event(Some(message)),
|
||||
);
|
||||
sess.send_event(turn_context, event).await;
|
||||
return Err(());
|
||||
}
|
||||
return Ok(PreTurnCompactionOutcome::CompactedWithoutIncomingItems);
|
||||
}
|
||||
Err(err) => {
|
||||
if !pre_turn_context_items.is_empty() {
|
||||
// Preserve model-visible settings updates even when pre-turn compaction
|
||||
// fails before we can persist turn input.
|
||||
sess.record_conversation_items(turn_context, pre_turn_context_items)
|
||||
.await;
|
||||
}
|
||||
if matches!(err, CodexErr::Interrupted) {
|
||||
return Err(());
|
||||
}
|
||||
let event = match err {
|
||||
CodexErr::ContextWindowExceeded => {
|
||||
error!(
|
||||
turn_id = %turn_context.sub_id,
|
||||
auto_compact_callsite = ?AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
|
||||
incoming_items_tokens_estimate,
|
||||
auto_compact_limit,
|
||||
reason = "pre-turn compaction exceeded context window after fallback",
|
||||
"incoming user/context is too large for pre-turn auto-compaction flow"
|
||||
);
|
||||
let message = format!(
|
||||
"Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
|
||||
);
|
||||
EventMsg::Error(
|
||||
CodexErr::ContextWindowExceeded.to_error_event(Some(message)),
|
||||
)
|
||||
}
|
||||
other => {
|
||||
let compact_error_prefix =
|
||||
if should_use_remote_compact_task(&turn_context.provider) {
|
||||
"Error running remote compact task"
|
||||
} else {
|
||||
"Error running local compact task"
|
||||
};
|
||||
EventMsg::Error(
|
||||
other.to_error_event(Some(compact_error_prefix.to_string())),
|
||||
)
|
||||
}
|
||||
};
|
||||
sess.send_event(turn_context, event).await;
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(err, CodexErr::Interrupted) {
|
||||
Err(err) => {
|
||||
if !pre_turn_context_items.is_empty() {
|
||||
// Preserve model-visible settings updates even when pre-turn compaction fails
|
||||
// before we can persist turn input.
|
||||
sess.record_conversation_items(turn_context, pre_turn_context_items)
|
||||
.await;
|
||||
}
|
||||
if matches!(err, CodexErr::Interrupted) {
|
||||
return Err(());
|
||||
}
|
||||
let event = match err {
|
||||
CodexErr::ContextWindowExceeded => {
|
||||
error!(
|
||||
turn_id = %turn_context.sub_id,
|
||||
auto_compact_callsite = ?AutoCompactCallsite::PreTurnIncludingIncomingUserMessage,
|
||||
incoming_items_tokens_estimate,
|
||||
auto_compact_limit,
|
||||
reason = "pre-turn compaction exceeded context window",
|
||||
"incoming user/context is too large for pre-turn auto-compaction flow"
|
||||
);
|
||||
let message = format!(
|
||||
"Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
|
||||
);
|
||||
EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message)))
|
||||
}
|
||||
other => {
|
||||
let compact_error_prefix =
|
||||
if should_use_remote_compact_task(&turn_context.provider) {
|
||||
"Error running remote compact task"
|
||||
} else {
|
||||
"Error running local compact task"
|
||||
};
|
||||
EventMsg::Error(other.to_error_event(Some(compact_error_prefix.to_string())))
|
||||
}
|
||||
};
|
||||
sess.send_event(turn_context, event).await;
|
||||
return Err(());
|
||||
}
|
||||
let event = match err {
|
||||
CodexErr::ContextWindowExceeded => {
|
||||
error!(
|
||||
turn_id = %turn_context.sub_id,
|
||||
auto_compact_callsite = ?AutoCompactCallsite::PreTurnIncludingIncomingUserMessage,
|
||||
incoming_items_tokens_estimate,
|
||||
auto_compact_limit,
|
||||
reason = "pre-turn compaction exceeded context window",
|
||||
"incoming user/context is too large for pre-turn auto-compaction flow"
|
||||
);
|
||||
let message = format!(
|
||||
"Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
|
||||
);
|
||||
EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message)))
|
||||
}
|
||||
other => {
|
||||
let compact_error_prefix = if should_use_remote_compact_task(&turn_context.provider)
|
||||
{
|
||||
"Error running remote compact task"
|
||||
} else {
|
||||
"Error running local compact task"
|
||||
};
|
||||
EventMsg::Error(other.to_error_event(Some(compact_error_prefix.to_string())))
|
||||
}
|
||||
};
|
||||
sess.send_event(turn_context, event).await;
|
||||
return Err(());
|
||||
}
|
||||
|
||||
// If compaction no-oped because there was no user-turn boundary even after including incoming
|
||||
|
||||
@@ -5,7 +5,6 @@ use codex_core::built_in_model_providers;
|
||||
use codex_core::compact::SUMMARIZATION_PROMPT;
|
||||
use codex_core::compact::SUMMARY_PREFIX;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::ItemCompletedEvent;
|
||||
@@ -3316,7 +3315,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
|
||||
]);
|
||||
let mut responses = vec![first_turn];
|
||||
responses.extend(
|
||||
(0..6).map(|_| {
|
||||
(0..12).map(|_| {
|
||||
sse_failed(
|
||||
"compact-failed",
|
||||
"context_length_exceeded",
|
||||
@@ -3373,7 +3372,6 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
|
||||
requests.len() >= 2,
|
||||
"expected first turn and at least one compaction request"
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"pre_turn_compaction_context_window_exceeded_shapes",
|
||||
format_labeled_requests_snapshot(
|
||||
|
||||
@@ -1561,7 +1561,17 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
|
||||
)
|
||||
.await;
|
||||
|
||||
let compact_mock = responses::mount_compact_response_once(
|
||||
let include_attempt_mock = responses::mount_compact_response_once(
|
||||
harness.server(),
|
||||
ResponseTemplate::new(400).set_body_json(serde_json::json!({
|
||||
"error": {
|
||||
"code": "context_length_exceeded",
|
||||
"message": "Your input exceeds the context window of this model. Please adjust your input and try again."
|
||||
}
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
let fallback_attempt_mock = responses::mount_compact_response_once(
|
||||
harness.server(),
|
||||
ResponseTemplate::new(400).set_body_json(serde_json::json!({
|
||||
"error": {
|
||||
@@ -1607,7 +1617,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
|
||||
.await;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
assert_eq!(compact_mock.requests().len(), 1);
|
||||
let requests = responses_mock.requests();
|
||||
assert_eq!(
|
||||
requests.len(),
|
||||
@@ -1619,15 +1628,45 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
|
||||
"expected turn to stop after compaction failure"
|
||||
);
|
||||
|
||||
let include_attempt_request = compact_mock.single_request();
|
||||
let mut compact_requests = include_attempt_mock.requests();
|
||||
compact_requests.extend(fallback_attempt_mock.requests());
|
||||
assert_eq!(
|
||||
compact_requests.len(),
|
||||
2,
|
||||
"expected include-incoming and fallback compact attempts"
|
||||
);
|
||||
let include_attempt_request = compact_requests
|
||||
.iter()
|
||||
.find(|request| request.body_contains_text("USER_TWO"))
|
||||
.cloned()
|
||||
.expect("expected one compact attempt containing incoming user message");
|
||||
let fallback_attempt_request = compact_requests
|
||||
.iter()
|
||||
.find(|request| !request.body_contains_text("USER_TWO"))
|
||||
.cloned()
|
||||
.expect("expected one fallback compact attempt excluding incoming user message");
|
||||
assert!(
|
||||
include_attempt_request.body_contains_text("USER_TWO"),
|
||||
"include-incoming pre-turn remote compaction attempt should include incoming user message"
|
||||
);
|
||||
assert!(
|
||||
!fallback_attempt_request.body_contains_text("USER_TWO"),
|
||||
"fallback pre-turn remote compaction attempt should exclude incoming user message"
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
"remote_pre_turn_compaction_context_window_exceeded_shapes",
|
||||
format_labeled_requests_snapshot(
|
||||
"Remote pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.",
|
||||
&[(
|
||||
"Remote Compaction Request (Incoming User Excluded)",
|
||||
&include_attempt_request
|
||||
),]
|
||||
"Remote pre-turn auto-compaction context-window failure: initial compaction includes incoming user content, fallback compaction excludes incoming user content, and the turn errors.",
|
||||
&[
|
||||
(
|
||||
"Remote Compaction Request (Incoming User Included)",
|
||||
&include_attempt_request
|
||||
),
|
||||
(
|
||||
"Remote Compaction Request (Fallback Incoming User Excluded)",
|
||||
&fallback_attempt_request
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
|
||||
+10
-3
@@ -1,13 +1,20 @@
|
||||
---
|
||||
source: core/tests/suite/compact_remote.rs
|
||||
expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.\",\n&[(\"Remote Compaction Request (Incoming User Excluded)\",\n&include_attempt_request),])"
|
||||
expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction context-window failure: initial compaction includes incoming user content, fallback compaction excludes incoming user content, and the turn errors.\",\n&[(\"Remote Compaction Request (Incoming User Included)\",\n&include_attempt_request),\n(\"Remote Compaction Request (Fallback Incoming User Excluded)\",\n&fallback_attempt_request),])"
|
||||
---
|
||||
Scenario: Remote pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.
|
||||
Scenario: Remote pre-turn auto-compaction context-window failure: initial compaction includes incoming user content, fallback compaction excludes incoming user content, and the turn errors.
|
||||
|
||||
## Remote Compaction Request (Incoming User Excluded)
|
||||
## Remote Compaction Request (Incoming User Included)
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/user:<AGENTS_MD>
|
||||
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
03:message/user:USER_ONE
|
||||
04:message/assistant:REMOTE_FIRST_REPLY
|
||||
05:message/user:USER_TWO
|
||||
|
||||
## Remote Compaction Request (Fallback Incoming User Excluded)
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/user:<AGENTS_MD>
|
||||
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
03:message/user:USER_ONE
|
||||
04:message/assistant:REMOTE_FIRST_REPLY
|
||||
|
||||
Reference in New Issue
Block a user