Files
codex/codex-rs/core/src/compact_model_fallback.rs
T
jif bc9cad3473 Treat incomplete responses as terminal errors (#53451)
## Why

Incomplete responses such as `max_output_tokens` were classified as stream errors, allowing retries and compaction model fallback after the response had hit its output limit.

## What changed

Introduce `ResponseIncomplete` errors that preserve the server-provided reason and stop stream processing immediately. Make these errors non-retryable and stop compaction without falling back to the selected model. Record incomplete responses separately in sampler failure metrics.

## Testing

Add regression tests that verify incomplete responses surface a terminal error without retries over SSE and WebSocket, and that compaction stops without retrying or switching models for both local and remote compaction.

GitOrigin-RevId: 6e107d441b9bf03fa47ea47b88eeb94b71a9f729
2026-10-12 18:53:49 +00:00

77 lines
2.4 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::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"
);
}