Files
codex/codex-rs/core/src/compact_model_fallback.rs
T
Charlie Marsh 65f2709052 Treat content policy violations as terminal errors (#60479)
## What changed

Recognize `content_policy_violation` in HTTP 400 responses and streamed errors as a dedicated, non-retryable error. Preserve the provider's message and available HTTP status, use a fallback message when needed, and report `CodexErrorInfo::InvalidPrompt` to clients.

End the response stream immediately on this error and stop compaction from falling back to the current model after a content policy rejection. Add redacted diagnostic output and a dedicated sampler failure reason.

## Testing

Extend coverage for HTTP and wrapped WebSocket errors, missing or blank provider messages, terminal stream failures with retry headers, and content policy rejections during local and remote compaction on model downshift.

GitOrigin-RevId: 592170ac30c07b0b303a8d5cb15bd35c8f821b55
2026-11-10 00:53:04 +00:00

79 lines
2.5 KiB
Rust

use crate::session::turn_context::TurnContext;
use codex_analytics::CompactionImplementation;
use codex_analytics::CompactionReason;
use codex_otel::SessionTelemetry;
use codex_protocol::error::CodexErr;
use codex_protocol::error::CodexErrorDetails;
use tracing::warn;
/// Returns whether a failed compaction attempt should use the current model.
pub(crate) fn should_retry_with_current_model(
error: &CodexErr,
previous: &TurnContext,
current: &TurnContext,
) -> bool {
if matches!(
error.details(),
CodexErrorDetails::TurnAborted
| CodexErrorDetails::Interrupted
| CodexErrorDetails::SessionBudgetExceeded
| CodexErrorDetails::InvalidWorkspaceSelected(_)
| CodexErrorDetails::ContentPolicyViolation { .. }
| CodexErrorDetails::ResponseIncomplete(_)
) || (previous.model_info().slug == current.model_info().slug
&& previous.cyber_access_program == current.cyber_access_program)
{
return false;
}
current.provider.info().is_openai()
&& current
.auth_manager
.as_deref()
.and_then(codex_login::AuthManager::auth_cached)
.is_some_and(|auth| auth.uses_codex_backend() || auth.is_api_key_auth())
}
pub(crate) fn record_model_fallback(
session_telemetry: &SessionTelemetry,
previous_model: &str,
current_model: &str,
reason: CompactionReason,
implementation: CompactionImplementation,
fallback_error: Option<&CodexErr>,
) {
let reason_tag = match reason {
CompactionReason::UserRequested => "user_requested",
CompactionReason::ContextLimit => "context_limit",
CompactionReason::ModelDownshift => "model_downshift",
CompactionReason::CompHashChanged => "comp_hash_changed",
};
let implementation_tag = match implementation {
CompactionImplementation::Responses => "responses",
CompactionImplementation::ResponsesCompactionV2 => "responses_compaction_v2",
};
let outcome = if fallback_error.is_none() {
"succeeded"
} else {
"failed"
};
session_telemetry.counter(
"codex.compaction.model_fallback",
/*inc*/ 1,
&[
("reason", reason_tag),
("implementation", implementation_tag),
("outcome", outcome),
],
);
warn!(
previous_model,
current_model,
?reason,
?implementation,
outcome,
?fallback_error,
"previous-model compaction failed; retried with current model"
);
}