Skip to main content

klieo_llm_common/
http.rs

1//! HTTP-client factory shared by every LLM provider crate.
2
3use std::time::Duration;
4
5/// Tunable defaults applied to every provider's [`reqwest::Client`].
6///
7/// Provider crates clone [`DEFAULT_HTTP_DEFAULTS`] and override fields
8/// when they need per-provider tuning (e.g. a higher pool size for a
9/// bulk-embedding endpoint).
10#[derive(Debug, Clone)]
11pub struct HttpDefaults {
12    /// Whole-request timeout.
13    pub timeout: Duration,
14    /// TCP connect timeout.
15    pub connect_timeout: Duration,
16    /// Per-host keep-alive pool ceiling.
17    pub pool_max_idle_per_host: usize,
18    /// `User-Agent` header value. Empty string leaves reqwest's default.
19    pub user_agent: String,
20}
21
22const LLM_HTTP_TIMEOUT: Duration = Duration::from_secs(120);
23const LLM_HTTP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
24
25/// Workspace-wide defaults. Match the per-provider constants the
26/// individual crates used before this extraction (120s / 10s / 4).
27pub const DEFAULT_HTTP_DEFAULTS: HttpDefaults = HttpDefaults {
28    timeout: LLM_HTTP_TIMEOUT,
29    connect_timeout: LLM_HTTP_CONNECT_TIMEOUT,
30    pool_max_idle_per_host: 4,
31    user_agent: String::new(),
32};
33
34/// Build a [`reqwest::Client`] with [`HttpDefaults`] applied.
35///
36/// Cross-origin redirects are disabled (CWE-200): provider crates carry
37/// bearer tokens in their `Authorization` header and a follow-redirect
38/// to an attacker-controlled host would leak them.
39pub fn build_http_client(defaults: &HttpDefaults) -> reqwest::Client {
40    let mut builder = reqwest::Client::builder()
41        .timeout(defaults.timeout)
42        .connect_timeout(defaults.connect_timeout)
43        .pool_max_idle_per_host(defaults.pool_max_idle_per_host)
44        .redirect(reqwest::redirect::Policy::none());
45    if !defaults.user_agent.is_empty() {
46        builder = builder.user_agent(&defaults.user_agent);
47    }
48    builder
49        .build()
50        .expect("reqwest::Client::builder() with valid HttpDefaults must succeed")
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn defaults_match_pre_extraction_provider_constants() {
59        assert_eq!(DEFAULT_HTTP_DEFAULTS.timeout, Duration::from_secs(120));
60        assert_eq!(
61            DEFAULT_HTTP_DEFAULTS.connect_timeout,
62            Duration::from_secs(10)
63        );
64        assert_eq!(DEFAULT_HTTP_DEFAULTS.pool_max_idle_per_host, 4);
65    }
66
67    #[test]
68    fn build_http_client_returns_a_client() {
69        let _ = build_http_client(&DEFAULT_HTTP_DEFAULTS);
70    }
71
72    #[test]
73    fn build_http_client_with_user_agent() {
74        let cfg = HttpDefaults {
75            user_agent: "klieo-test/0.1".into(),
76            ..DEFAULT_HTTP_DEFAULTS
77        };
78        let _ = build_http_client(&cfg);
79    }
80}