mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Add recovery guidance to content-filter retries (#49119)
## Why Responses stopped by `content_filter` previously followed generic stream-error retry handling without guidance about the block. Retried requests need instructions to explain the limitation and offer a permitted alternative. ## What changed - Recognize content-filter failures as a distinct error while preserving retryability and the existing public error representation. - Append developer guidance to the conversation after each sampling block, retaining delivered output and using the existing retry handling. - Support a model catalog override through `model_messages.content_filter_guidance`. Missing, null, blank, or values exceeding 512 UTF-8 bytes fall back to bundled guidance. - The bundled guidance tells the model to avoid reproducing or working around blocked content, briefly explain the limitation, offer a permitted alternative, and continue unrelated authorized work. ## Testing Add a scenario and request-history snapshot covering repeated blocks, catalog guidance on each retry, retained partial output, and eventual completion. Add error-mapping assertions for retryability, public error classification, and error text. GitOrigin-RevId: ba0c201b3a5a3acf5cfa971332027240e0690225
This commit is contained in:
@@ -28,6 +28,7 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
|
||||
ApiError::Transport(_)
|
||||
| ApiError::Api { .. }
|
||||
| ApiError::Stream(_)
|
||||
| ApiError::ContentFilter
|
||||
| ApiError::ContextWindowExceeded
|
||||
| ApiError::QuotaExceeded
|
||||
| ApiError::UsageNotIncluded
|
||||
@@ -56,6 +57,7 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
|
||||
CodexErr::new(CodexErrorDetails::RateLimitExceeded(message))
|
||||
}
|
||||
ApiError::Stream(msg) => CodexErr::Stream(msg),
|
||||
ApiError::ContentFilter => CodexErr::new(CodexErrorDetails::ContentFilter),
|
||||
ApiError::ServerOverloaded { .. } => CodexErr::ServerOverloaded,
|
||||
ApiError::FlexUnavailable => CodexErr::new(CodexErrorDetails::FlexUnavailable),
|
||||
ApiError::Api { status, message } => {
|
||||
|
||||
@@ -11,6 +11,25 @@ fn map_api_error_maps_server_overloaded() {
|
||||
assert!(matches!(err.details(), CodexErrorDetails::ServerOverloaded));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_api_error_preserves_content_filter_retry_and_public_error() {
|
||||
let err = map_api_error(ApiError::ContentFilter);
|
||||
assert!(matches!(err.details(), CodexErrorDetails::ContentFilter));
|
||||
assert_eq!(
|
||||
(
|
||||
err.retry_delay(/*retry_count*/ 1).is_some(),
|
||||
err.to_codex_protocol_error(),
|
||||
err.to_string(),
|
||||
),
|
||||
(
|
||||
true,
|
||||
CodexErrorInfo::Other,
|
||||
"stream disconnected before completion: Incomplete response returned, reason: content_filter"
|
||||
.to_string(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn map_api_error_preserves_retry_delay() {
|
||||
let retry_delay = std::time::Duration::from_secs(17);
|
||||
|
||||
@@ -14,6 +14,8 @@ pub enum ApiError {
|
||||
Api { status: StatusCode, message: String },
|
||||
#[error("stream error: {0}")]
|
||||
Stream(String),
|
||||
#[error("stream error: Incomplete response returned, reason: content_filter")]
|
||||
ContentFilter,
|
||||
#[error("context window exceeded")]
|
||||
ContextWindowExceeded,
|
||||
#[error("quota exceeded")]
|
||||
|
||||
@@ -425,6 +425,9 @@ pub fn process_responses_event(
|
||||
.and_then(Value::as_str)
|
||||
});
|
||||
let reason = reason.unwrap_or("unknown");
|
||||
if reason == "content_filter" {
|
||||
return Err(ResponsesEventError::Api(ApiError::ContentFilter));
|
||||
}
|
||||
if reason != "interrupted" {
|
||||
let message = format!("Incomplete response returned, reason: {reason}");
|
||||
return Err(ResponsesEventError::Api(ApiError::Stream(message)));
|
||||
|
||||
Reference in New Issue
Block a user