Add Atlas Cloud LLM provider

This commit is contained in:
nb213 2026-07-13 21:38:44 +08:00 committed by Valerio
parent 4072861677
commit 0124ab9df3
4 changed files with 92 additions and 7 deletions

View file

@ -338,7 +338,7 @@ struct Cli {
#[arg(long, num_args = 0..=1, default_missing_value = "3")]
summarize: Option<usize>,
/// 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<String>,
@ -2239,6 +2239,14 @@ async fn build_llm_provider(cli: &Cli) -> Result<Box<dyn LlmProvider>, 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<Box<dyn LlmProvider>, 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(),
);
}

View file

@ -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));

View file

@ -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<String>, model: Option<String>) -> Option<Self> {
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<String, LlmError> {
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");
}
}

View file

@ -1,4 +1,5 @@
pub mod anthropic;
pub mod atlascloud;
pub mod gemini;
pub mod ollama;
pub mod openai;