nanocodex-oai-api 0.3.0

Tower-native OpenAI Responses API and managed context for Nanocodex
Documentation
use std::{collections::HashMap, time::Duration};

use serde::Deserialize;

pub(super) fn retryable_api_error(event: &str) -> Option<(&'static str, Option<Duration>)> {
    let event: ApiErrorEnvelope = serde_json::from_str(event).ok()?;
    let error = event.error();
    let code = error.and_then(|error| error.code.as_deref());
    let discriminator = code.or_else(|| error.and_then(|error| error.kind.as_deref()));

    let class = match event.event_type.as_deref() {
        Some("response.incomplete") => "api_incomplete",
        Some("response.failed") => {
            if code.is_some_and(is_terminal_response_failure) {
                return None;
            }
            match discriminator {
                Some("rate_limit_exceeded") => "api_rate_limit",
                Some("server_error" | "websocket_connection_limit_reached") => "api_server",
                _ => "api_failed",
            }
        }
        _ => match discriminator {
            Some("server_is_overloaded" | "slow_down") => return None,
            Some("server_error" | "websocket_connection_limit_reached") => "api_server",
            Some("rate_limit_exceeded") => "api_rate_limit",
            _ => return None,
        },
    };

    let server_delay = error
        .and_then(|error| error.retry_after)
        .and_then(|seconds| Duration::try_from_secs_f64(seconds).ok())
        .or_else(|| retry_after_header(&event.headers));
    Some((class, server_delay))
}

pub(super) fn api_error_has_code(event: &str, expected: &str) -> bool {
    let Ok(event) = serde_json::from_str::<ApiErrorEnvelope>(event) else {
        return false;
    };
    event.error().and_then(|error| error.code.as_deref()) == Some(expected)
}

fn is_terminal_response_failure(code: &str) -> bool {
    matches!(
        code,
        "context_length_exceeded"
            | "insufficient_quota"
            | "usage_not_included"
            | "cyber_policy"
            | "invalid_prompt"
            | "bio_policy"
            | "server_is_overloaded"
            | "slow_down"
    )
}

fn retry_after_header(headers: &HashMap<String, RetryAfterValue>) -> Option<Duration> {
    headers
        .iter()
        .find(|(name, _)| name.eq_ignore_ascii_case("retry-after"))
        .and_then(|(_, value)| value.seconds())
        .and_then(|seconds| Duration::try_from_secs_f64(seconds).ok())
}

#[derive(Deserialize)]
struct ApiErrorEnvelope {
    #[serde(default, rename = "type")]
    event_type: Option<Box<str>>,
    #[serde(default)]
    error: Option<ApiErrorDetail>,
    #[serde(default)]
    response: Option<ApiErrorResponse>,
    #[serde(default)]
    headers: HashMap<String, RetryAfterValue>,
}

impl ApiErrorEnvelope {
    fn error(&self) -> Option<&ApiErrorDetail> {
        self.error
            .as_ref()
            .or_else(|| self.response.as_ref()?.error.as_ref())
    }
}

#[derive(Deserialize)]
struct ApiErrorResponse {
    #[serde(default)]
    error: Option<ApiErrorDetail>,
}

#[derive(Deserialize)]
struct ApiErrorDetail {
    #[serde(default, rename = "type")]
    kind: Option<Box<str>>,
    #[serde(default)]
    code: Option<Box<str>>,
    #[serde(default)]
    retry_after: Option<f64>,
}

#[derive(Deserialize)]
#[serde(untagged)]
enum RetryAfterValue {
    Number(f64),
    String(Box<str>),
}

impl RetryAfterValue {
    fn seconds(&self) -> Option<f64> {
        match self {
            Self::Number(seconds) => Some(*seconds),
            Self::String(seconds) => seconds.parse().ok(),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use super::retryable_api_error;

    #[test]
    fn retries_incomplete_responses() {
        let event = r#"{
            "type": "response.incomplete",
            "response": {
                "status": "incomplete",
                "incomplete_details": { "reason": "max_output_tokens" }
            },
            "headers": { "Retry-After": "1.25" }
        }"#;

        assert_eq!(
            retryable_api_error(event),
            Some(("api_incomplete", Some(Duration::from_millis(1_250))))
        );
    }

    #[test]
    fn retries_unknown_and_missing_failed_response_errors() {
        let unknown = r#"{
            "type": "response.failed",
            "response": {
                "error": {
                    "code": "new_provider_failure",
                    "message": "temporary failure"
                }
            }
        }"#;
        let missing = r#"{
            "type": "response.failed",
            "response": { "status": "failed", "error": null }
        }"#;

        assert_eq!(
            retryable_api_error(unknown).map(|(class, _)| class),
            Some("api_failed")
        );
        assert_eq!(
            retryable_api_error(missing).map(|(class, _)| class),
            Some("api_failed")
        );
    }

    #[test]
    fn overload_failures_are_terminal() {
        for code in ["server_is_overloaded", "slow_down"] {
            let failed = format!(
                r#"{{
                    "type": "response.failed",
                    "response": {{ "error": {{ "code": "{code}" }} }}
                }}"#
            );
            let error = format!(
                r#"{{
                    "type": "error",
                    "error": {{ "code": "{code}" }}
                }}"#
            );
            assert_eq!(retryable_api_error(&failed), None, "failed: {code}");
            assert_eq!(retryable_api_error(&error), None, "error: {code}");
        }
    }

    #[test]
    fn known_response_failures_remain_terminal() {
        for code in [
            "context_length_exceeded",
            "insufficient_quota",
            "usage_not_included",
            "cyber_policy",
            "invalid_prompt",
            "bio_policy",
        ] {
            let event = format!(
                r#"{{
                    "type": "response.failed",
                    "response": {{ "error": {{ "code": "{code}" }} }}
                }}"#
            );
            assert_eq!(retryable_api_error(&event), None, "{code}");
        }
    }

    #[test]
    fn top_level_unknown_errors_remain_terminal() {
        let event = r#"{
            "type": "error",
            "error": {
                "code": "invalid_request_error",
                "message": "reject this logical turn"
            }
        }"#;

        assert_eq!(retryable_api_error(event), None);
    }
}