Treat model-not-found provider errors as terminal failures (#56445)

## Why

Provider errors with code `model_not_found` should end the turn immediately and preserve the provider's message instead of triggering retries.

## What changed

- Recognize `model_not_found` in HTTP 400, 403, and 404 responses and in streamed `error` and `response.failed` events.
- Preserve the provider message and any available HTTP status, and expose `modelNotFound` through the app-server protocol and generated schemas and SDK types.

## Testing

Add coverage for HTTP classification, statusless stream errors, and terminal failures without retries across HTTP, SSE, and WebSocket responses. Verify that `thread/read` preserves the error reported by the live notification.

GitOrigin-RevId: 88f824ac03c8c9669915b1e40c67b76c08326fbe
This commit is contained in:
Charlie Marsh
2026-10-23 11:43:14 +00:00
committed by copyberry
parent ca9d103f39
commit 60adfa8513
39 changed files with 190 additions and 67 deletions
+13 -12
View File
@@ -1,6 +1,6 @@
use crate::TransportError;
use crate::error::ApiError;
use crate::error::parse_flex_unavailable;
use crate::error::parse_terminal_provider_error;
use crate::rate_limits::parse_promo_message;
use crate::rate_limits::parse_rate_limit_for_limit;
use crate::rate_limits::parse_rate_limit_reached_type;
@@ -36,6 +36,7 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
| ApiError::FlexUnavailable
| ApiError::RateLimit(_)
| ApiError::InvalidRequest { .. }
| ApiError::ModelNotFound { .. }
| ApiError::InvalidPrompt { .. }
| ApiError::CyberPolicy { .. }
| ApiError::BioPolicy { .. }
@@ -78,6 +79,9 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
})
}
ApiError::InvalidRequest { message } => CodexErr::InvalidRequest(message),
ApiError::ModelNotFound { message, status } => {
CodexErr::new(CodexErrorDetails::ModelNotFound { message, status })
}
ApiError::InvalidPrompt { message } => {
CodexErr::new(CodexErrorDetails::InvalidPrompt { message })
}
@@ -101,10 +105,14 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
..
} => {
let body_text = body.unwrap_or_default();
let parsed_body = serde_json::from_str::<Value>(&body_text).ok();
let provider_error = parsed_body.as_ref().and_then(|body| body.get("error"));
if let Some(error) = parse_terminal_provider_error(provider_error, Some(status)) {
return map_api_error_details(error);
}
if status == http::StatusCode::SERVICE_UNAVAILABLE
&& let Ok(value) = serde_json::from_str::<serde_json::Value>(&body_text)
&& let Some(error) = value.get("error")
&& let Some(error) = provider_error
{
match error.get("code").and_then(Value::as_str) {
Some("server_is_overloaded") => return CodexErr::ServerOverloaded,
@@ -123,8 +131,7 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
if (status == http::StatusCode::BAD_REQUEST
|| status == http::StatusCode::FORBIDDEN)
&& let Ok(parsed) = serde_json::from_str::<Value>(&body_text)
&& let Some(error) = parsed.get("error")
&& let Some(error) = provider_error
&& error.get("code").and_then(Value::as_str)
== Some(MISALIGNMENT_POLICY_VIOLATION_ERROR_CODE)
{
@@ -145,8 +152,7 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
}
if status == http::StatusCode::BAD_REQUEST {
if let Ok(parsed) = serde_json::from_str::<Value>(&body_text)
&& let Some(error) = parsed.get("error")
if let Some(error) = provider_error
&& let Some(
code @ (CYBER_POLICY_ERROR_CODE
| BIO_POLICY_ERROR_CODE
@@ -183,11 +189,6 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
} else if status == http::StatusCode::INTERNAL_SERVER_ERROR {
CodexErr::InternalServerError
} else if status == http::StatusCode::TOO_MANY_REQUESTS {
if let Ok(body) = serde_json::from_str::<Value>(&body_text)
&& let Some(error) = body.get("error").and_then(parse_flex_unavailable)
{
return map_api_error(error);
}
if let Ok(err) = serde_json::from_str::<UsageErrorResponse>(&body_text) {
if err.error.error_type.as_deref() == Some("usage_limit_reached") {
let limit_id = extract_header(headers.as_ref(), ACTIVE_LIMIT_HEADER);