Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/providers/codex/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1467,7 +1467,7 @@ mod tests {
fn status_error_preserves_buffered_websocket_event_message() {
let error = codex_status_error(
CodexResponse {
body: br#"data: {"type":"error","error":{"status":400,"message":"bad request"}}\n\n"#
body: b"data: {\"type\":\"error\",\"error\":{\"status\":400,\"message\":\"bad request\"}}\n\n"
.to_vec(),
status: 400,
headers: Vec::new(),
Expand Down
37 changes: 22 additions & 15 deletions src/providers/codex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,9 @@ impl Provider for CodexProvider {
Ok(b) => b,
Err(e) => {
clear_continuation(ctx.session_id.as_deref());
return json_error(
StatusCode::BAD_GATEWAY,
"api_error",
format!("Stream translation error: {e}"),
);
return map_codex_failure_to_response(&format!(
"Stream translation error: {e}"
));
}
};
if let Some(monitor) = ctx.monitor.as_ref() {
Expand Down Expand Up @@ -229,11 +227,7 @@ impl Provider for CodexProvider {
}
Err(e) => {
clear_continuation(ctx.session_id.as_deref());
json_error(
StatusCode::BAD_GATEWAY,
"api_error",
format!("Accumulation error: {e}"),
)
map_codex_failure_to_response(&format!("Accumulation error: {e}"))
}
}
}
Expand Down Expand Up @@ -445,11 +439,7 @@ async fn live_stream_response_once(
};
}
clear_continuation(ctx.session_id.as_deref());
return LiveStreamStart::Response(json_error(
StatusCode::BAD_GATEWAY,
"api_error",
message,
));
return LiveStreamStart::Response(map_codex_failure_to_response(&message));
}
};
if !chunk.is_empty() {
Expand Down Expand Up @@ -753,6 +743,11 @@ fn update_continuation_from_upstream(
// ---------------------------------------------------------------------------

fn map_codex_error_to_response(err: &client::CodexError) -> Response {
let message = codex_error_message(err);
if is_context_window_overflow(message) {
return map_codex_failure_to_response(message);
}

match err.status {
401 | 403 => json_error(
StatusCode::UNAUTHORIZED,
Expand Down Expand Up @@ -799,6 +794,18 @@ fn map_codex_error_to_response(err: &client::CodexError) -> Response {
}
}

fn map_codex_failure_to_response(message: &str) -> Response {
if is_context_window_overflow(message) {
json_error(StatusCode::PAYLOAD_TOO_LARGE, "request_too_large", message)
} else {
json_error(StatusCode::BAD_GATEWAY, "api_error", message)
}
}

fn is_context_window_overflow(message: &str) -> bool {
message.to_ascii_lowercase().contains("context window")
}

fn codex_error_message(err: &client::CodexError) -> &str {
err.detail.as_deref().unwrap_or({
if err.status == 0 {
Expand Down
42 changes: 40 additions & 2 deletions tests/smoke_cutover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,42 @@ async fn smoke_codex_http_messages_uses_mock_upstream() {
assert_eq!(sent["stream"], true);
}

#[allow(clippy::await_holding_lock)]
#[tokio::test]
async fn smoke_codex_http_context_window_error_requests_compaction() {
let _guard = env_lock();
let config = TempDir::new().unwrap();
write_auth(config.path(), "codex");

let upstream = spawn_http_upstream(|_body: Value| {
"data: {\"type\":\"response.failed\",\"response\":{\"error\":{\"message\":\"input exceeds context window\"}}}\n\n"
.as_bytes()
.to_vec()
})
.await;

let _config_env = EnvGuard::set("CCP_CONFIG_DIR", config.path());
let _base_url_env = EnvGuard::set("CCP_CODEX_BASE_URL", &upstream);
let _transport_env = EnvGuard::set("CCP_CODEX_TRANSPORT", "http");
let response = call_messages("gpt-5.5").await;
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();

assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
let value: Value = serde_json::from_slice(&body).unwrap();
assert_eq!(value["type"], "error");
assert_eq!(value["error"]["type"], "request_too_large");
assert!(
value["error"]["message"]
.as_str()
.is_some_and(|message| message.contains("input exceeds context window")),
"response body: {}",
String::from_utf8_lossy(&body)
);
}

#[allow(clippy::await_holding_lock)]
#[tokio::test]
async fn smoke_codex_http_traffic_capture_writes_upstream_artifacts() {
Expand Down Expand Up @@ -887,7 +923,7 @@ async fn smoke_codex_websocket_stream_returns_delta_before_terminal() {

#[allow(clippy::await_holding_lock)]
#[tokio::test(flavor = "multi_thread")]
async fn smoke_codex_websocket_stream_returns_json_for_early_error() {
async fn smoke_codex_websocket_context_window_error_requests_compaction() {
let _guard = env_lock();
let config = TempDir::new().unwrap();
write_auth(config.path(), "codex");
Expand All @@ -913,11 +949,13 @@ async fn smoke_codex_websocket_stream_returns_json_for_early_error() {

assert_eq!(
status,
StatusCode::BAD_GATEWAY,
StatusCode::PAYLOAD_TOO_LARGE,
"response body: {}",
String::from_utf8_lossy(&body)
);
let value: Value = serde_json::from_slice(&body).unwrap();
assert_eq!(value["type"], "error");
assert_eq!(value["error"]["type"], "request_too_large");
assert_eq!(value["error"]["message"], "input exceeds context window");
}

Expand Down