Skip to content

Commit 200aed1

Browse files
zmanianclaude
andauthored
feat: configurable LLM request timeout via LLM_REQUEST_TIMEOUT_SECS (#615) (#630)
Add LLM_REQUEST_TIMEOUT_SECS env var (default: 120) to configure the HTTP request timeout for LLM API calls. Primarily useful for local models (Ollama, vLLM, LM Studio) that need more time for prompt evaluation on consumer hardware. The timeout is applied to the NearAI provider's HTTP client. Other providers (Anthropic, OpenAI) use rig-core's default client. - Add request_timeout_secs field to LlmConfig - Thread timeout through create_llm_provider -> NearAiChatProvider - Add NearAiChatProvider::new_with_timeout constructor - Add .env.example documentation - 2 regression tests for default and custom timeout values Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4c0275b commit 200aed1

5 files changed

Lines changed: 72 additions & 8 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DATABASE_POOL_SIZE=10
55
# LLM Provider
66
# LLM_BACKEND=nearai # default
77
# Possible values: nearai, ollama, openai_compatible, openai, anthropic, tinfoil
8+
# LLM_REQUEST_TIMEOUT_SECS=120 # Increase for local LLMs (Ollama, vLLM, LM Studio)
89

910
# === Anthropic Direct ===
1011
# Two auth modes:

src/config/llm.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ pub struct LlmConfig {
103103
/// Resolved provider config for registry-based providers.
104104
/// `None` when backend is "nearai".
105105
pub provider: Option<RegistryProviderConfig>,
106+
/// HTTP request timeout in seconds for LLM API calls.
107+
/// Default: 120. Increase for local LLMs (Ollama, vLLM, LM Studio) that
108+
/// need more time for prompt evaluation on consumer hardware.
109+
pub request_timeout_secs: u64,
106110
}
107111

108112
/// NEAR AI configuration.
@@ -165,6 +169,7 @@ impl LlmConfig {
165169
smart_routing_cascade: false,
166170
},
167171
provider: None,
172+
request_timeout_secs: 120,
168173
}
169174
}
170175

@@ -254,6 +259,8 @@ impl LlmConfig {
254259
)?)
255260
};
256261

262+
let request_timeout_secs = parse_optional_env("LLM_REQUEST_TIMEOUT_SECS", 120)?;
263+
257264
Ok(Self {
258265
backend: if is_nearai {
259266
"nearai".to_string()
@@ -265,6 +272,7 @@ impl LlmConfig {
265272
session,
266273
nearai,
267274
provider,
275+
request_timeout_secs,
268276
})
269277
}
270278

@@ -1016,4 +1024,30 @@ mod tests {
10161024
assert_eq!(parsed, variant, "round-trip failed for {s}");
10171025
}
10181026
}
1027+
1028+
#[test]
1029+
fn test_request_timeout_defaults_to_120() {
1030+
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
1031+
// SAFETY: Under ENV_MUTEX.
1032+
unsafe {
1033+
std::env::remove_var("LLM_REQUEST_TIMEOUT_SECS");
1034+
}
1035+
let config = LlmConfig::resolve(&Settings::default()).expect("resolve");
1036+
assert_eq!(config.request_timeout_secs, 120);
1037+
}
1038+
1039+
#[test]
1040+
fn test_request_timeout_configurable() {
1041+
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
1042+
// SAFETY: Under ENV_MUTEX.
1043+
unsafe {
1044+
std::env::set_var("LLM_REQUEST_TIMEOUT_SECS", "300");
1045+
}
1046+
let config = LlmConfig::resolve(&Settings::default()).expect("resolve");
1047+
assert_eq!(config.request_timeout_secs, 300);
1048+
// SAFETY: Cleanup
1049+
unsafe {
1050+
std::env::remove_var("LLM_REQUEST_TIMEOUT_SECS");
1051+
}
1052+
}
10191053
}

