mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Distinguish policy-restricted capacity failures from server overloads (#57452)
## What changed - Recognize `policy_capacity_unavailable` in HTTP and streaming API errors and expose it as `policyCapacityUnavailable` through the app-server protocol and generated bindings. - Display “Policy-restricted capacity is unavailable. Please try again later.” while preserving overload retry behavior based on `Retry-After`. - Handle the new error in TUI overload rendering, reviewer retries, and error telemetry. ## Testing Add coverage for HTTP and streaming error classification, retry advice, WebSocket terminal error attribution, protocol serialization, and TUI rendering with and without an upstream message. GitOrigin-RevId: 1b9536fba1e11cc891dcf689aee35410d82f5412
This commit is contained in:
committed by
copyberry
parent
824b7666fb
commit
6a6e2fb1f0
@@ -24,6 +24,7 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
|
||||
ApiError::Retryable { retry_after, .. }
|
||||
| ApiError::RateLimitExceeded { retry_after, .. }
|
||||
| ApiError::ServerOverloaded { retry_after }
|
||||
| ApiError::PolicyCapacityUnavailable { retry_after }
|
||||
| ApiError::Transport(TransportError::Http { retry_after, .. }) => *retry_after,
|
||||
ApiError::Transport(_)
|
||||
| ApiError::Api { .. }
|
||||
@@ -64,6 +65,9 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
|
||||
CodexErr::new(CodexErrorDetails::ResponseIncomplete(incomplete))
|
||||
}
|
||||
ApiError::ServerOverloaded { .. } => CodexErr::ServerOverloaded,
|
||||
ApiError::PolicyCapacityUnavailable { .. } => {
|
||||
CodexErr::new(CodexErrorDetails::PolicyCapacityUnavailable)
|
||||
}
|
||||
ApiError::FlexUnavailable => CodexErr::new(CodexErrorDetails::FlexUnavailable),
|
||||
ApiError::Api { status, message } => {
|
||||
let user_message = api_error_user_message(status, &message);
|
||||
@@ -115,6 +119,9 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
|
||||
&& let Some(error) = provider_error
|
||||
{
|
||||
match error.get("code").and_then(Value::as_str) {
|
||||
Some("policy_capacity_unavailable") => {
|
||||
return CodexErr::new(CodexErrorDetails::PolicyCapacityUnavailable);
|
||||
}
|
||||
Some("server_is_overloaded") => return CodexErr::ServerOverloaded,
|
||||
Some("slow_down") => {
|
||||
return CodexErr::new(CodexErrorDetails::RateLimitExceeded(
|
||||
|
||||
@@ -11,6 +11,45 @@ fn map_api_error_maps_server_overloaded() {
|
||||
assert!(matches!(err.details(), CodexErrorDetails::ServerOverloaded));
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn http_policy_capacity_preserves_retry_advice_and_terminal_behavior() {
|
||||
use std::time::Duration;
|
||||
for (code, expected) in [
|
||||
(
|
||||
"policy_capacity_unavailable",
|
||||
CodexErrorInfo::PolicyCapacityUnavailable,
|
||||
),
|
||||
("server_is_overloaded", CodexErrorInfo::ServerOverloaded),
|
||||
] {
|
||||
for advice in [RetryAfter::from_delay(Duration::from_secs(3)), None] {
|
||||
let error = map_api_error(ApiError::Transport(TransportError::Http {
|
||||
status: http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
url: None,
|
||||
headers: None,
|
||||
body: Some(
|
||||
serde_json::json!({"error": {
|
||||
"code": code, "type": "service_unavailable_error",
|
||||
}})
|
||||
.to_string(),
|
||||
),
|
||||
retry_after: advice,
|
||||
}));
|
||||
assert_eq!(
|
||||
(
|
||||
error.to_codex_protocol_error(),
|
||||
error.retry_after(),
|
||||
error.retry_delay(/*retry_count*/ 1)
|
||||
),
|
||||
(
|
||||
expected.clone(),
|
||||
advice,
|
||||
advice.map(|_| Duration::from_secs(3))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_api_error_preserves_content_filter_retry_and_public_error() {
|
||||
let err = map_api_error(ApiError::ContentFilter);
|
||||
|
||||
@@ -57,6 +57,9 @@ pub enum ApiError {
|
||||
},
|
||||
#[error("Flex capacity unavailable.")]
|
||||
FlexUnavailable,
|
||||
/// Server-confirmed capacity failure on a policy-restricted route.
|
||||
#[error("policy-restricted capacity unavailable")]
|
||||
PolicyCapacityUnavailable { retry_after: Option<RetryAfter> },
|
||||
#[error("server overloaded")]
|
||||
ServerOverloaded { retry_after: Option<RetryAfter> },
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@ pub(super) fn parse_failed_response(response: Option<Value>) -> ApiError {
|
||||
.message
|
||||
.unwrap_or_else(|| "Invalid request.".to_string()),
|
||||
},
|
||||
Some("policy_capacity_unavailable") => ApiError::PolicyCapacityUnavailable {
|
||||
retry_after: retry_after_header,
|
||||
},
|
||||
Some("server_is_overloaded") => ApiError::ServerOverloaded {
|
||||
retry_after: retry_after_header,
|
||||
},
|
||||
|
||||
@@ -16,6 +16,58 @@ use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Only the explicit policy capacity code changes overload attribution.
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn policy_capacity_classification_preserves_overload_retry_behavior() {
|
||||
for (code, error_type, expected) in [
|
||||
(
|
||||
"policy_capacity_unavailable",
|
||||
None,
|
||||
CodexErrorInfo::PolicyCapacityUnavailable,
|
||||
),
|
||||
(
|
||||
"server_is_overloaded",
|
||||
Some("policy_error"),
|
||||
CodexErrorInfo::ServerOverloaded,
|
||||
),
|
||||
(
|
||||
"server_is_overloaded",
|
||||
None,
|
||||
CodexErrorInfo::ServerOverloaded,
|
||||
),
|
||||
] {
|
||||
for retry_after in [Some("3"), None] {
|
||||
let event = serde_json::from_value(json!({
|
||||
"type": "response.failed",
|
||||
"response": {"error": {
|
||||
"type": error_type,
|
||||
"code": code,
|
||||
"headers": {"Retry-After": retry_after},
|
||||
}},
|
||||
}))
|
||||
.unwrap();
|
||||
let error = map_api_error(process_responses_event(event).unwrap_err().into_api_error());
|
||||
assert_eq!(
|
||||
(
|
||||
error.to_codex_protocol_error(),
|
||||
error.retry_delay(/*retry_count*/ 1),
|
||||
error.to_string()
|
||||
),
|
||||
(
|
||||
expected.clone(),
|
||||
retry_after.map(|_| Duration::from_secs(3)),
|
||||
if expected == CodexErrorInfo::PolicyCapacityUnavailable {
|
||||
"Policy-restricted capacity is unavailable. Please try again later."
|
||||
} else {
|
||||
"Selected model is at capacity. Please try a different model."
|
||||
}
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Flex failures remain terminal even with retry headers and a later completed event.
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn flex_failure_with_retry_header_ends_stream_immediately() {
|
||||
|
||||
Reference in New Issue
Block a user