{"record":{"id":"abdf1ee77ccee63e","repo":"stalwartlabs/stalwart","slug":"openapi-request-failed","errorCode":null,"errorMessage":"OpenAPI request failed","messagePattern":"OpenAPI request failed","errorType":"exception","errorClass":"trc::Error(AiEvent::ApiError)","httpStatus":null,"severity":"error","filePath":"crates/common/src/enterprise/llm.rs","lineNumber":94,"sourceCode":"    pub model: String,\n    pub choices: Vec<TextCompletionChoice>,\n}\n\n#[derive(Deserialize, Debug)]\npub struct TextCompletionChoice {\n    pub index: i32,\n    pub finish_reason: String,\n    pub text: String,\n}\n\nimpl AiApiConfig {\n    pub async fn send_request(\n        &self,\n        prompt: impl Into<String>,\n        temperature: Option<f64>,\n    ) -> trc::Result<String> {\n        self.post_api(prompt, temperature).await.map_err(|err| {\n            trc::Error::new(trc::EventType::Ai(trc::AiEvent::ApiError))\n                .id(self.id.clone())\n                .details(\"OpenAPI request failed\")\n                .reason(err)\n        })\n    }\n\n    async fn post_api(\n        &self,\n        prompt: impl Into<String>,\n        temperature: Option<f64>,\n    ) -> Result<String, String> {\n        // Serialize body\n        let body = match self.api_type {\n            ApiType::ChatCompletion => serde_json::to_string(&ChatCompletionRequest {\n                model: self.model.to_string(),\n                messages: vec![Message {\n                    role: \"user\".to_string(),\n                    content: prompt.into(),","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/stalwartlabs/stalwart/blob/e96200385781a6a9995a8b839ac27d6c75a983ee/crates/common/src/enterprise/llm.rs#L76-L112","documentation":"The enterprise LLM (OpenAI-compatible API) client wraps any failure from its underlying POST (post_api) into an AiEvent::ApiError with details \"OpenAPI request failed\", preserving the original error as reason. It signals that the prompt could not be completed by the remote API.","triggerScenarios":"Calling `send_request` when post_api fails: invalid/missing API key (401), unreachable host, timeout, quota exceeded, or malformed response from the OpenAI-compatible endpoint.","commonSituations":"Missing or expired OPENAI_API_KEY; enterprise LLM feature misconfigured (wrong base URL); network egress blocked; account out of credits.","solutions":["Inspect err.reason() for the underlying cause (HTTP status, connect error).","Verify the API key and endpoint configuration for the LLM integration.","Test connectivity to the endpoint with curl from the server host.","Check quota/billing on the API provider account.","Add retry with backoff for transient 429/5xx responses."],"exampleFix":"// before (config)\n[enterprise.llm]\napi-key = \"\" // empty\n// after\n[enterprise.llm]\napi-key = \"sk-...\"\nurl = \"https://api.openai.com/v1/chat/completions\"","handlingStrategy":"try-catch","validationCode":"// before calling, verify config is complete\nif llm.api_key.is_empty() || llm.url.is_empty() { return Err(anyhow!(\"LLM not configured\")); }","typeGuard":null,"tryCatchPattern":"match llm.send_request(prompt, None).await {\n    Ok(reply) => reply,\n    Err(err) => { log::error!(\"LLM call failed: {:?} reason={:?}\", err, err.reason()); fallback_response() },\n}","preventionTips":["Validate API key and endpoint config at startup, not per-call.","Pre-flight the endpoint with a cheap request during health checks.","Retry with backoff on 429/5xx; fail fast on 401/403.","Monitor provider quota and billing alerts."],"tags":["http","api","llm","network"],"backgroundTag":"api-error-response","analyzedSha":"e96200385781a6a9995a8b839ac27d6c75a983ee","analyzedAt":"2026-09-06T22:07:17.982Z","contentChangedAt":"2026-09-06T22:07:17.982Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}