merge: resolve conflicts between feat/whatsapp-email-channels and main

- Keep main's WhatsApp implementation (webhook-based, simpler)
- Preserve email channel fixes from our branch
- Merge all main branch updates (daemon, cron, health, etc.)
- Resolve Cargo.lock conflicts
This commit is contained in:
argenis de la rosa 2026-02-14 14:59:16 -05:00
commit 4e6da51924
40 changed files with 6925 additions and 780 deletions

View file

@ -4,11 +4,13 @@ pub mod gemini;
pub mod ollama;
pub mod openai;
pub mod openrouter;
pub mod reliable;
pub mod traits;
pub use traits::Provider;
use compatible::{AuthStyle, OpenAiCompatibleProvider};
use reliable::ReliableProvider;
/// Factory: create the right provider from config
#[allow(clippy::too_many_lines)]
@ -114,6 +116,42 @@ pub fn create_provider(name: &str, api_key: Option<&str>) -> anyhow::Result<Box<
}
}
/// Create provider chain with retry and fallback behavior.
pub fn create_resilient_provider(
primary_name: &str,
api_key: Option<&str>,
reliability: &crate::config::ReliabilityConfig,
) -> anyhow::Result<Box<dyn Provider>> {
let mut providers: Vec<(String, Box<dyn Provider>)> = Vec::new();
providers.push((
primary_name.to_string(),
create_provider(primary_name, api_key)?,
));
for fallback in &reliability.fallback_providers {
if fallback == primary_name || providers.iter().any(|(name, _)| name == fallback) {
continue;
}
match create_provider(fallback, api_key) {
Ok(provider) => providers.push((fallback.clone(), provider)),
Err(e) => {
tracing::warn!(
fallback_provider = fallback,
"Ignoring invalid fallback provider: {e}"
);
}
}
}
Ok(Box::new(ReliableProvider::new(
providers,
reliability.provider_retries,
reliability.provider_backoff_ms,
)))
}
#[cfg(test)]
mod tests {
use super::*;
@ -307,6 +345,34 @@ mod tests {
assert!(create_provider("", None).is_err());
}
#[test]
fn resilient_provider_ignores_duplicate_and_invalid_fallbacks() {
let reliability = crate::config::ReliabilityConfig {
provider_retries: 1,
provider_backoff_ms: 100,
fallback_providers: vec![
"openrouter".into(),
"nonexistent-provider".into(),
"openai".into(),
"openai".into(),
],
channel_initial_backoff_secs: 2,
channel_max_backoff_secs: 60,
scheduler_poll_secs: 15,
scheduler_retries: 2,
};
let provider = create_resilient_provider("openrouter", Some("sk-test"), &reliability);
assert!(provider.is_ok());
}
#[test]
fn resilient_provider_errors_for_invalid_primary() {
let reliability = crate::config::ReliabilityConfig::default();
let provider = create_resilient_provider("totally-invalid", Some("sk-test"), &reliability);
assert!(provider.is_err());
}
#[test]
fn factory_all_providers_create_successfully() {
let providers = [