diff --git a/crates/webclaw-cli/src/main.rs b/crates/webclaw-cli/src/main.rs index e3434c5..ea2aac5 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,15 @@ 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_base_url.clone(), + 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,7 +2258,7 @@ 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 { diff --git a/crates/webclaw-llm/src/providers/atlascloud.rs b/crates/webclaw-llm/src/providers/atlascloud.rs new file mode 100644 index 0000000..a234ec9 --- /dev/null +++ b/crates/webclaw-llm/src/providers/atlascloud.rs @@ -0,0 +1,76 @@ +/// 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, + base_url: Option, + model: Option, + ) -> Option { + let key = super::load_api_key(key_override, "ATLASCLOUD_API_KEY")?; + 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))?; + 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, None).is_none()); + } + + #[test] + fn explicit_key_constructs_with_atlas_defaults() { + 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"); + } + + #[test] + 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"); + 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;