Skip to content
Open
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
84 changes: 84 additions & 0 deletions codex-rs/core/src/api_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use codex_login::token_data::PlanType;
use http::HeaderMap;
use serde::Deserialize;
use serde_json::Value;
use tracing::warn;

use crate::auth::CodexAuth;
use crate::error::CodexErr;
Expand All @@ -18,6 +19,12 @@ use crate::error::UnexpectedResponseError;
use crate::error::UsageLimitReachedError;
use crate::model_provider_info::ModelProviderInfo;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct InlineImageRequestLimitBadRequestObservation {
pub(crate) bytes_exceeded: bool,
pub(crate) images_exceeded: bool,
}

pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
match err {
ApiError::ContextWindowExceeded => CodexErr::ContextWindowExceeded,
Expand Down Expand Up @@ -63,6 +70,17 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
.contains("The image data you provided does not represent a valid image")
{
CodexErr::InvalidImageRequest()
} else if let Some(observation) =
inline_image_request_limit_bad_request_observation(&body_text)
{
warn!(
response_status = %status,
bytes_exceeded = observation.bytes_exceeded,
images_exceeded = observation.images_exceeded,
response_body = %body_text,
"responses request rejected by upstream inline image limit"
);
CodexErr::InvalidRequest(body_text)
} else {
CodexErr::InvalidRequest(body_text)
}
Expand Down Expand Up @@ -138,6 +156,59 @@ fn extract_request_tracking_id(headers: Option<&HeaderMap>) -> Option<String> {
extract_request_id(headers).or_else(|| extract_header(headers, CF_RAY_HEADER))
}

pub(crate) fn inline_image_request_limit_bad_request_observation(
body: &str,
) -> Option<InlineImageRequestLimitBadRequestObservation> {
if let Ok(error) = serde_json::from_str::<BadRequestErrorResponse>(body) {
return inline_image_request_limit_observation(
&error.error.message,
error.error.code.as_deref(),
error.error.error_type.as_deref(),
);
}

inline_image_request_limit_observation_from_message(body)
}

pub(crate) fn inline_image_request_limit_observation(
message: &str,
code: Option<&str>,
error_type: Option<&str>,
) -> Option<InlineImageRequestLimitBadRequestObservation> {
if matches!(
(code, error_type),
(Some("max_images_per_request"), _) | (_, Some("max_images_per_request"))
) {
return Some(InlineImageRequestLimitBadRequestObservation {
bytes_exceeded: false,
images_exceeded: true,
});
}

inline_image_request_limit_observation_from_message(message)
}

fn inline_image_request_limit_observation_from_message(
message: &str,
) -> Option<InlineImageRequestLimitBadRequestObservation> {
let bytes_exceeded = matches_inline_image_byte_limit_message(message);
if !bytes_exceeded {
return None;
}

Some(InlineImageRequestLimitBadRequestObservation {
bytes_exceeded,
images_exceeded: false,
})
}

fn matches_inline_image_byte_limit_message(message: &str) -> bool {
message
.strip_prefix("Total image data in 'input' exceeds the ")
.and_then(|rest| rest.split_once(" byte limit"))
.is_some_and(|(limit, _)| !limit.is_empty() && limit.chars().all(|c| c.is_ascii_digit()))
}

fn extract_request_id(headers: Option<&HeaderMap>) -> Option<String> {
extract_header(headers, REQUEST_ID_HEADER)
.or_else(|| extract_header(headers, OAI_REQUEST_ID_HEADER))
Expand Down Expand Up @@ -201,6 +272,19 @@ struct UsageErrorResponse {
error: UsageErrorBody,
}

#[derive(Debug, Deserialize)]
struct BadRequestErrorResponse {
error: BadRequestErrorBody,
}

#[derive(Debug, Deserialize)]
struct BadRequestErrorBody {
message: String,
#[serde(rename = "type")]
error_type: Option<String>,
code: Option<String>,
}

#[derive(Debug, Deserialize)]
struct UsageErrorBody {
#[serde(rename = "type")]
Expand Down
59 changes: 59 additions & 0 deletions codex-rs/core/src/api_bridge_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,65 @@ fn map_api_error_extracts_identity_auth_details_from_headers() {
assert_eq!(err.identity_error_code.as_deref(), Some("token_expired"));
}

#[test]
fn inline_image_request_limit_bad_request_matches_byte_limit_copy() {
assert_eq!(
inline_image_request_limit_bad_request_observation(
"Total image data in 'input' exceeds the 536870912 byte limit."
),
Some(InlineImageRequestLimitBadRequestObservation {
bytes_exceeded: true,
images_exceeded: false,
})
);
}

#[test]
fn inline_image_request_limit_bad_request_matches_live_byte_limit_copy() {
assert_eq!(
inline_image_request_limit_bad_request_observation(
"Total image data in 'input' exceeds the 536870912 byte limit for a single /v1/responses request."
),
Some(InlineImageRequestLimitBadRequestObservation {
bytes_exceeded: true,
images_exceeded: false,
})
);
}

#[test]
fn inline_image_request_limit_bad_request_matches_structured_image_count_error() {
assert_eq!(
inline_image_request_limit_bad_request_observation(
r#"{"error":{"message":"Invalid request.","type":"max_images_per_request","param":null,"code":"max_images_per_request"}}"#
),
Some(InlineImageRequestLimitBadRequestObservation {
bytes_exceeded: false,
images_exceeded: true,
})
);
}

#[test]
fn inline_image_request_limit_bad_request_ignores_message_only_image_count_copy() {
assert_eq!(
inline_image_request_limit_bad_request_observation(
"This request contains 1501 images, which exceeds the 1500 image limit for a single Responses API request."
),
None
);
}

#[test]
fn inline_image_request_limit_bad_request_ignores_other_bad_requests() {
assert_eq!(
inline_image_request_limit_bad_request_observation(
"Request body is missing required field: input"
),
None
);
}

#[test]
fn core_auth_provider_reports_when_auth_header_will_attach() {
let auth = CoreAuthProvider {
Expand Down
30 changes: 30 additions & 0 deletions codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use std::sync::atomic::Ordering;

use crate::api_bridge::CoreAuthProvider;
use crate::api_bridge::auth_provider_from_auth;
use crate::api_bridge::inline_image_request_limit_bad_request_observation;
use crate::api_bridge::map_api_error;
use crate::auth::UnauthorizedRecovery;
use crate::auth_env_telemetry::AuthEnvTelemetry;
Expand Down Expand Up @@ -61,6 +62,7 @@ use codex_api::error::ApiError;
use codex_api::requests::responses::Compression;
use codex_api::response_create_client_metadata;
use codex_otel::SessionTelemetry;
use codex_otel::WellKnownApiRequestError;
use codex_otel::current_span_w3c_trace_context;

use codex_protocol::ThreadId;
Expand Down Expand Up @@ -1674,6 +1676,22 @@ fn api_error_http_status(error: &ApiError) -> Option<u16> {
}
}

fn upstream_inline_image_request_limit_observation_from_transport_error(
error: &TransportError,
) -> Option<crate::api_bridge::InlineImageRequestLimitBadRequestObservation> {
let TransportError::Http {
status,
body: Some(body_text),
..
} = error
else {
return None;
};
if *status != StatusCode::BAD_REQUEST {
return None;
}
inline_image_request_limit_bad_request_observation(body_text)
}
struct ApiTelemetry {
session_telemetry: SessionTelemetry,
auth_context: AuthRequestTelemetryContext,
Expand Down Expand Up @@ -1710,6 +1728,17 @@ impl RequestTelemetry for ApiTelemetry {
let debug = error
.map(extract_response_debug_context)
.unwrap_or_default();
let well_known_error = match error
.and_then(upstream_inline_image_request_limit_observation_from_transport_error)
{
Some(observation) if observation.images_exceeded => {
WellKnownApiRequestError::TooManyImages
}
Some(observation) if observation.bytes_exceeded => {
WellKnownApiRequestError::RequestSizeExceeded
}
Some(_) | None => WellKnownApiRequestError::None,
};
self.session_telemetry.record_api_request(
attempt,
status,
Expand All @@ -1725,6 +1754,7 @@ impl RequestTelemetry for ApiTelemetry {
debug.cf_ray.as_deref(),
debug.auth_error.as_deref(),
debug.auth_error_code.as_deref(),
well_known_error,
);
emit_feedback_request_tags_with_auth_env(
&FeedbackRequestTags {
Expand Down
Loading
Loading