openrouter-rs 0.9.0

A type-safe OpenRouter Rust SDK
Documentation
use http::StatusCode;
use reqwest::Response;
use serde::de::DeserializeOwned;
use serde_json::Value;

use crate::{
    api::errors::{parse_api_error, unreadable_error_response},
    error::OpenRouterError,
};

fn body_preview(body_text: &str, limit: usize) -> String {
    let normalized = body_text.replace('\r', "\\r").replace('\n', "\\n");
    let mut preview = String::new();
    let mut chars = normalized.chars();

    for _ in 0..limit {
        match chars.next() {
            Some(ch) => preview.push(ch),
            None => return preview,
        }
    }

    if chars.next().is_some() {
        preview.push_str("...");
    }

    preview
}

fn response_request_id(response: &Response) -> Option<String> {
    response
        .headers()
        .get("x-request-id")
        .and_then(|value| value.to_str().ok())
        .map(ToOwned::to_owned)
        .or_else(|| {
            response
                .headers()
                .get("request-id")
                .and_then(|value| value.to_str().ok())
                .map(ToOwned::to_owned)
        })
}

fn body_contains_api_error(body_text: &str) -> bool {
    serde_json::from_str::<Value>(body_text)
        .ok()
        .and_then(|value| value.get("error").cloned())
        .is_some()
}

pub(crate) fn response_deserialization_error(
    context: &str,
    status: StatusCode,
    error: &serde_json::Error,
    body_text: &str,
) -> OpenRouterError {
    OpenRouterError::Unknown(format!(
        "Failed to deserialize {context} response (status {status}): {error}; body preview: {}",
        body_preview(body_text, 240)
    ))
}

pub(crate) async fn parse_json_response<T: DeserializeOwned>(
    response: Response,
    context: &str,
) -> Result<T, OpenRouterError> {
    let status = response.status();
    let request_id = response_request_id(&response);
    let body_text = response.text().await?;

    match serde_json::from_str(&body_text) {
        Ok(parsed) => Ok(parsed),
        Err(error) => {
            if body_contains_api_error(&body_text) {
                Err(parse_api_error(status, request_id, &body_text))
            } else {
                Err(response_deserialization_error(
                    context, status, &error, &body_text,
                ))
            }
        }
    }
}

pub(crate) async fn error_from_response(response: Response) -> OpenRouterError {
    let status = response.status();
    let request_id = response_request_id(&response);
    let text = match response.text().await {
        Ok(text) => text,
        Err(error) => return unreadable_error_response(status, request_id, &error.to_string()),
    };

    parse_api_error(status, request_id, &text)
}

pub(crate) async fn handle_error(response: Response) -> Result<(), OpenRouterError> {
    Err(error_from_response(response).await)
}