src/llm/mod.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ pub fn create_llm_provider(
5858
config: &LlmConfig,
5959
session: Arc<SessionManager>,
6060
) -> Result<Arc<dyn LlmProvider>, LlmError> {
61+
let timeout = config.request_timeout_secs;
62+
6163
if config.backend == "nearai" || config.backend == "near_ai" || config.backend == "near" {
62-
return create_llm_provider_with_config(&config.nearai, session);
64+
return create_llm_provider_with_config(&config.nearai, session, timeout);
6365
}
6466

6567
let reg_config = config
@@ -79,6 +81,7 @@ pub fn create_llm_provider(
7981
pub fn create_llm_provider_with_config(
8082
config: &NearAiConfig,
8183
session: Arc<SessionManager>,
84+
request_timeout_secs: u64,
8285
) -> Result<Arc<dyn LlmProvider>, LlmError> {
8386
let auth_mode = if config.api_key.is_some() {
8487
"API key"
@@ -89,9 +92,14 @@ pub fn create_llm_provider_with_config(
8992
model = %config.model,
9093
base_url = %config.base_url,
9194
auth = auth_mode,
95+
timeout_secs = request_timeout_secs,
9296
"Using NEAR AI (Chat Completions API)"
9397
);
94-
Ok(Arc::new(NearAiChatProvider::new(config.clone(), session)?))
98+
Ok(Arc::new(NearAiChatProvider::new_with_timeout(
99+
config.clone(),
100+
session,
101+
request_timeout_secs,
102+
)?))
95103
}
96104

97105
/// Create a provider from a registry-resolved config.
@@ -365,7 +373,11 @@ pub fn build_provider_chain(
365373
let llm: Arc<dyn LlmProvider> = if let Some(ref cheap_model) = config.nearai.cheap_model {
366374
let mut cheap_config = config.nearai.clone();
367375
cheap_config.model = cheap_model.clone();
368-
let cheap = create_llm_provider_with_config(&cheap_config, session.clone())?;
376+
let cheap = create_llm_provider_with_config(
377+
&cheap_config,
378+
session.clone(),
379+
config.request_timeout_secs,
380+
)?;
369381
let cheap: Arc<dyn LlmProvider> = if retry_config.max_retries > 0 {
370382
Arc::new(RetryProvider::new(cheap, retry_config.clone()))
371383
} else {
@@ -397,7 +409,11 @@ pub fn build_provider_chain(
397409
}
398410
let mut fallback_config = config.nearai.clone();
399411
fallback_config.model = fallback_model.clone();
400-
let fallback = create_llm_provider_with_config(&fallback_config, session.clone())?;
412+
let fallback = create_llm_provider_with_config(
413+
&fallback_config,
414+
session.clone(),
415+
config.request_timeout_secs,
416+
)?;
401417
tracing::info!(
402418
primary = %llm.model_name(),
403419
fallback = %fallback.model_name(),
@@ -503,6 +519,7 @@ mod tests {
503519
session: SessionConfig::default(),
504520
nearai: test_nearai_config(),
505521
provider: None,
522+
request_timeout_secs: 120,
506523
}
507524
}
508525

src/llm/nearai_chat.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,28 @@ impl NearAiChatProvider {
5858
/// By default this enables tool-message flattening for compatibility with
5959
/// providers that reject `role: "tool"` messages.
6060
pub fn new(config: NearAiConfig, session: Arc<SessionManager>) -> Result<Self, LlmError> {
61-
Self::new_with_flatten(config, session, true)
61+
Self::new_with_options(config, session, true, 120)
6262
}
6363

64-
/// Create a chat completions provider with configurable tool-message flattening.
65-
pub fn new_with_flatten(
64+
/// Create a new provider with a custom request timeout.
65+
pub fn new_with_timeout(
66+
config: NearAiConfig,
67+
session: Arc<SessionManager>,
68+
request_timeout_secs: u64,
69+
) -> Result<Self, LlmError> {
70+
Self::new_with_options(config, session, true, request_timeout_secs)
71+
}
72+
73+
/// Create a chat completions provider with configurable tool-message flattening
74+
/// and request timeout.
75+
pub fn new_with_options(
6676
config: NearAiConfig,
6777
session: Arc<SessionManager>,
6878
flatten_tool_messages: bool,
79+
request_timeout_secs: u64,
6980
) -> Result<Self, LlmError> {
7081
let client = Client::builder()
71-
.timeout(std::time::Duration::from_secs(120))
82+
.timeout(std::time::Duration::from_secs(request_timeout_secs))
7283
.build()
7384
.map_err(|e| LlmError::RequestFailed {
7485
provider: "nearai_chat".to_string(),

src/setup/wizard.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,6 +1491,7 @@ impl SetupWizard {
14911491
smart_routing_cascade: true,
14921492
},
14931493
provider: None,
1494+
request_timeout_secs: 120,
14941495
};
14951496

14961497
match create_llm_provider(&config, session) {

0 commit comments

Comments
 (0)