From cc13fec16d897525b821aa224a2ea65a9ee4e41d Mon Sep 17 00:00:00 2001 From: Edvard Date: Sat, 14 Feb 2026 18:43:26 -0500 Subject: [PATCH] fix: add provider warmup to prevent cold-start timeout on first channel message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first API request after daemon startup consistently timed out (120s) when using channels (Telegram, Discord, etc.), requiring a retry before succeeding. This happened because the reqwest HTTP client's connection pool was cold — no TLS handshake, DNS resolution, or HTTP/2 negotiation had occurred yet. The fix adds a `warmup()` method to the Provider trait that establishes the connection pool on startup by hitting a lightweight endpoint (`/api/v1/auth/key` for OpenRouter). The channel server calls this immediately after creating the provider, before entering the message processing loop. Tested on Raspberry Pi 5 (aarch64) with OpenRouter + DeepSeek v3.2 via Telegram channel. Before: first message took 2-7 minutes (120s timeout + retries). After: first message responds in <30s with no retries. Co-Authored-By: Claude Opus 4.6 --- src/channels/mod.rs | 7 +++++++ src/providers/openrouter.rs | 16 ++++++++++++++++ src/providers/reliable.rs | 8 ++++++++ src/providers/traits.rs | 6 ++++++ 4 files changed, 37 insertions(+) diff --git a/src/channels/mod.rs b/src/channels/mod.rs index cb15934..396bd7d 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -426,6 +426,13 @@ pub async fn start_channels(config: Config) -> Result<()> { config.api_key.as_deref(), &config.reliability, )?); + + // Warm up the provider connection pool (TLS handshake, DNS, HTTP/2 setup) + // so the first real message doesn't hit a cold-start timeout. + if let Err(e) = provider.warmup().await { + tracing::warn!("Provider warmup failed (non-fatal): {e}"); + } + let model = config .default_model .clone() diff --git a/src/providers/openrouter.rs b/src/providers/openrouter.rs index 3d99481..b796ff5 100644 --- a/src/providers/openrouter.rs +++ b/src/providers/openrouter.rs @@ -51,6 +51,22 @@ impl OpenRouterProvider { #[async_trait] impl Provider for OpenRouterProvider { + async fn warmup(&self) -> anyhow::Result<()> { + // Hit a lightweight endpoint to establish TLS + HTTP/2 connection pool. + // This prevents the first real chat request from timing out on cold start. + let api_key = self + .api_key + .as_ref() + .ok_or_else(|| anyhow::anyhow!("No API key for warmup"))?; + let _ = self + .client + .get("https://openrouter.ai/api/v1/auth/key") + .header("Authorization", format!("Bearer {api_key}")) + .send() + .await; + Ok(()) + } + async fn chat_with_system( &self, system_prompt: Option<&str>, diff --git a/src/providers/reliable.rs b/src/providers/reliable.rs index c324f21..7b0af14 100644 --- a/src/providers/reliable.rs +++ b/src/providers/reliable.rs @@ -25,6 +25,14 @@ impl ReliableProvider { #[async_trait] impl Provider for ReliableProvider { + async fn warmup(&self) -> anyhow::Result<()> { + if let Some((name, provider)) = self.providers.first() { + tracing::info!(provider = name, "Warming up provider connection pool"); + provider.warmup().await?; + } + Ok(()) + } + async fn chat_with_system( &self, system_prompt: Option<&str>, diff --git a/src/providers/traits.rs b/src/providers/traits.rs index 8a24714..ff9adad 100644 --- a/src/providers/traits.rs +++ b/src/providers/traits.rs @@ -14,4 +14,10 @@ pub trait Provider: Send + Sync { model: &str, temperature: f64, ) -> anyhow::Result; + + /// Warm up the HTTP connection pool (TLS handshake, DNS, HTTP/2 setup). + /// Default implementation is a no-op; providers with HTTP clients should override. + async fn warmup(&self) -> anyhow::Result<()> { + Ok(()) + } }