mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +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
@@ -664,6 +664,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
+1
@@ -8594,6 +8594,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
+1
@@ -4410,6 +4410,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
+1
@@ -65,6 +65,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"flexUnavailable",
|
||||
"serverOverloaded",
|
||||
"modelNotFound",
|
||||
"policyCapacityUnavailable",
|
||||
"cyberPolicy",
|
||||
"misalignmentPolicyViolation",
|
||||
"tooManyDenials",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -9,4 +9,4 @@ import type { NonSteerableTurnKind } from "./NonSteerableTurnKind";
|
||||
* When an upstream HTTP status is available (for example, from the Responses API or a provider),
|
||||
* it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.
|
||||
*/
|
||||
export type CodexErrorInfo = "contextWindowExceeded" | "sessionBudgetExceeded" | "usageLimitExceeded" | "rateLimitExceeded" | "flexUnavailable" | "serverOverloaded" | "modelNotFound" | "cyberPolicy" | "misalignmentPolicyViolation" | "tooManyDenials" | { "httpConnectionFailed": { httpStatusCode: number | null, } } | { "responseStreamConnectionFailed": { httpStatusCode: number | null, } } | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | { "responseStreamDisconnected": { httpStatusCode: number | null, } } | { "responseTooManyFailedAttempts": { httpStatusCode: number | null, } } | { "activeTurnNotSteerable": { turnKind: NonSteerableTurnKind, } } | "other" | string | { [key: string]: unknown };
|
||||
export type CodexErrorInfo = "contextWindowExceeded" | "sessionBudgetExceeded" | "usageLimitExceeded" | "rateLimitExceeded" | "flexUnavailable" | "serverOverloaded" | "modelNotFound" | "policyCapacityUnavailable" | "cyberPolicy" | "misalignmentPolicyViolation" | "tooManyDenials" | { "httpConnectionFailed": { httpStatusCode: number | null, } } | { "responseStreamConnectionFailed": { httpStatusCode: number | null, } } | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | { "responseStreamDisconnected": { httpStatusCode: number | null, } } | { "responseTooManyFailedAttempts": { httpStatusCode: number | null, } } | { "activeTurnNotSteerable": { turnKind: NonSteerableTurnKind, } } | "other" | string | { [key: string]: unknown };
|
||||
|
||||
@@ -4679,7 +4679,7 @@ mod tests {
|
||||
error: Some(ErrorEvent {
|
||||
misalignment: None,
|
||||
message: "Selected model is at capacity. Please try a different model.".into(),
|
||||
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
|
||||
codex_error_info: Some(CodexErrorInfo::PolicyCapacityUnavailable),
|
||||
}),
|
||||
completed_at: Some(20),
|
||||
duration_ms: Some(10_000),
|
||||
@@ -4713,7 +4713,7 @@ mod tests {
|
||||
message: "Selected model is at capacity. Please try a different model."
|
||||
.into(),
|
||||
codex_error_info: Some(
|
||||
crate::protocol::v2::CodexErrorInfo::ServerOverloaded,
|
||||
crate::protocol::v2::CodexErrorInfo::PolicyCapacityUnavailable,
|
||||
),
|
||||
additional_details: None,
|
||||
}),
|
||||
|
||||
@@ -81,6 +81,8 @@ pub enum CodexErrorInfo {
|
||||
FlexUnavailable,
|
||||
ServerOverloaded,
|
||||
ModelNotFound,
|
||||
// Capacity is unavailable on a route restricted by server policy.
|
||||
PolicyCapacityUnavailable,
|
||||
CyberPolicy,
|
||||
MisalignmentPolicyViolation,
|
||||
TooManyDenials,
|
||||
@@ -157,6 +159,9 @@ impl From<CoreCodexErrorInfo> for CodexErrorInfo {
|
||||
CoreCodexErrorInfo::FlexUnavailable => CodexErrorInfo::FlexUnavailable,
|
||||
CoreCodexErrorInfo::ServerOverloaded => CodexErrorInfo::ServerOverloaded,
|
||||
CoreCodexErrorInfo::ModelNotFound => CodexErrorInfo::ModelNotFound,
|
||||
CoreCodexErrorInfo::PolicyCapacityUnavailable => {
|
||||
CodexErrorInfo::PolicyCapacityUnavailable
|
||||
}
|
||||
CoreCodexErrorInfo::CyberPolicy => CodexErrorInfo::CyberPolicy,
|
||||
CoreCodexErrorInfo::BioPolicy => CodexErrorInfo::Other,
|
||||
CoreCodexErrorInfo::MisalignmentPolicyViolation => {
|
||||
|
||||
@@ -4796,6 +4796,10 @@ fn codex_error_info_serializes_http_status_code_in_camel_case() {
|
||||
#[test]
|
||||
fn core_error_info_converts_to_camel_case() {
|
||||
for (core, expected) in [
|
||||
(
|
||||
CoreCodexErrorInfo::PolicyCapacityUnavailable,
|
||||
json!("policyCapacityUnavailable"),
|
||||
),
|
||||
(CoreCodexErrorInfo::CyberPolicy, json!("cyberPolicy")),
|
||||
(CoreCodexErrorInfo::BioPolicy, json!("other")),
|
||||
(CoreCodexErrorInfo::InvalidPrompt, json!("other")),
|
||||
|
||||
@@ -2444,7 +2444,8 @@ fn websocket_error_detail(err: &ApiError) -> String {
|
||||
| ApiError::BioPolicy { .. }
|
||||
| ApiError::MisalignmentPolicyViolation { .. }
|
||||
| ApiError::FlexUnavailable
|
||||
| ApiError::ServerOverloaded { .. } => format!("handshake error: {err}"),
|
||||
| ApiError::ServerOverloaded { .. }
|
||||
| ApiError::PolicyCapacityUnavailable { .. } => format!("handshake error: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -2027,8 +2027,14 @@ async fn websocket_rate_limit_without_retry_after_is_terminal() -> Result<()> {
|
||||
|
||||
/// When an overload stops giving advice, it is terminal even if the retry limit
|
||||
/// has been reached. The transport must not switch after that terminal error.
|
||||
#[test_case::test_case("policy_capacity_unavailable", "server_is_overloaded", CodexErrorInfo::ServerOverloaded; "prior_policy_failure_stays_on_its_attempt")]
|
||||
#[test_case::test_case("server_is_overloaded", "policy_capacity_unavailable", CodexErrorInfo::PolicyCapacityUnavailable; "terminal_policy_failure_is_preserved")]
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn websocket_overload_retries_only_while_it_has_retry_after() -> Result<()> {
|
||||
async fn websocket_overload_retries_only_while_it_has_retry_after(
|
||||
first_code: &str,
|
||||
last_code: &str,
|
||||
expected_error: CodexErrorInfo,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let mut telemetry = RetryTelemetryCapture::install();
|
||||
@@ -2042,7 +2048,7 @@ async fn websocket_overload_retries_only_while_it_has_retry_after() -> Result<()
|
||||
"type": "error",
|
||||
"status": 503,
|
||||
"error": {
|
||||
"code": "server_is_overloaded",
|
||||
"code": first_code,
|
||||
"message": "This model is disabled.",
|
||||
"headers": { "Retry-After": "1" }
|
||||
}
|
||||
@@ -2061,7 +2067,7 @@ async fn websocket_overload_retries_only_while_it_has_retry_after() -> Result<()
|
||||
vec![vec![json!({
|
||||
"type": "error",
|
||||
"status": 503,
|
||||
"error": {"code": "server_is_overloaded", "message": "This model is disabled."}
|
||||
"error": {"code": last_code, "message": "This model is disabled."}
|
||||
})]],
|
||||
])
|
||||
.await;
|
||||
@@ -2095,13 +2101,14 @@ async fn websocket_overload_retries_only_while_it_has_retry_after() -> Result<()
|
||||
match wait_for_event(&test.codex, |_| true).await {
|
||||
EventMsg::Error(error) => {
|
||||
error_events += 1;
|
||||
assert_eq!(
|
||||
error.codex_error_info,
|
||||
Some(CodexErrorInfo::ServerOverloaded)
|
||||
);
|
||||
assert_eq!(error.codex_error_info, Some(expected_error.clone()));
|
||||
assert_eq!(
|
||||
error.message,
|
||||
"Selected model is at capacity. Please try a different model."
|
||||
if last_code == "policy_capacity_unavailable" {
|
||||
"Policy-restricted capacity is unavailable. Please try again later."
|
||||
} else {
|
||||
"Selected model is at capacity. Please try a different model."
|
||||
}
|
||||
);
|
||||
}
|
||||
EventMsg::StreamError(_) => stream_error_events += 1,
|
||||
@@ -2113,7 +2120,7 @@ async fn websocket_overload_retries_only_while_it_has_retry_after() -> Result<()
|
||||
EventMsg::TurnComplete(event) => {
|
||||
assert_eq!(
|
||||
event.error.and_then(|error| error.codex_error_info),
|
||||
Some(CodexErrorInfo::ServerOverloaded)
|
||||
Some(expected_error.clone())
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@ fn should_retry_guardian_review(outcome: &GuardianReviewOutcome) -> bool {
|
||||
CodexErrorInfo::RateLimitExceeded
|
||||
| CodexErrorInfo::FlexUnavailable
|
||||
| CodexErrorInfo::ServerOverloaded
|
||||
| CodexErrorInfo::PolicyCapacityUnavailable
|
||||
| CodexErrorInfo::InternalServerError => true,
|
||||
CodexErrorInfo::HttpConnectionFailed { http_status_code }
|
||||
| CodexErrorInfo::ResponseStreamConnectionFailed { http_status_code }
|
||||
|
||||
@@ -46,6 +46,7 @@ fn guardian_review_retry_only_retries_recoverable_errors() {
|
||||
};
|
||||
let transient_error_info = [
|
||||
CodexErrorInfo::ServerOverloaded,
|
||||
CodexErrorInfo::PolicyCapacityUnavailable,
|
||||
CodexErrorInfo::FlexUnavailable,
|
||||
CodexErrorInfo::HttpConnectionFailed {
|
||||
http_status_code: Some(502),
|
||||
|
||||
@@ -58,6 +58,7 @@ pub(super) fn sampler_failure_reason(error: &LunaSamplerError) -> &'static str {
|
||||
| ApiError::BioPolicy { .. }
|
||||
| ApiError::MisalignmentPolicyViolation { .. } => "policy_error",
|
||||
ApiError::ServerOverloaded { .. } => "server_overloaded",
|
||||
ApiError::PolicyCapacityUnavailable { .. } => "policy_capacity_unavailable",
|
||||
ApiError::FlexUnavailable => "flex_unavailable",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ impl SamplingExecution {
|
||||
| ApiError::Stream(_)
|
||||
| ApiError::ContentFilter
|
||||
| ApiError::ServerOverloaded { .. }
|
||||
| ApiError::PolicyCapacityUnavailable { .. }
|
||||
| ApiError::FlexUnavailable,
|
||||
)
|
||||
| LunaSamplerError::Api(ApiError::Transport(
|
||||
|
||||
@@ -24,6 +24,8 @@ enum CodexErrorInfoWire {
|
||||
FlexUnavailable,
|
||||
ServerOverloaded,
|
||||
ModelNotFound,
|
||||
/// Capacity is unavailable on a route restricted by server policy.
|
||||
PolicyCapacityUnavailable,
|
||||
CyberPolicy,
|
||||
BioPolicy,
|
||||
MisalignmentPolicyViolation,
|
||||
|
||||
@@ -168,6 +168,9 @@ pub enum CodexErrorDetails {
|
||||
UsageLimitReached(UsageLimitReachedError),
|
||||
#[error("Selected model is at capacity. Please try a different model.")]
|
||||
ServerOverloaded,
|
||||
/// Policy-restricted capacity follows the same retry advice as ordinary overloads.
|
||||
#[error("Policy-restricted capacity is unavailable. Please try again later.")]
|
||||
PolicyCapacityUnavailable,
|
||||
#[error("Flex capacity unavailable.")]
|
||||
FlexUnavailable,
|
||||
#[error("{message}")]
|
||||
@@ -445,9 +448,9 @@ impl CodexErr {
|
||||
| CodexErrorDetails::CyberPolicy { .. }
|
||||
| CodexErrorDetails::BioPolicy { .. }
|
||||
| CodexErrorDetails::MisalignmentPolicyViolation { .. } => None,
|
||||
CodexErrorDetails::ServerOverloaded | CodexErrorDetails::RetryLimit(_) => {
|
||||
self.server_retry_delay()
|
||||
}
|
||||
CodexErrorDetails::ServerOverloaded
|
||||
| CodexErrorDetails::PolicyCapacityUnavailable
|
||||
| CodexErrorDetails::RetryLimit(_) => self.server_retry_delay(),
|
||||
CodexErrorDetails::Stream(..)
|
||||
| CodexErrorDetails::ContentFilter
|
||||
| CodexErrorDetails::RateLimitExceeded(_)
|
||||
@@ -503,6 +506,9 @@ impl CodexErr {
|
||||
| CodexErrorDetails::QuotaExceeded
|
||||
| CodexErrorDetails::UsageNotIncluded => CodexErrorInfo::UsageLimitExceeded,
|
||||
CodexErrorDetails::ServerOverloaded => CodexErrorInfo::ServerOverloaded,
|
||||
CodexErrorDetails::PolicyCapacityUnavailable => {
|
||||
CodexErrorInfo::PolicyCapacityUnavailable
|
||||
}
|
||||
CodexErrorDetails::FlexUnavailable => CodexErrorInfo::FlexUnavailable,
|
||||
CodexErrorDetails::CyberPolicy { .. } => CodexErrorInfo::CyberPolicy,
|
||||
CodexErrorDetails::BioPolicy { .. } => CodexErrorInfo::BioPolicy,
|
||||
|
||||
@@ -2049,6 +2049,8 @@ pub enum CodexErrorInfo {
|
||||
FlexUnavailable,
|
||||
ServerOverloaded,
|
||||
ModelNotFound,
|
||||
/// Capacity is unavailable on a route restricted by server policy.
|
||||
PolicyCapacityUnavailable,
|
||||
CyberPolicy,
|
||||
BioPolicy,
|
||||
MisalignmentPolicyViolation,
|
||||
@@ -2095,6 +2097,7 @@ impl CodexErrorInfo {
|
||||
| Self::FlexUnavailable
|
||||
| Self::ServerOverloaded
|
||||
| Self::ModelNotFound
|
||||
| Self::PolicyCapacityUnavailable
|
||||
| Self::CyberPolicy
|
||||
| Self::BioPolicy
|
||||
| Self::MisalignmentPolicyViolation
|
||||
|
||||
@@ -93,6 +93,9 @@ pub fn telemetry_api_error_message(error: &ApiError) -> String {
|
||||
ApiError::BioPolicy { .. } => "bio policy".to_string(),
|
||||
ApiError::MisalignmentPolicyViolation { .. } => "misalignment policy violation".to_string(),
|
||||
ApiError::ServerOverloaded { .. } => "server overloaded".to_string(),
|
||||
ApiError::PolicyCapacityUnavailable { .. } => {
|
||||
"policy-restricted capacity unavailable".to_string()
|
||||
}
|
||||
ApiError::FlexUnavailable => "flex capacity unavailable".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,10 @@ pub(super) fn app_server_rate_limit_error_kind(
|
||||
info: &AppServerCodexErrorInfo,
|
||||
) -> Option<RateLimitErrorKind> {
|
||||
match info {
|
||||
AppServerCodexErrorInfo::ServerOverloaded => Some(RateLimitErrorKind::ServerOverloaded),
|
||||
AppServerCodexErrorInfo::ServerOverloaded
|
||||
| AppServerCodexErrorInfo::PolicyCapacityUnavailable => {
|
||||
Some(RateLimitErrorKind::ServerOverloaded)
|
||||
}
|
||||
AppServerCodexErrorInfo::UsageLimitExceeded => Some(RateLimitErrorKind::UsageLimit),
|
||||
AppServerCodexErrorInfo::ResponseTooManyFailedAttempts {
|
||||
http_status_code: Some(429),
|
||||
|
||||
@@ -1889,46 +1889,63 @@ async fn live_app_server_rate_limit_error_renders_upstream_message() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_app_server_server_overloaded_error_renders_error() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
for (message, error_info) in [
|
||||
("server overloaded", CodexErrorInfo::ServerOverloaded),
|
||||
(
|
||||
"server overloaded",
|
||||
CodexErrorInfo::PolicyCapacityUnavailable,
|
||||
),
|
||||
("", CodexErrorInfo::ServerOverloaded),
|
||||
("", CodexErrorInfo::PolicyCapacityUnavailable),
|
||||
] {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TurnStarted(TurnStartedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: AppServerTurn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
error: None,
|
||||
started_at: Some(0),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
drain_insert_history(&mut rx);
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TurnStarted(TurnStartedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: AppServerTurn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
error: None,
|
||||
started_at: Some(0),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: "server overloaded".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
|
||||
additional_details: None,
|
||||
},
|
||||
will_retry: false,
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
misalignment: None,
|
||||
message: message.to_string(),
|
||||
codex_error_info: Some(error_info),
|
||||
additional_details: None,
|
||||
},
|
||||
will_retry: false,
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
assert_eq!(lines_to_single_string(&cells[0]), "■ server overloaded\n");
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
insta::allow_duplicates! {
|
||||
if message.is_empty() {
|
||||
insta::assert_snapshot!(rendered, @"■ Codex is currently experiencing high load.");
|
||||
} else {
|
||||
insta::assert_snapshot!(rendered, @"■ server overloaded");
|
||||
}
|
||||
}
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -513,6 +513,7 @@ class CodexErrorInfoValue(str, Enum):
|
||||
flex_unavailable = "flexUnavailable"
|
||||
server_overloaded = "serverOverloaded"
|
||||
model_not_found = "modelNotFound"
|
||||
policy_capacity_unavailable = "policyCapacityUnavailable"
|
||||
cyber_policy = "cyberPolicy"
|
||||
misalignment_policy_violation = "misalignmentPolicyViolation"
|
||||
too_many_denials = "tooManyDenials"
|
||||
|
||||
Reference in New Issue
Block a user