fix: increase Ollama retry config + add transient-only mode (#7677)

Signed-off-by: fre <anonwurcod@proton.me>
This commit is contained in:
fre$h
2026-03-26 12:56:57 -04:00
committed by GitHub
parent f5ad3384b8
commit dfbd2dd7c6
5 changed files with 134 additions and 14 deletions
+2 -2
View File
@@ -181,12 +181,12 @@ impl BedrockProvider {
.get_param::<u64>("BEDROCK_MAX_RETRY_INTERVAL_MS") .get_param::<u64>("BEDROCK_MAX_RETRY_INTERVAL_MS")
.unwrap_or(BEDROCK_DEFAULT_MAX_RETRY_INTERVAL_MS); .unwrap_or(BEDROCK_DEFAULT_MAX_RETRY_INTERVAL_MS);
RetryConfig { RetryConfig::new(
max_retries, max_retries,
initial_interval_ms, initial_interval_ms,
backoff_multiplier, backoff_multiplier,
max_interval_ms, max_interval_ms,
} )
} }
fn should_enable_caching(&self) -> bool { fn should_enable_caching(&self) -> bool {
+2 -2
View File
@@ -217,12 +217,12 @@ impl DatabricksProvider {
.and_then(|v: String| v.parse::<u64>().ok()) .and_then(|v: String| v.parse::<u64>().ok())
.unwrap_or(DEFAULT_MAX_RETRY_INTERVAL_MS); .unwrap_or(DEFAULT_MAX_RETRY_INTERVAL_MS);
RetryConfig { RetryConfig::new(
max_retries, max_retries,
initial_interval_ms, initial_interval_ms,
backoff_multiplier, backoff_multiplier,
max_interval_ms, max_interval_ms,
} )
} }
fn load_fast_retry_config(_config: &crate::config::Config) -> RetryConfig { fn load_fast_retry_config(_config: &crate::config::Config) -> RetryConfig {
+52 -1
View File
@@ -2,7 +2,7 @@ use super::api_client::{ApiClient, AuthMethod};
use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata}; use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata};
use super::errors::ProviderError; use super::errors::ProviderError;
use super::openai_compatible::handle_status_openai_compat; use super::openai_compatible::handle_status_openai_compat;
use super::retry::ProviderRetry; use super::retry::{ProviderRetry, RetryConfig};
use super::utils::{ImageFormat, RequestLog}; use super::utils::{ImageFormat, RequestLog};
use crate::config::declarative_providers::DeclarativeProviderConfig; use crate::config::declarative_providers::DeclarativeProviderConfig;
use crate::conversation::message::Message; use crate::conversation::message::Message;
@@ -36,6 +36,14 @@ pub const OLLAMA_KNOWN_MODELS: &[&str] = &[
]; ];
pub const OLLAMA_DOC_URL: &str = "https://ollama.com/library"; pub const OLLAMA_DOC_URL: &str = "https://ollama.com/library";
// Ollama-specific retry config: large models can take 30-120s to load into memory,
// during which Ollama returns 500 errors. Use more retries with gradual backoff
// to wait for the model to become ready.
const OLLAMA_MAX_RETRIES: usize = 10;
const OLLAMA_INITIAL_RETRY_INTERVAL_MS: u64 = 2000;
const OLLAMA_BACKOFF_MULTIPLIER: f64 = 1.5;
const OLLAMA_MAX_RETRY_INTERVAL_MS: u64 = 15_000;
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub struct OllamaProvider { pub struct OllamaProvider {
#[serde(skip)] #[serde(skip)]
@@ -211,6 +219,16 @@ impl Provider for OllamaProvider {
self.model.clone() self.model.clone()
} }
fn retry_config(&self) -> RetryConfig {
RetryConfig::new(
OLLAMA_MAX_RETRIES,
OLLAMA_INITIAL_RETRY_INTERVAL_MS,
OLLAMA_BACKOFF_MULTIPLIER,
OLLAMA_MAX_RETRY_INTERVAL_MS,
)
.transient_only()
}
async fn stream( async fn stream(
&self, &self,
model_config: &ModelConfig, model_config: &ModelConfig,
@@ -340,4 +358,37 @@ mod tests {
apply_ollama_options(&mut payload, &model_config); apply_ollama_options(&mut payload, &model_config);
assert!(payload.get("options").is_none()); assert!(payload.get("options").is_none());
} }
#[test]
fn test_ollama_retry_config_is_transient_only() {
let config = RetryConfig::new(
OLLAMA_MAX_RETRIES,
OLLAMA_INITIAL_RETRY_INTERVAL_MS,
OLLAMA_BACKOFF_MULTIPLIER,
OLLAMA_MAX_RETRY_INTERVAL_MS,
)
.transient_only();
assert!(config.transient_only);
use super::super::errors::ProviderError;
use super::super::retry::should_retry;
assert!(!should_retry(
&ProviderError::RequestFailed("Resource not found (404)".into()),
&config
));
assert!(!should_retry(
&ProviderError::RequestFailed("Bad request (400)".into()),
&config
));
assert!(should_retry(
&ProviderError::ServerError("500 model loading".into()),
&config
));
assert!(should_retry(
&ProviderError::NetworkError("connection refused".into()),
&config
));
}
} }
+77 -9
View File
@@ -20,6 +20,9 @@ pub struct RetryConfig {
pub(crate) backoff_multiplier: f64, pub(crate) backoff_multiplier: f64,
/// Maximum interval between retries in milliseconds /// Maximum interval between retries in milliseconds
pub(crate) max_interval_ms: u64, pub(crate) max_interval_ms: u64,
/// When true, only retry on transient errors (ServerError, NetworkError,
/// RateLimitExceeded). RequestFailed (4xx client errors) will not be retried.
pub(crate) transient_only: bool,
} }
impl Default for RetryConfig { impl Default for RetryConfig {
@@ -29,6 +32,7 @@ impl Default for RetryConfig {
initial_interval_ms: DEFAULT_INITIAL_RETRY_INTERVAL_MS, initial_interval_ms: DEFAULT_INITIAL_RETRY_INTERVAL_MS,
backoff_multiplier: DEFAULT_BACKOFF_MULTIPLIER, backoff_multiplier: DEFAULT_BACKOFF_MULTIPLIER,
max_interval_ms: DEFAULT_MAX_RETRY_INTERVAL_MS, max_interval_ms: DEFAULT_MAX_RETRY_INTERVAL_MS,
transient_only: false,
} }
} }
} }
@@ -45,9 +49,15 @@ impl RetryConfig {
initial_interval_ms, initial_interval_ms,
backoff_multiplier, backoff_multiplier,
max_interval_ms, max_interval_ms,
transient_only: false,
} }
} }
pub fn transient_only(mut self) -> Self {
self.transient_only = true;
self
}
pub fn max_retries(&self) -> usize { pub fn max_retries(&self) -> usize {
self.max_retries self.max_retries
} }
@@ -71,14 +81,14 @@ impl RetryConfig {
} }
} }
pub fn should_retry(error: &ProviderError) -> bool { pub fn should_retry(error: &ProviderError, config: &RetryConfig) -> bool {
matches!( match error {
error,
ProviderError::RateLimitExceeded { .. } ProviderError::RateLimitExceeded { .. }
| ProviderError::ServerError(_) | ProviderError::ServerError(_)
| ProviderError::NetworkError(_) | ProviderError::NetworkError(_) => true,
| ProviderError::RequestFailed(_) ProviderError::RequestFailed(_) => !config.transient_only,
) _ => false,
}
} }
pub async fn retry_operation<F, Fut, T>( pub async fn retry_operation<F, Fut, T>(
@@ -96,7 +106,7 @@ where
match operation().await { match operation().await {
Ok(result) => return Ok(result), Ok(result) => return Ok(result),
Err(error) => { Err(error) => {
if should_retry(&error) && attempts < config.max_retries { if should_retry(&error, config) && attempts < config.max_retries {
attempts += 1; attempts += 1;
tracing::warn!( tracing::warn!(
"Request failed, retrying ({}/{}): {:?}", "Request failed, retrying ({}/{}): {:?}",
@@ -195,7 +205,7 @@ impl<P: Provider> ProviderRetry for P {
} }
} }
if should_retry(&error) && attempts < config.max_retries { if should_retry(&error, &config) && attempts < config.max_retries {
attempts += 1; attempts += 1;
tracing::warn!( tracing::warn!(
"Request failed, retrying ({}/{}): {:?}", "Request failed, retrying ({}/{}): {:?}",
@@ -232,3 +242,61 @@ impl<P: Provider> ProviderRetry for P {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_retries_request_failed() {
let config = RetryConfig::default();
let error = ProviderError::RequestFailed("Bad request (400): model not found".into());
assert!(should_retry(&error, &config));
}
#[test]
fn transient_only_skips_request_failed() {
let config = RetryConfig::default().transient_only();
let error = ProviderError::RequestFailed("Bad request (400): model not found".into());
assert!(!should_retry(&error, &config));
}
#[test]
fn transient_only_still_retries_server_error() {
let config = RetryConfig::default().transient_only();
assert!(should_retry(
&ProviderError::ServerError("500 internal".into()),
&config
));
}
#[test]
fn transient_only_still_retries_network_error() {
let config = RetryConfig::default().transient_only();
assert!(should_retry(
&ProviderError::NetworkError("connection refused".into()),
&config
));
}
#[test]
fn transient_only_still_retries_rate_limit() {
let config = RetryConfig::default().transient_only();
assert!(should_retry(
&ProviderError::RateLimitExceeded {
details: "too many requests".into(),
retry_delay: None,
},
&config
));
}
#[test]
fn never_retries_auth_errors() {
let config = RetryConfig::default();
assert!(!should_retry(
&ProviderError::Authentication("invalid key".into()),
&config
));
}
}
+1
View File
@@ -0,0 +1 @@
registry=https://registry.npmjs.org/