This commit is contained in:
Ahmed Ibrahim
2025-11-25 14:18:41 -08:00
parent 53ff941cf3
commit 70b613be81

266
codex-rs/core/src/status.rs Normal file
View File

@@ -0,0 +1,266 @@
use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use codex_client::HttpTransport;
use codex_client::Request;
use codex_client::ReqwestTransport;
use http::header::CONTENT_TYPE;
use reqwest::Method;
use serde::Deserialize;
use serde::Serialize;
use strum_macros::Display;
use tokio::time::Instant;
const STATUS_WIDGET_URL: &str = "https://status.openai.com/proxy/status.openai.com";
const CODEX_COMPONENT_NAME: &str = "Codex";
const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Display, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ComponentHealth {
Operational,
DegradedPerformance,
PartialOutage,
MajorOutage,
UnderMaintenance,
#[serde(other)]
Unknown,
}
impl ComponentHealth {
fn operational() -> Self {
Self::Operational
}
pub(crate) fn is_operational(self) -> bool {
self == Self::Operational
}
}
pub(crate) struct IdleWarning {
last_event: Instant,
idle_timeout: Duration,
warning_sent: bool,
}
impl IdleWarning {
pub(crate) fn new(idle_timeout: Duration) -> Self {
Self {
last_event: Instant::now(),
idle_timeout,
warning_sent: false,
}
}
pub(crate) fn deadline(&self) -> Instant {
self.last_event + self.idle_timeout
}
pub(crate) fn mark_event(&mut self) {
self.last_event = Instant::now();
}
pub(crate) async fn maybe_warning_message(&mut self) -> Option<String> {
if self.warning_sent {
return None;
}
if let Ok(status) = fetch_codex_health().await {
if !status.is_operational() {
self.warning_sent = true;
return Some(format!(
"OpenAI status: {status:?}. Responses may be delayed or stalled."
));
}
}
None
}
}
impl Default for IdleWarning {
fn default() -> Self {
Self::new(DEFAULT_IDLE_TIMEOUT)
}
}
async fn fetch_codex_health() -> Result<ComponentHealth> {
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(10))
.build()
.context("building HTTP client")?;
let response = ReqwestTransport::new(client)
.execute(Request::new(Method::GET, STATUS_WIDGET_URL.to_string()))
.await
.context("requesting status widget")?;
let content_type = response
.headers
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_ascii_lowercase();
if !content_type.contains("json") {
let snippet = String::from_utf8_lossy(&response.body)
.chars()
.take(200)
.collect::<String>();
bail!(
"Expected JSON from {STATUS_WIDGET_URL}: Content-Type={content_type}. Body starts with: {snippet:?}"
);
}
let payload: StatusPayload =
serde_json::from_slice(&response.body).context("parsing status widget JSON")?;
derive_component_health(&payload, CODEX_COMPONENT_NAME)
}
#[derive(Debug, Clone, Deserialize, Default)]
struct StatusPayload {
#[serde(default)]
summary: Summary,
}
#[derive(Debug, Clone, Deserialize, Default)]
struct Summary {
#[serde(default)]
components: Vec<Component>,
#[serde(default)]
affected_components: Vec<AffectedComponent>,
}
#[derive(Debug, Clone, Deserialize)]
struct Component {
id: String,
name: String,
}
#[derive(Debug, Clone, Deserialize)]
struct AffectedComponent {
component_id: String,
#[serde(default = "ComponentHealth::operational")]
status: ComponentHealth,
}
fn derive_component_health(
payload: &StatusPayload,
component_name: &str,
) -> Result<ComponentHealth> {
let component = payload
.summary
.components
.iter()
.find(|component| component.name == component_name)
.ok_or_else(|| anyhow!("Component {component_name:?} not found in status summary"))?;
let status = payload
.summary
.affected_components
.iter()
.find(|affected| affected.component_id == component.id)
.map(|affected| affected.status)
.unwrap_or(ComponentHealth::Operational);
Ok(status)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn defaults_to_operational_when_not_affected() {
let payload = serde_json::from_value::<StatusPayload>(json!({
"summary": {
"id": "sum-1",
"name": "OpenAI",
"components": [
{"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"},
{"id": "cmp-2", "name": "Chat", "status_page_id": "page-1"}
]
}
}))
.expect("valid payload");
let status = derive_component_health(&payload, "Codex").expect("codex component exists");
assert_eq!(status, ComponentHealth::Operational);
assert!(status.is_operational());
}
#[test]
fn uses_affected_component_status() {
let payload = serde_json::from_value::<StatusPayload>(json!({
"summary": {
"id": "sum-1",
"name": "OpenAI",
"components": [
{"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"}
],
"affected_components": [
{"component_id": "cmp-1", "status": "major_outage"}
]
}
}))
.expect("valid payload");
let status = derive_component_health(&payload, "Codex").expect("codex component exists");
assert_eq!(status, ComponentHealth::MajorOutage);
assert!(!status.is_operational());
}
#[test]
fn unknown_status_is_preserved_as_unknown() {
let payload = serde_json::from_value::<StatusPayload>(json!({
"summary": {
"id": "sum-1",
"name": "OpenAI",
"components": [
{"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"}
],
"affected_components": [
{"component_id": "cmp-1", "status": "custom_status"}
]
}
}))
.expect("valid payload");
let status = derive_component_health(&payload, "Codex").expect("codex component exists");
assert_eq!(status, ComponentHealth::Unknown);
assert!(!status.is_operational());
}
#[test]
fn missing_component_returns_error() {
let payload = serde_json::from_value::<StatusPayload>(json!({
"summary": {
"id": "sum-1",
"name": "OpenAI",
"components": [],
"affected_components": []
}
}))
.expect("valid payload");
let error =
derive_component_health(&payload, "Codex").expect_err("missing component should error");
assert!(
error
.to_string()
.contains("Component \"Codex\" not found in status summary")
);
}
}