From d10c1324a14a05708bc9e958edbb7cb5b74f305b Mon Sep 17 00:00:00 2001 From: nb213 Date: Mon, 13 Jul 2026 21:38:44 +0800 Subject: [PATCH 1/3] Add Atlas Cloud LLM provider --- crates/webclaw-cli/src/main.rs | 14 +++- crates/webclaw-llm/src/chain.rs | 13 ++-- .../webclaw-llm/src/providers/atlascloud.rs | 71 +++++++++++++++++++ crates/webclaw-llm/src/providers/mod.rs | 1 + 4 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 crates/webclaw-llm/src/providers/atlascloud.rs diff --git a/crates/webclaw-cli/src/main.rs b/crates/webclaw-cli/src/main.rs index e3434c5..480af3b 100644 --- a/crates/webclaw-cli/src/main.rs +++ b/crates/webclaw-cli/src/main.rs @@ -338,7 +338,7 @@ struct Cli { #[arg(long, num_args = 0..=1, default_missing_value = "3")] summarize: Option, - /// Force a specific LLM provider (ollama, openai, anthropic) + /// Force a specific LLM provider (ollama, openai, atlascloud, anthropic) #[arg(long, env = "WEBCLAW_LLM_PROVIDER")] llm_provider: Option, @@ -2239,6 +2239,14 @@ async fn build_llm_provider(cli: &Cli) -> Result, String> { .ok_or("OPENAI_API_KEY not set")?; Ok(Box::new(provider)) } + "atlascloud" => { + let provider = webclaw_llm::providers::atlascloud::AtlasCloudProvider::new( + None, + cli.llm_model.clone(), + ) + .ok_or("ATLASCLOUD_API_KEY not set")?; + Ok(Box::new(provider)) + } "anthropic" => { let provider = webclaw_llm::providers::anthropic::AnthropicProvider::with_base_url( None, @@ -2249,14 +2257,14 @@ async fn build_llm_provider(cli: &Cli) -> Result, String> { Ok(Box::new(provider)) } other => Err(format!( - "unknown LLM provider: {other} (use ollama, openai, or anthropic)" + "unknown LLM provider: {other} (use ollama, openai, atlascloud, or anthropic)" )), } } else { let chain = webclaw_llm::ProviderChain::default().await; if chain.is_empty() { return Err( - "no LLM providers available -- start Ollama or set OPENAI_API_KEY / ANTHROPIC_API_KEY" + "no LLM providers available -- start Ollama or set OPENAI_API_KEY / ATLASCLOUD_API_KEY / ANTHROPIC_API_KEY" .into(), ); } diff --git a/crates/webclaw-llm/src/chain.rs b/crates/webclaw-llm/src/chain.rs index e2c6b8b..ecdbb16 100644 --- a/crates/webclaw-llm/src/chain.rs +++ b/crates/webclaw-llm/src/chain.rs @@ -1,5 +1,5 @@ /// Provider chain — tries providers in order until one succeeds. -/// Default order: Ollama (local, free) -> OpenAI -> Gemini -> Anthropic. +/// Default order: Ollama (local, free) -> OpenAI -> Atlas Cloud -> Gemini -> Anthropic. /// Only includes providers that are actually configured/available. use async_trait::async_trait; use tracing::{debug, warn}; @@ -7,8 +7,8 @@ use tracing::{debug, warn}; use crate::error::LlmError; use crate::provider::{CompletionRequest, LlmProvider}; use crate::providers::{ - anthropic::AnthropicProvider, gemini::GeminiProvider, ollama::OllamaProvider, - openai::OpenAiProvider, + anthropic::AnthropicProvider, atlascloud::AtlasCloudProvider, gemini::GeminiProvider, + ollama::OllamaProvider, openai::OpenAiProvider, }; pub struct ProviderChain { @@ -16,7 +16,7 @@ pub struct ProviderChain { } impl ProviderChain { - /// Build the default chain: Ollama -> OpenAI -> Gemini -> Anthropic. + /// Build the default chain: Ollama -> OpenAI -> Atlas Cloud -> Gemini -> Anthropic. /// Ollama is always added (availability checked at call time). /// Cloud providers are only added if their API keys are configured. /// Gemini sits ahead of Anthropic so Google Cloud credits are preferred, @@ -37,6 +37,11 @@ impl ProviderChain { providers.push(Box::new(openai)); } + if let Some(atlascloud) = AtlasCloudProvider::new(None, None) { + debug!("atlascloud configured, adding to chain"); + providers.push(Box::new(atlascloud)); + } + if let Some(gemini) = GeminiProvider::new(None, None, None) { debug!("gemini configured, adding to chain"); providers.push(Box::new(gemini)); diff --git a/crates/webclaw-llm/src/providers/atlascloud.rs b/crates/webclaw-llm/src/providers/atlascloud.rs new file mode 100644 index 0000000..5753014 --- /dev/null +++ b/crates/webclaw-llm/src/providers/atlascloud.rs @@ -0,0 +1,71 @@ +/// Atlas Cloud provider — OpenAI-compatible chat completions with Atlas defaults. +use async_trait::async_trait; + +use crate::error::LlmError; +use crate::provider::{CompletionRequest, LlmProvider}; + +use super::openai::OpenAiProvider; + +pub struct AtlasCloudProvider { + inner: OpenAiProvider, +} + +impl AtlasCloudProvider { + /// Returns `None` if no Atlas Cloud API key is available (param or env). + pub fn new(key_override: Option, model: Option) -> Option { + let key = super::load_api_key(key_override, "ATLASCLOUD_API_KEY")?; + let base_url = std::env::var("ATLASCLOUD_BASE_URL") + .ok() + .unwrap_or_else(|| "https://api.atlascloud.ai/v1".into()); + let model = model.unwrap_or_else(|| "qwen/qwen3.5-flash".into()); + let inner = OpenAiProvider::new(Some(key), Some(base_url), Some(model))?; + Some(Self { inner }) + } + + pub fn default_model(&self) -> &str { + self.inner.default_model() + } +} + +#[async_trait] +impl LlmProvider for AtlasCloudProvider { + async fn complete(&self, request: &CompletionRequest) -> Result { + self.inner.complete(request).await + } + + async fn is_available(&self) -> bool { + self.inner.is_available().await + } + + fn name(&self) -> &str { + "atlascloud" + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_key_returns_none() { + assert!(AtlasCloudProvider::new(Some(String::new()), None).is_none()); + } + + #[test] + fn explicit_key_constructs_with_atlas_defaults() { + let provider = AtlasCloudProvider::new(Some("test-key".into()), None) + .expect("should construct"); + assert_eq!(provider.name(), "atlascloud"); + assert_eq!(provider.default_model(), "qwen/qwen3.5-flash"); + } + + #[test] + fn explicit_model_override() { + let provider = AtlasCloudProvider::new( + Some("test-key".into()), + Some("deepseek-ai/deepseek-v4-pro".into()), + ) + .expect("should construct"); + assert_eq!(provider.default_model(), "deepseek-ai/deepseek-v4-pro"); + } +} diff --git a/crates/webclaw-llm/src/providers/mod.rs b/crates/webclaw-llm/src/providers/mod.rs index d6ae34a..7a5dd65 100644 --- a/crates/webclaw-llm/src/providers/mod.rs +++ b/crates/webclaw-llm/src/providers/mod.rs @@ -1,4 +1,5 @@ pub mod anthropic; +pub mod atlascloud; pub mod gemini; pub mod ollama; pub mod openai; From f1713514c8861eb9acccf5eb3b4729318e2e77df Mon Sep 17 00:00:00 2001 From: nb213 Date: Mon, 13 Jul 2026 21:39:47 +0800 Subject: [PATCH 2/3] Support Atlas Cloud base URL override --- crates/webclaw-cli/src/main.rs | 1 + crates/webclaw-llm/src/chain.rs | 2 +- crates/webclaw-llm/src/providers/atlascloud.rs | 15 ++++++++++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/webclaw-cli/src/main.rs b/crates/webclaw-cli/src/main.rs index 480af3b..3076e5e 100644 --- a/crates/webclaw-cli/src/main.rs +++ b/crates/webclaw-cli/src/main.rs @@ -2242,6 +2242,7 @@ async fn build_llm_provider(cli: &Cli) -> Result, String> { "atlascloud" => { let provider = webclaw_llm::providers::atlascloud::AtlasCloudProvider::new( None, + cli.llm_base_url.clone(), cli.llm_model.clone(), ) .ok_or("ATLASCLOUD_API_KEY not set")?; diff --git a/crates/webclaw-llm/src/chain.rs b/crates/webclaw-llm/src/chain.rs index ecdbb16..4b6e9db 100644 --- a/crates/webclaw-llm/src/chain.rs +++ b/crates/webclaw-llm/src/chain.rs @@ -37,7 +37,7 @@ impl ProviderChain { providers.push(Box::new(openai)); } - if let Some(atlascloud) = AtlasCloudProvider::new(None, None) { + if let Some(atlascloud) = AtlasCloudProvider::new(None, None, None) { debug!("atlascloud configured, adding to chain"); providers.push(Box::new(atlascloud)); } diff --git a/crates/webclaw-llm/src/providers/atlascloud.rs b/crates/webclaw-llm/src/providers/atlascloud.rs index 5753014..01e680c 100644 --- a/crates/webclaw-llm/src/providers/atlascloud.rs +++ b/crates/webclaw-llm/src/providers/atlascloud.rs @@ -12,10 +12,14 @@ pub struct AtlasCloudProvider { impl AtlasCloudProvider { /// Returns `None` if no Atlas Cloud API key is available (param or env). - pub fn new(key_override: Option, model: Option) -> Option { + pub fn new( + key_override: Option, + base_url: Option, + model: Option, + ) -> Option { let key = super::load_api_key(key_override, "ATLASCLOUD_API_KEY")?; - let base_url = std::env::var("ATLASCLOUD_BASE_URL") - .ok() + let base_url = base_url + .or_else(|| std::env::var("ATLASCLOUD_BASE_URL").ok()) .unwrap_or_else(|| "https://api.atlascloud.ai/v1".into()); let model = model.unwrap_or_else(|| "qwen/qwen3.5-flash".into()); let inner = OpenAiProvider::new(Some(key), Some(base_url), Some(model))?; @@ -48,12 +52,12 @@ mod tests { #[test] fn empty_key_returns_none() { - assert!(AtlasCloudProvider::new(Some(String::new()), None).is_none()); + assert!(AtlasCloudProvider::new(Some(String::new()), None, None).is_none()); } #[test] fn explicit_key_constructs_with_atlas_defaults() { - let provider = AtlasCloudProvider::new(Some("test-key".into()), None) + let provider = AtlasCloudProvider::new(Some("test-key".into()), None, None) .expect("should construct"); assert_eq!(provider.name(), "atlascloud"); assert_eq!(provider.default_model(), "qwen/qwen3.5-flash"); @@ -63,6 +67,7 @@ mod tests { fn explicit_model_override() { let provider = AtlasCloudProvider::new( Some("test-key".into()), + Some("https://proxy.example.com/v1".into()), Some("deepseek-ai/deepseek-v4-pro".into()), ) .expect("should construct"); From d053babca3487e4922725a2b6cd483ba59c222f3 Mon Sep 17 00:00:00 2001 From: nb213 Date: Mon, 13 Jul 2026 21:40:57 +0800 Subject: [PATCH 3/3] Apply rustfmt to Atlas Cloud provider --- crates/webclaw-llm/src/providers/atlascloud.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/webclaw-llm/src/providers/atlascloud.rs b/crates/webclaw-llm/src/providers/atlascloud.rs index 01e680c..a234ec9 100644 --- a/crates/webclaw-llm/src/providers/atlascloud.rs +++ b/crates/webclaw-llm/src/providers/atlascloud.rs @@ -57,8 +57,8 @@ mod tests { #[test] fn explicit_key_constructs_with_atlas_defaults() { - let provider = AtlasCloudProvider::new(Some("test-key".into()), None, None) - .expect("should construct"); + let provider = + AtlasCloudProvider::new(Some("test-key".into()), None, None).expect("should construct"); assert_eq!(provider.name(), "atlascloud"); assert_eq!(provider.default_model(), "qwen/qwen3.5-flash"); }