codex-client doesn't retry transient "Item with id ... not found" (HTTP 400) Responses API errors
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", ownenv_key, notrequires_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 foundreports 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
RetryOnflag (e.g.retry_item_not_found) instead of being unconditional, for consistency with howretry_429/retry_5xxare exposed today. - Whether there's a preferred way to fingerprint this error server-side (an error
code, rather than matching onmessage/paramtext) 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.