Download codex-rs/codex-api/src/api_bridge_tests.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 19.3 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/codex-api/src/api_bridge_tests.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/codex-api/src/api_bridge_tests.rs
-
curl -L -o api_bridge_tests.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/codex-api/src/api_bridge_tests.rs
19.3 kB
| use super::*; | |
| use base64::Engine; | |
| use codex_protocol::protocol::CodexErrorInfo; | |
| use codex_protocol::protocol::RateLimitReachedType; | |
| use pretty_assertions::assert_eq; | |
| fn map_api_error_maps_server_overloaded() { | |
| let err = map_api_error(ApiError::ServerOverloaded); | |
| assert!(matches!(err.details(), CodexErrorDetails::ServerOverloaded)); | |
| } | |
| fn map_api_error_preserves_retry_delay() { | |
| let retry_delay = std::time::Duration::from_secs(17); | |
| for (error, expected_code, expected_message) in [ | |
| ( | |
| ApiError::Retryable { | |
| message: "retry later".to_string(), | |
| delay: Some(retry_delay), | |
| }, | |
| CodexErrorInfo::Other, | |
| "stream disconnected before completion: retry later", | |
| ), | |
| ( | |
| ApiError::RateLimitExceeded { | |
| message: "retry later".to_string(), | |
| delay: Some(retry_delay), | |
| }, | |
| CodexErrorInfo::RateLimitExceeded, | |
| "rate limit exceeded: retry later", | |
| ), | |
| ] { | |
| let err = map_api_error(error); | |
| assert_eq!( | |
| ( | |
| err.to_codex_protocol_error(), | |
| err.retry_delay(), | |
| err.is_retryable(), | |
| err.http_status_code_value(), | |
| err.to_string(), | |
| ), | |
| ( | |
| expected_code, | |
| Some(retry_delay), | |
| true, | |
| None, | |
| expected_message.to_string(), | |
| ) | |
| ); | |
| } | |
| } | |
| fn map_api_error_distinguishes_capacity_from_slow_down() { | |
| for (code, expected, retryable) in [ | |
| ( | |
| "server_is_overloaded", | |
| CodexErrorInfo::ServerOverloaded, | |
| false, | |
| ), | |
| ("slow_down", CodexErrorInfo::RateLimitExceeded, true), | |
| ("unknown_error", CodexErrorInfo::Other, true), | |
| ] { | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::SERVICE_UNAVAILABLE, | |
| url: None, | |
| headers: None, | |
| body: Some( | |
| serde_json::json!({"error": {"code": code, "message": "retry later"}}).to_string(), | |
| ), | |
| })); | |
| assert_eq!( | |
| (err.to_codex_protocol_error(), err.is_retryable()), | |
| (expected, retryable) | |
| ); | |
| } | |
| } | |
| fn map_api_error_maps_cloudflare_blocked_response_to_user_message() { | |
| let mut headers = HeaderMap::new(); | |
| headers.insert(CF_RAY_HEADER, http::HeaderValue::from_static("ray-id")); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::FORBIDDEN, | |
| url: Some("http://example.com/blocked".to_string()), | |
| headers: Some(headers), | |
| body: Some( | |
| "<html><body>Cloudflare error: Sorry, you have been blocked</body></html>".to_string(), | |
| ), | |
| })); | |
| let CodexErrorDetails::UnexpectedStatus(err) = err.details() else { | |
| panic!("expected CodexErrorDetails::UnexpectedStatus, got {err:?}"); | |
| }; | |
| assert_eq!( | |
| err.user_message.as_deref(), | |
| Some( | |
| "Access blocked by Cloudflare. This usually happens when connecting from a restricted region (status 403 Forbidden)" | |
| ) | |
| ); | |
| assert_eq!( | |
| err.to_string(), | |
| "Access blocked by Cloudflare. This usually happens when connecting from a restricted region (status 403 Forbidden), url: http://example.com/blocked, cf-ray: ray-id" | |
| ); | |
| } | |
| fn map_api_error_maps_cyber_policy_from_400_body() { | |
| let body = serde_json::json!({ | |
| "error": { | |
| "message": "This request has been flagged for potentially high-risk cyber activity.", | |
| "type": "invalid_request", | |
| "param": null, | |
| "code": "cyber_policy" | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::BAD_REQUEST, | |
| url: Some("http://example.com/v1/responses".to_string()), | |
| headers: None, | |
| body: Some(body), | |
| })); | |
| let CodexErrorDetails::CyberPolicy { message } = err.details() else { | |
| panic!("expected CodexErrorDetails::CyberPolicy, got {err:?}"); | |
| }; | |
| assert_eq!( | |
| message, | |
| "This request has been flagged for potentially high-risk cyber activity." | |
| ); | |
| } | |
| fn map_api_error_maps_wrapped_websocket_cyber_policy_from_400_body() { | |
| let body = serde_json::json!({ | |
| "type": "error", | |
| "status": 400, | |
| "error": { | |
| "message": "This websocket request was flagged.", | |
| "type": "invalid_request", | |
| "code": "cyber_policy" | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::BAD_REQUEST, | |
| url: Some("ws://example.com/v1/responses".to_string()), | |
| headers: None, | |
| body: Some(body), | |
| })); | |
| let CodexErrorDetails::CyberPolicy { message } = err.details() else { | |
| panic!("expected CodexErrorDetails::CyberPolicy, got {err:?}"); | |
| }; | |
| assert_eq!(message, "This websocket request was flagged."); | |
| } | |
| fn map_api_error_uses_cyber_policy_fallback_for_missing_message() { | |
| let body = serde_json::json!({ | |
| "error": { | |
| "code": "cyber_policy" | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::BAD_REQUEST, | |
| url: Some("http://example.com/v1/responses".to_string()), | |
| headers: None, | |
| body: Some(body), | |
| })); | |
| let CodexErrorDetails::CyberPolicy { message } = err.details() else { | |
| panic!("expected CodexErrorDetails::CyberPolicy, got {err:?}"); | |
| }; | |
| assert_eq!( | |
| message, | |
| "This request has been flagged for possible cybersecurity risk." | |
| ); | |
| } | |
| fn map_api_error_maps_misalignment_policy_violation_from_400_body() { | |
| assert_misalignment_policy_violation_from_http_body(http::StatusCode::BAD_REQUEST); | |
| } | |
| fn map_api_error_maps_misalignment_policy_violation_from_403_body() { | |
| assert_misalignment_policy_violation_from_http_body(http::StatusCode::FORBIDDEN); | |
| } | |
| fn assert_misalignment_policy_violation_from_http_body(status: http::StatusCode) { | |
| let body = serde_json::json!({ | |
| "error": { | |
| "message": "This request violated the misalignment policy.", | |
| "type": "invalid_request_error", | |
| "code": "misalignment_policy_violation" | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status, | |
| url: Some("http://example.com/v1/responses".to_string()), | |
| headers: None, | |
| body: Some(body), | |
| })); | |
| let CodexErrorDetails::MisalignmentPolicyViolation { | |
| message, | |
| misalignment, | |
| } = err.details() | |
| else { | |
| panic!("expected CodexErrorDetails::MisalignmentPolicyViolation, got {err:?}"); | |
| }; | |
| assert_eq!(message, "This request violated the misalignment policy."); | |
| assert_eq!(misalignment, &None); | |
| assert!(!err.is_retryable()); | |
| } | |
| fn map_api_error_preserves_misalignment_details_from_403_body() { | |
| let body = serde_json::json!({ | |
| "error": { | |
| "message": "This request violated the misalignment policy.", | |
| "code": "misalignment_policy_violation", | |
| "misalignment": { | |
| "error_type": "unauthorized_data_transfer", | |
| "detailed_explanation": "The agent attempted an external transfer.", | |
| "steer": { "message": "Do not transfer the user's files." } | |
| } | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::FORBIDDEN, | |
| url: Some("http://example.com/v1/responses".to_string()), | |
| headers: None, | |
| body: Some(body), | |
| })); | |
| let CodexErrorDetails::MisalignmentPolicyViolation { | |
| message, | |
| misalignment, | |
| } = err.details() | |
| else { | |
| panic!("expected CodexErrorDetails::MisalignmentPolicyViolation, got {err:?}"); | |
| }; | |
| assert_eq!(message, "This request violated the misalignment policy."); | |
| assert_eq!( | |
| misalignment, | |
| &Some(MisalignmentErrorDetails { | |
| error_type: Some("unauthorized_data_transfer".to_string()), | |
| detailed_explanation: Some("The agent attempted an external transfer.".to_string()), | |
| steer: Some(codex_protocol::protocol::MisalignmentSteer { | |
| message: "Do not transfer the user's files.".to_string(), | |
| }), | |
| }) | |
| ); | |
| assert!(!err.is_retryable()); | |
| } | |
| fn map_api_error_preserves_misalignment_details_from_wrapped_websocket_error() { | |
| let body = serde_json::json!({ | |
| "type": "error", | |
| "status": 403, | |
| "error": { | |
| "message": "This websocket request violated the misalignment policy.", | |
| "code": "misalignment_policy_violation", | |
| "misalignment": { | |
| "error_type": "future_safety_category", | |
| "detailed_explanation": "The agent attempted an external transfer.", | |
| "steer": { "message": "Do not transfer the user's files." } | |
| } | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::FORBIDDEN, | |
| url: Some("ws://example.com/v1/responses".to_string()), | |
| headers: None, | |
| body: Some(body), | |
| })); | |
| let CodexErrorDetails::MisalignmentPolicyViolation { | |
| message, | |
| misalignment, | |
| } = err.details() | |
| else { | |
| panic!("expected CodexErrorDetails::MisalignmentPolicyViolation, got {err:?}"); | |
| }; | |
| assert_eq!( | |
| message, | |
| "This websocket request violated the misalignment policy." | |
| ); | |
| assert_eq!( | |
| misalignment, | |
| &Some(MisalignmentErrorDetails { | |
| error_type: Some("future_safety_category".to_string()), | |
| detailed_explanation: Some("The agent attempted an external transfer.".to_string()), | |
| steer: Some(codex_protocol::protocol::MisalignmentSteer { | |
| message: "Do not transfer the user's files.".to_string(), | |
| }), | |
| }) | |
| ); | |
| assert!(!err.is_retryable()); | |
| } | |
| fn map_api_error_keeps_unknown_400_errors_generic() { | |
| let body = serde_json::json!({ | |
| "error": { | |
| "message": "Some other bad request.", | |
| "code": "some_other_policy" | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::BAD_REQUEST, | |
| url: Some("http://example.com/v1/responses".to_string()), | |
| headers: None, | |
| body: Some(body.clone()), | |
| })); | |
| let CodexErrorDetails::InvalidRequest(message) = err.details() else { | |
| panic!("expected CodexErrorDetails::InvalidRequest, got {err:?}"); | |
| }; | |
| assert_eq!(message, &body); | |
| } | |
| fn map_api_error_distinguishes_http_quota_errors_from_rate_limits() { | |
| for error in [ | |
| serde_json::json!({"type": "insufficient_quota"}), | |
| serde_json::json!({"code": "insufficient_quota"}), | |
| serde_json::json!({"code": "credit_balance_exhausted"}), | |
| serde_json::json!({"code": "organization_spend_limit_exceeded"}), | |
| serde_json::json!({"code": "project_spend_limit_exceeded"}), | |
| serde_json::json!({"code": "organization_usage_limit_exceeded"}), | |
| serde_json::json!({"type": "rate_limit_error", "code": "rate_limit_exceeded"}), | |
| serde_json::json!({"type": "rate_limit_error", "code": "slow_down"}), | |
| ] { | |
| let expected = if error["type"] == "rate_limit_error" { | |
| CodexErrorInfo::ResponseTooManyFailedAttempts { | |
| http_status_code: Some(429), | |
| } | |
| } else { | |
| CodexErrorInfo::UsageLimitExceeded | |
| }; | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::TOO_MANY_REQUESTS, | |
| url: None, | |
| headers: None, | |
| body: Some(serde_json::json!({"error": error}).to_string()), | |
| })); | |
| assert_eq!(err.to_codex_protocol_error(), expected, "{error}"); | |
| } | |
| } | |
| fn map_api_error_maps_usage_limit_limit_name_header() { | |
| let mut headers = HeaderMap::new(); | |
| headers.insert( | |
| ACTIVE_LIMIT_HEADER, | |
| http::HeaderValue::from_static("codex_other"), | |
| ); | |
| headers.insert( | |
| "x-codex-other-limit-name", | |
| http::HeaderValue::from_static("codex_other"), | |
| ); | |
| let body = serde_json::json!({ | |
| "error": { | |
| "type": "usage_limit_reached", | |
| "plan_type": "pro", | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::TOO_MANY_REQUESTS, | |
| url: Some("http://example.com/v1/responses".to_string()), | |
| headers: Some(headers), | |
| body: Some(body), | |
| })); | |
| let CodexErrorDetails::UsageLimitReached(usage_limit) = err.details() else { | |
| panic!("expected CodexErrorDetails::UsageLimitReached, got {err:?}"); | |
| }; | |
| assert_eq!( | |
| usage_limit | |
| .rate_limits | |
| .as_ref() | |
| .and_then(|snapshot| snapshot.limit_name.as_deref()), | |
| Some("codex_other") | |
| ); | |
| } | |
| fn map_api_error_does_not_fallback_limit_name_to_limit_id() { | |
| let mut headers = HeaderMap::new(); | |
| headers.insert( | |
| ACTIVE_LIMIT_HEADER, | |
| http::HeaderValue::from_static("codex_other"), | |
| ); | |
| let body = serde_json::json!({ | |
| "error": { | |
| "type": "usage_limit_reached", | |
| "plan_type": "pro", | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::TOO_MANY_REQUESTS, | |
| url: Some("http://example.com/v1/responses".to_string()), | |
| headers: Some(headers), | |
| body: Some(body), | |
| })); | |
| let CodexErrorDetails::UsageLimitReached(usage_limit) = err.details() else { | |
| panic!("expected CodexErrorDetails::UsageLimitReached, got {err:?}"); | |
| }; | |
| assert_eq!( | |
| usage_limit | |
| .rate_limits | |
| .as_ref() | |
| .and_then(|snapshot| snapshot.limit_name.as_deref()), | |
| None | |
| ); | |
| } | |
| fn map_api_error_copies_rate_limit_reached_type_to_usage_limit_snapshot() { | |
| for (active_limit, expected_limit_id) in [(None, "codex"), (Some("codex_other"), "codex_other")] | |
| { | |
| let mut headers = HeaderMap::new(); | |
| if let Some(active_limit) = active_limit { | |
| headers.insert( | |
| ACTIVE_LIMIT_HEADER, | |
| http::HeaderValue::from_static(active_limit), | |
| ); | |
| } | |
| for (name, value) in [ | |
| ("x-codex-credits-has-credits", "true"), | |
| ("x-codex-credits-unlimited", "false"), | |
| ("x-codex-credits-balance", ""), | |
| ( | |
| "x-codex-rate-limit-reached-type", | |
| "workspace_member_usage_limit_reached", | |
| ), | |
| ] { | |
| headers.insert(name, http::HeaderValue::from_static(value)); | |
| } | |
| let body = serde_json::json!({ | |
| "error": { | |
| "type": "usage_limit_reached", | |
| "plan_type": "pro", | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::TOO_MANY_REQUESTS, | |
| url: Some("http://example.com/v1/responses".to_string()), | |
| headers: Some(headers), | |
| body: Some(body), | |
| })); | |
| let CodexErrorDetails::UsageLimitReached(usage_limit) = err.details() else { | |
| panic!("expected CodexErrorDetails::UsageLimitReached, got {err:?}"); | |
| }; | |
| assert_eq!( | |
| usage_limit.rate_limit_reached_type, | |
| Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached) | |
| ); | |
| let snapshot = usage_limit | |
| .rate_limits | |
| .as_ref() | |
| .expect("usage limit snapshot"); | |
| assert_eq!(snapshot.limit_id.as_deref(), Some(expected_limit_id)); | |
| assert_eq!( | |
| snapshot.rate_limit_reached_type, | |
| Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached) | |
| ); | |
| assert_eq!( | |
| snapshot.credits.as_ref().map(|credits| ( | |
| credits.has_credits, | |
| credits.unlimited, | |
| credits.balance.as_deref() | |
| )), | |
| Some((true, false, None)) | |
| ); | |
| } | |
| } | |
| fn map_api_error_ignores_unparseable_rate_limit_reached_type_headers() { | |
| let values = [ | |
| http::HeaderValue::from_static("future_rate_limit_reached_type"), | |
| http::HeaderValue::from_bytes(&[0xff]).expect("valid opaque header value"), | |
| ]; | |
| for value in values { | |
| let mut headers = HeaderMap::new(); | |
| headers.insert("x-codex-rate-limit-reached-type", value); | |
| let body = serde_json::json!({ | |
| "error": { | |
| "type": "usage_limit_reached", | |
| "plan_type": "pro", | |
| } | |
| }) | |
| .to_string(); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::TOO_MANY_REQUESTS, | |
| url: Some("http://example.com/v1/responses".to_string()), | |
| headers: Some(headers), | |
| body: Some(body), | |
| })); | |
| let CodexErrorDetails::UsageLimitReached(usage_limit) = err.details() else { | |
| panic!("expected CodexErrorDetails::UsageLimitReached, got {err:?}"); | |
| }; | |
| assert_eq!(usage_limit.rate_limit_reached_type, None); | |
| } | |
| } | |
| fn map_api_error_extracts_identity_auth_details_from_headers() { | |
| let mut headers = HeaderMap::new(); | |
| headers.insert(REQUEST_ID_HEADER, http::HeaderValue::from_static("req-401")); | |
| headers.insert(CF_RAY_HEADER, http::HeaderValue::from_static("ray-401")); | |
| headers.insert( | |
| X_OPENAI_AUTHORIZATION_ERROR_HEADER, | |
| http::HeaderValue::from_static("missing_authorization_header"), | |
| ); | |
| let x_error_json = | |
| base64::engine::general_purpose::STANDARD.encode(r#"{"error":{"code":"token_expired"}}"#); | |
| headers.insert( | |
| X_ERROR_JSON_HEADER, | |
| http::HeaderValue::from_str(&x_error_json).expect("valid x-error-json header"), | |
| ); | |
| let err = map_api_error(ApiError::Transport(TransportError::Http { | |
| status: http::StatusCode::UNAUTHORIZED, | |
| url: Some("https://chatgpt.com/backend-api/codex/models".to_string()), | |
| headers: Some(headers), | |
| body: Some(r#"{"detail":"Unauthorized"}"#.to_string()), | |
| })); | |
| let CodexErrorDetails::UnexpectedStatus(err) = err.details() else { | |
| panic!("expected CodexErrorDetails::UnexpectedStatus, got {err:?}"); | |
| }; | |
| assert_eq!(err.request_id.as_deref(), Some("req-401")); | |
| assert_eq!(err.cf_ray.as_deref(), Some("ray-401")); | |
| assert_eq!( | |
| err.identity_authorization_error.as_deref(), | |
| Some("missing_authorization_header") | |
| ); | |
| assert_eq!(err.identity_error_code.as_deref(), Some("token_expired")); | |
| } | |