agentix 0.21.0

Multi-provider LLM client for Rust — streaming, non-streaming, tool calls, MCP, DeepSeek, OpenAI, Anthropic, Gemini
Documentation
use tracing::warn;

use crate::error::ApiError;

// ── Shared HTTP POST helper ────────────────────────────────────────────────────

pub(crate) struct PostConfig {
    pub use_query_key: bool,
    pub auth_header: Option<&'static str>,
    pub extra_headers: &'static [(&'static str, &'static str)],
    pub max_retries: u32,
    pub retry_delay_ms: u64,
}

pub(crate) async fn post_streaming<T: serde::Serialize>(
    http: &reqwest::Client,
    url: &str,
    body: &T,
    token: &str,
    cfg: &PostConfig,
) -> Result<reqwest::Response, ApiError> {
    let use_query_key = cfg.use_query_key;
    let auth_header = cfg.auth_header;
    let extra_headers = cfg.extra_headers;
    let max_retries = cfg.max_retries;
    let retry_delay_ms = cfg.retry_delay_ms;
    let effective_url = if use_query_key {
        if url.contains('?') {
            format!("{}&key={}", url, token)
        } else {
            format!("{}?key={}", url, token)
        }
    } else {
        url.to_string()
    };

    let mut attempts = 0u32;
    let mut delay = retry_delay_ms;
    loop {
        let mut builder = http.post(&effective_url);
        if !use_query_key {
            match auth_header {
                Some(h) => {
                    builder = builder.header(h, token);
                }
                None => {
                    builder = builder.bearer_auth(token);
                }
            }
        }
        for &(name, value) in extra_headers {
            builder = builder.header(name, value);
        }
        #[cfg(feature = "sensitive-logs")]
        if crate::sensitive_logs_enabled() {
            let request_body = serde_json::to_string(body)
                .unwrap_or_else(|e| format!("<failed to serialize body: {e}>"));

            tracing::info!(
                url = %effective_url,
                body = %request_body,
                "sending POST request"
            );
        }

        builder = builder.json(body);

        match builder.send().await.map_err(ApiError::Network) {
            Ok(resp) if resp.status().is_success() => {
                tracing::info!(
                    url = %effective_url,
                    status = %resp.status(),
                    "received streaming HTTP response"
                );
                return Ok(resp);
            }
            Ok(resp) => {
                let status = resp.status();
                let b = resp.text().await.unwrap_or_else(|e| e.to_string());
                let err = ApiError::http(status, b);
                if err.is_retriable() && attempts < max_retries {
                    attempts += 1;
                    warn!(error = %err, attempt = attempts, "transient error, retrying in {}ms", delay);
                    tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
                    delay *= 2;
                } else {
                    return Err(err);
                }
            }
            Err(e) => {
                if e.is_retriable() && attempts < max_retries {
                    attempts += 1;
                    warn!(error = %e, attempt = attempts, "transient error, retrying in {}ms", delay);
                    tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
                    delay *= 2;
                } else {
                    return Err(e);
                }
            }
        }
    }
}

/// Like `post_streaming`, but expects a full JSON response body (non-streaming).
pub(crate) async fn post_json<T: serde::Serialize>(
    http: &reqwest::Client,
    url: &str,
    body: &T,
    token: &str,
    cfg: &PostConfig,
) -> Result<String, ApiError> {
    let resp = post_streaming(http, url, body, token, cfg).await?;
    let response_body = resp.text().await.map_err(ApiError::Network)?;
    #[cfg(feature = "sensitive-logs")]
    if crate::sensitive_logs_enabled() {
        tracing::info!(url = %url, body = %response_body, "received raw HTTP response body");
    }
    Ok(response_body)
}