mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +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:
@@ -94,6 +94,11 @@ pub enum CodexErrorDetails {
|
||||
/// The Session loop treats this as a transient error and will automatically retry the turn.
|
||||
#[error("stream disconnected before completion: {0}")]
|
||||
Stream(String),
|
||||
/// A response stopped by the content filter. Sampling retries need developer guidance.
|
||||
#[error(
|
||||
"stream disconnected before completion: Incomplete response returned, reason: content_filter"
|
||||
)]
|
||||
ContentFilter,
|
||||
/// A retryable upstream rate limit received inside the response stream.
|
||||
#[error("rate limit exceeded: {0}")]
|
||||
RateLimitExceeded(String),
|
||||
@@ -411,6 +416,7 @@ impl CodexErr {
|
||||
| CodexErrorDetails::BioPolicy { .. }
|
||||
| CodexErrorDetails::MisalignmentPolicyViolation { .. } => None,
|
||||
CodexErrorDetails::Stream(..)
|
||||
| CodexErrorDetails::ContentFilter
|
||||
| CodexErrorDetails::RateLimitExceeded(_)
|
||||
| CodexErrorDetails::Timeout
|
||||
| CodexErrorDetails::RequestTimeout
|
||||
|
||||
@@ -40,6 +40,7 @@ async fn codex_err_debug_preserves_legacy_shape() {
|
||||
fn retryability_preserves_error_details_distinctions() {
|
||||
let errors = [
|
||||
(CodexErr::ServerOverloaded, false),
|
||||
(CodexErr::new(CodexErrorDetails::ContentFilter), true),
|
||||
(
|
||||
CodexErr::new(CodexErrorDetails::RateLimitExceeded("retry later".into())),
|
||||
true,
|
||||
|
||||
@@ -542,6 +542,10 @@ impl ModelInfo {
|
||||
/// retained to decode catalogs produced before personality selection was removed.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct ModelMessages {
|
||||
/// Developer guidance after a content-filter block. Missing, null, blank, or values over
|
||||
/// 512 UTF-8 bytes use the bundled guidance.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub content_filter_guidance: Option<String>,
|
||||
/// Additional developer instructions for persistent mode. Missing or null uses the built-in
|
||||
/// instructions; an empty string disables them.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1503,6 +1507,7 @@ mod tests {
|
||||
#[test]
|
||||
fn models_response_prefers_template_and_preserves_message_siblings() {
|
||||
let messages = ModelMessages {
|
||||
content_filter_guidance: Some("Offer a permitted alternative.".to_string()),
|
||||
persistent_instructions: Some("Persistent catalog instructions".to_string()),
|
||||
tools: Some(ToolMessages {
|
||||
send_user_message_async: Some(ToolMessage {
|
||||
|
||||
Reference in New Issue
Block a user