stalwartlabs/stalwart · error · trc::Error(AiEvent::ApiError)

OpenAPI request failed

Error message

OpenAPI request failed

What it means

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.

Source

Thrown at crates/common/src/enterprise/llm.rs:94

    pub model: String,
    pub choices: Vec<TextCompletionChoice>,
}

#[derive(Deserialize, Debug)]
pub struct TextCompletionChoice {
    pub index: i32,
    pub finish_reason: String,
    pub text: String,
}

impl AiApiConfig {
    pub async fn send_request(
        &self,
        prompt: impl Into<String>,
        temperature: Option<f64>,
    ) -> trc::Result<String> {
        self.post_api(prompt, temperature).await.map_err(|err| {
            trc::Error::new(trc::EventType::Ai(trc::AiEvent::ApiError))
                .id(self.id.clone())
                .details("OpenAPI request failed")
                .reason(err)
        })
    }

    async fn post_api(
        &self,
        prompt: impl Into<String>,
        temperature: Option<f64>,
    ) -> Result<String, String> {
        // Serialize body
        let body = match self.api_type {
            ApiType::ChatCompletion => serde_json::to_string(&ChatCompletionRequest {
                model: self.model.to_string(),
                messages: vec![Message {
                    role: "user".to_string(),
                    content: prompt.into(),

View on GitHub (pinned to e962003857)

Solutions

  1. Inspect err.reason() for the underlying cause (HTTP status, connect error).
  2. Verify the API key and endpoint configuration for the LLM integration.
  3. Test connectivity to the endpoint with curl from the server host.
  4. Check quota/billing on the API provider account.
  5. Add retry with backoff for transient 429/5xx responses.

Example fix

// before (config)
[enterprise.llm]
api-key = "" // empty
// after
[enterprise.llm]
api-key = "sk-..."
url = "https://api.openai.com/v1/chat/completions"
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling, verify config is complete
if llm.api_key.is_empty() || llm.url.is_empty() { return Err(anyhow!("LLM not configured")); }

Try / catch

match llm.send_request(prompt, None).await {
    Ok(reply) => reply,
    Err(err) => { log::error!("LLM call failed: {:?} reason={:?}", err, err.reason()); fallback_response() },
}

Prevention

When it happens

Trigger: 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.

Common situations: Missing or expired OPENAI_API_KEY; enterprise LLM feature misconfigured (wrong base URL); network egress blocked; account out of credits.

Related errors


AI-assisted analysis of stalwartlabs/stalwart@e962003857 (2026-09-06). Data as JSON: /api/errors/abdf1ee77ccee63e. Report an issue: GitHub.