mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Handle context length errors consistently across response transports (#60482)
## Why HTTP 400 responses with `context_length_exceeded` need to reach context-window recovery so manual compaction can retry with less history. Streaming context-window failures should end the stream immediately. ## What changed - Map `context_length_exceeded` through the shared terminal provider error parser for HTTP 400 responses and events without an HTTP status. - Return SSE context-window failures immediately. - Preserve retryable handling for WebSocket errors carrying HTTP 503, even when their code is `context_length_exceeded`. ## Testing Add coverage for WebSocket status handling and immediate SSE termination with retry headers. Update the manual compaction test to verify recovery from an HTTP 400 context-window error by dropping one history item and retrying. GitOrigin-RevId: 56d8c831c2fad2ca1318318a3b4258fcc516d689
This commit is contained in:
@@ -1326,6 +1326,32 @@ mod tests {
|
||||
assert!(body.contains("Model does not support image inputs"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapped_context_length_error_respects_http_status() {
|
||||
for status in [400, 503] {
|
||||
let payload = json!({
|
||||
"type": "error",
|
||||
"status": status,
|
||||
"error": {"code": "context_length_exceeded"}
|
||||
})
|
||||
.to_string();
|
||||
let event = parse_wrapped_websocket_error_event(&payload).unwrap();
|
||||
let error = map_wrapped_websocket_error_event(event, payload).unwrap();
|
||||
let error = crate::api_bridge::map_api_error(error);
|
||||
assert_eq!(
|
||||
matches!(
|
||||
error.details(),
|
||||
codex_protocol::error::CodexErrorDetails::ContextWindowExceeded
|
||||
),
|
||||
status == 400
|
||||
);
|
||||
assert_eq!(
|
||||
error.retry_delay(/*retry_count*/ 1).is_some(),
|
||||
status == 503
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_wrapped_websocket_error_event_with_connection_limit_maps_retryable() {
|
||||
let payload = json!({
|
||||
|
||||
@@ -96,6 +96,9 @@ pub(crate) fn parse_terminal_provider_error(
|
||||
.and_then(Value::as_str)
|
||||
.filter(|message| !message.trim().is_empty());
|
||||
match (code, status) {
|
||||
("context_length_exceeded", None | Some(StatusCode::BAD_REQUEST)) => {
|
||||
Some(ApiError::ContextWindowExceeded)
|
||||
}
|
||||
// A non-streaming 400 already follows the ordinary invalid-request path.
|
||||
("invalid_access_program" | "unsupported_access_program", None) => {
|
||||
Some(ApiError::InvalidRequest {
|
||||
|
||||
@@ -680,6 +680,7 @@ async fn process_sse_with_treatment(
|
||||
if matches!(
|
||||
error,
|
||||
ApiError::FlexUnavailable
|
||||
| ApiError::ContextWindowExceeded
|
||||
| ApiError::InvalidRequest { .. }
|
||||
| ApiError::SessionBudgetExceeded
|
||||
| ApiError::AccessProgramNotEnabled { .. }
|
||||
|
||||
@@ -45,7 +45,6 @@ pub(super) fn parse_failed_response(response: Option<Value>) -> ApiError {
|
||||
};
|
||||
|
||||
match error.code.as_deref() {
|
||||
Some("context_length_exceeded") => ApiError::ContextWindowExceeded,
|
||||
Some(
|
||||
"insufficient_quota"
|
||||
| "credit_balance_exhausted"
|
||||
|
||||
@@ -115,6 +115,10 @@ async fn terminal_failure_with_retry_header_ends_stream_immediately() {
|
||||
for (code, expected) in [
|
||||
("flex_unavailable", CodexErrorInfo::FlexUnavailable),
|
||||
("budget_reached", CodexErrorInfo::SessionBudgetExceeded),
|
||||
(
|
||||
"context_length_exceeded",
|
||||
CodexErrorInfo::ContextWindowExceeded,
|
||||
),
|
||||
("invalid_access_program", CodexErrorInfo::Other),
|
||||
("unsupported_access_program", CodexErrorInfo::Other),
|
||||
("content_policy_violation", CodexErrorInfo::InvalidPrompt),
|
||||
|
||||
@@ -4464,22 +4464,25 @@ async fn manual_compact_retries_after_context_window_error() {
|
||||
ev_assistant_message("m1", FIRST_REPLY),
|
||||
ev_completed("r1"),
|
||||
]);
|
||||
let compact_failed = sse_failed(
|
||||
"resp-fail",
|
||||
"context_length_exceeded",
|
||||
CONTEXT_LIMIT_MESSAGE,
|
||||
);
|
||||
let compact_failed = wiremock::ResponseTemplate::new(/*s*/ 400).set_body_json(json!({
|
||||
"error": {
|
||||
"type": "invalid_request_error",
|
||||
"code": "context_length_exceeded",
|
||||
"message": CONTEXT_LIMIT_MESSAGE,
|
||||
"param": "input",
|
||||
}
|
||||
}));
|
||||
let compact_succeeds = sse(vec![
|
||||
ev_assistant_message("m2", SUMMARY_TEXT),
|
||||
ev_completed("r2"),
|
||||
]);
|
||||
|
||||
let request_log = mount_sse_sequence(
|
||||
let request_log = mount_response_sequence(
|
||||
&server,
|
||||
vec![
|
||||
user_turn.clone(),
|
||||
compact_failed.clone(),
|
||||
compact_succeeds.clone(),
|
||||
sse_response(user_turn),
|
||||
compact_failed,
|
||||
sse_response(compact_succeeds),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user