mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Stop retrying terminal runtime authentication denials (#60096)
## Why Invalid assertions and paused or revoked runtime instances cannot recover through request retries or credential refresh. Permanent refresh-token failures also need to retain their sign-in error instead of becoming retryable stream errors. ## What changed - Recognize terminal runtime denial codes in `401` and `403` response bodies and identity error headers, preserving HTTP diagnostics while stopping retries. - Skip credential recovery for runtime denials in runtime-enabled model providers and backend requests, while preserving recovery for ordinary credential expiration. - Propagate permanent refresh-token failures through runtime authentication as `RefreshTokenFailed` errors. ## Testing Add coverage for body and header denials, preserved HTTP diagnostics, provider-scoped credential recovery, and cached permanent refresh failures. Complete-turn tests verify that runtime denials stop without retries or refreshes while expired credentials recover in both runtime authentication modes. GitOrigin-RevId: 33f6f707454130e97e13f3b53e29d2c484602152
This commit is contained in:
@@ -429,6 +429,15 @@ impl CodexErr {
|
||||
/// otherwise use exponential backoff with jitter. Callers enforce their own retry budgets.
|
||||
pub fn retry_delay(&self, retry_count: u64) -> Option<Duration> {
|
||||
match self.details() {
|
||||
CodexErrorDetails::UnexpectedStatus(error)
|
||||
if is_runtime_auth_denial(
|
||||
error.status,
|
||||
&error.body,
|
||||
error.identity_error_code.as_deref(),
|
||||
) =>
|
||||
{
|
||||
None
|
||||
}
|
||||
CodexErrorDetails::TurnAborted
|
||||
| CodexErrorDetails::SessionBudgetExceeded
|
||||
| CodexErrorDetails::Interrupted
|
||||
@@ -629,6 +638,39 @@ pub struct UnexpectedResponseError {
|
||||
pub identity_error_code: Option<String>,
|
||||
}
|
||||
|
||||
/// Runtime denial cannot be repaired by retrying the request or refreshing its original credential.
|
||||
/// Keep unavailable verification state and ordinary credential errors on their existing paths.
|
||||
pub fn is_runtime_auth_denial(
|
||||
status: StatusCode,
|
||||
body: &str,
|
||||
identity_error_code: Option<&str>,
|
||||
) -> bool {
|
||||
if !matches!(status, StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN) {
|
||||
return false;
|
||||
}
|
||||
let is_terminal = |code| {
|
||||
matches!(
|
||||
code,
|
||||
Some(
|
||||
"runtime_assertion_invalid"
|
||||
| "runtime_instance_paused"
|
||||
| "runtime_instance_revoked"
|
||||
| "invalid_agent_instance_assertion"
|
||||
)
|
||||
)
|
||||
};
|
||||
if is_terminal(identity_error_code) {
|
||||
return true;
|
||||
}
|
||||
let Ok(body) = serde_json::from_str::<serde_json::Value>(body) else {
|
||||
return false;
|
||||
};
|
||||
is_terminal(
|
||||
body.pointer("/error/code")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
)
|
||||
}
|
||||
|
||||
const UNEXPECTED_RESPONSE_BODY_MAX_BYTES: usize = 1000;
|
||||
|
||||
impl UnexpectedResponseError {
|
||||
|
||||
Reference in New Issue
Block a user