inferencelayer 0.2.10

Kortexya's engine-native inference layer — LLM generation + embedding/encoder family on wgpu (WGSL kernels, any adapter) with a pure-Rust CPU fallback
Documentation
//! OpenAI-compatible error envelope for `lfm2-serve`.
//!
//! Every 4xx/5xx the serving binary returns carries the `{"error":{message,type,param,code}}`
//! shape that `openai-python`, the OpenAI TypeScript SDK and Open WebUI parse — so a rejected
//! request surfaces as a real typed client error (`BadRequestError`, `NotFoundError`, …) instead
//! of an opaque string body the client cannot classify. Building the envelope lives here, in the
//! library, so it is unit-testable without spinning up axum.

use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};

/// A structured API error rendered as the OpenAI error envelope.
///
/// Construct through the intent-named constructors ([`Self::invalid_request`], [`Self::not_found`],
/// …) so the HTTP status and OpenAI `type` stay in lockstep; attach [`Self::with_param`] /
/// [`Self::with_code`] when the offending field or a machine code is known.
#[derive(Debug, Clone)]
pub struct ApiError {
    pub status: StatusCode,
    pub message: String,
    /// OpenAI error `type` (`invalid_request_error`, `not_found_error`, …).
    pub kind: &'static str,
    /// The request field that caused the error, when identifiable (`temperature`, `model`, …).
    pub param: Option<String>,
    /// Machine-readable code (`model_not_found`, …), when one applies.
    pub code: Option<String>,
}

impl ApiError {
    /// 400 — a malformed or unsupported request (bad params, unimplemented feature).
    pub fn invalid_request(message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::BAD_REQUEST,
            message: message.into(),
            kind: "invalid_request_error",
            param: None,
            code: None,
        }
    }

    /// 404 — the named model (or route) does not exist on this server.
    pub fn not_found(message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::NOT_FOUND,
            message: message.into(),
            kind: "not_found_error",
            param: None,
            code: None,
        }
    }

    /// 503 — an engine thread is gone or overloaded; the request never ran.
    pub fn unavailable(message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::SERVICE_UNAVAILABLE,
            message: message.into(),
            kind: "service_unavailable",
            param: None,
            code: None,
        }
    }

    /// 500 — the engine failed mid-request (a bug or a GPU fault, not the caller's doing).
    pub fn internal(message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::INTERNAL_SERVER_ERROR,
            message: message.into(),
            kind: "internal_error",
            param: None,
            code: None,
        }
    }

    /// Name the offending request field (OpenAI `param`).
    pub fn with_param(mut self, param: impl Into<String>) -> Self {
        self.param = Some(param.into());
        self
    }

    /// Attach a machine-readable `code`.
    pub fn with_code(mut self, code: impl Into<String>) -> Self {
        self.code = Some(code.into());
        self
    }

    /// The `{"error":{…}}` JSON body OpenAI clients deserialize.
    pub fn envelope(&self) -> serde_json::Value {
        serde_json::json!({
            "error": {
                "message": self.message,
                "type": self.kind,
                "param": self.param,
                "code": self.code,
            }
        })
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        (self.status, Json(self.envelope())).into_response()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn should_render_openai_error_envelope_with_all_fields() {
        let e = ApiError::invalid_request("temperature must be in [0, 2]")
            .with_param("temperature")
            .with_code("out_of_range");
        let v = e.envelope();
        assert_eq!(v["error"]["message"], "temperature must be in [0, 2]");
        assert_eq!(v["error"]["type"], "invalid_request_error");
        assert_eq!(v["error"]["param"], "temperature");
        assert_eq!(v["error"]["code"], "out_of_range");
        assert_eq!(e.status, StatusCode::BAD_REQUEST);
    }

    #[test]
    fn should_null_absent_param_and_code() {
        // openai-python expects the keys PRESENT with null values, not omitted.
        let v = ApiError::not_found("model `foo` not found").envelope();
        assert!(v["error"]["param"].is_null());
        assert!(v["error"]["code"].is_null());
        assert_eq!(v["error"]["type"], "not_found_error");
    }

    #[test]
    fn should_map_intents_to_statuses() {
        assert_eq!(ApiError::not_found("x").status, StatusCode::NOT_FOUND);
        assert_eq!(
            ApiError::unavailable("x").status,
            StatusCode::SERVICE_UNAVAILABLE
        );
        assert_eq!(
            ApiError::internal("x").status,
            StatusCode::INTERNAL_SERVER_ERROR
        );
    }
}