codex-client doesn't retry transient "Item with id ... not found" (HTTP 400) Responses API errors

Open 💬 0 comments Opened Jul 9, 2026 by amansman77

Summary

The Responses API intermittently returns HTTP 400 invalid_request_error: Item with id '<id>' not found when a follow-up turn references a reasoning/function-call item from the immediately preceding response (observed most reliably against an Azure OpenAI-backed custom model_provider using wire_api = "responses"). Manually retrying the exact same request a moment later succeeds, which points to a transient server-side item-lookup race rather than a malformed request. Today, codex-client's retry policy does not retry on HTTP 400 at all, so this transient failure is always surfaced to the user instead of being absorbed automatically.

Environment

  • codex-rs built from main (local build, no tagged release)
  • model_provider = custom Azure OpenAI provider (wire_api = "responses", own env_key, not requires_openai_auth)
  • Surface: both codex app-server (Desktop app) and ad-hoc turns; not obviously correlated with any particular front-end
  • Error shape (paraphrased from observed responses):
{
  "error": {
    "message": "Item with id 'rs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' not found.",
    "type": "invalid_request_error",
    "param": "input",
    "code": null
  }
}

Why this looks transient, not a client bug

  • The exact same request payload succeeds on manual retry with no changes.
  • Microsoft has documented the same class of error for the Azure OpenAI Responses API as an intermittent backend race ("Azure OpenAI Intermittent 400 Error: Item with id not found", Microsoft Q&A) where an item referenced immediately after creation isn't yet visible to a lookup on the same backend. Adding a short delay before the follow-up request masks the issue, which is consistent with a replication/consistency lag rather than a genuinely invalid request.
  • Similar Item with id 'rs_...' not found reports exist against the Responses API from other clients (e.g. openai/openai-agents-python#2020, vercel/ai#7543, vercel/ai#9119), suggesting this isn't specific to one SDK's request construction.

Where this lives in codex-rs today

codex-rs/codex-client/src/retry.rs:

impl RetryOn {
    pub fn should_retry(&self, err: &TransportError, attempt: u64, max_attempts: u64) -> bool {
        if attempt >= max_attempts {
            return false;
        }
        match err {
            TransportError::Http { status, .. } => {
                (self.retry_429 && status.as_u16() == 429)
                    || (self.retry_5xx && status.is_server_error())
            }
            TransportError::Timeout | TransportError::Network(_) => self.retry_transport,
            _ => false,
        }
    }
}

Only 429, 5xx, and transport-level errors are retried. HTTP 400 is always treated as terminal, so this specific transient case is never retried automatically, even though TransportError::Http already carries the response body needed to distinguish it from a genuine bad request.

Suggested direction (not a PR — happy to send one if invited)

Narrowly special-case this one error shape inside should_retry, keyed off status 400 and a body match for invalid_request_error + not found + (Item with id or "param":"input"), so no other 400s are affected:

TransportError::Http { status, body, .. } => {
    (self.retry_429 && status.as_u16() == 429)
        || (self.retry_5xx && status.is_server_error())
        || is_transient_item_not_found(status.as_u16(), body)
}
fn is_transient_item_not_found(status: u16, body: &Option<String>) -> bool {
    if status != 400 {
        return false;
    }
    let Some(body) = body else { return false };
    body.contains("invalid_request_error")
        && body.contains("not found")
        && (body.contains("Item with id") || body.contains("\"param\":\"input\""))
}

This reuses the existing max_attempts / backoff machinery, so no new config surface is required; it would just make one more narrowly-scoped error class eligible for the retry budget that already exists per provider.

Open questions I'd want the team's take on before writing a real PR:

  • Whether this should be gated behind a new RetryOn flag (e.g. retry_item_not_found) instead of being unconditional, for consistency with how retry_429/retry_5xx are exposed today.
  • Whether there's a preferred way to fingerprint this error server-side (an error code, rather than matching on message/param text) that would be more robust than string matching.
  • Whether the team has independent visibility into how common/transient this is across providers, since I've only been able to reproduce it against a custom Azure OpenAI provider.

Happy to share full repro steps or open a PR along these lines if that's useful.

View original on GitHub ↗