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
+29 -3
View File
@@ -39,6 +39,11 @@ pub enum ApiError {
RateLimit(String),
#[error("invalid request: {message}")]
InvalidRequest { message: String },
#[error("{message}")]
ModelNotFound {
message: String,
status: Option<StatusCode>,
},
#[error("invalid prompt: {message}")]
InvalidPrompt { message: String },
#[error("cyber policy: {message}")]
@@ -62,9 +67,30 @@ impl From<RateLimitError> for ApiError {
}
}
pub(crate) fn parse_flex_unavailable(error: &Value) -> Option<ApiError> {
(error.get("code").and_then(Value::as_str) == Some("flex_unavailable"))
.then_some(ApiError::FlexUnavailable)
pub(crate) fn parse_terminal_provider_error(
error: Option<&Value>,
status: Option<StatusCode>,
) -> Option<ApiError> {
let error = error?;
let code = error.get("code")?.as_str()?;
match (code, status) {
("flex_unavailable", None | Some(StatusCode::TOO_MANY_REQUESTS)) => {
Some(ApiError::FlexUnavailable)
}
(
"model_not_found",
None | Some(StatusCode::BAD_REQUEST | StatusCode::FORBIDDEN | StatusCode::NOT_FOUND),
) => Some(ApiError::ModelNotFound {
message: error
.get("message")
.and_then(Value::as_str)
.filter(|message| !message.trim().is_empty())
.unwrap_or(code)
.to_owned(),
status,
}),
_ => None,
}
}
impl From<crate::AuthError> for ApiError {